feat(nuxt,schema): support new Nuxt folder structure (#27029)

This commit is contained in:
Daniel Roe 2024-05-02 14:24:31 +01:00 committed by GitHub
parent 061fbd4bd6
commit 2c39b3ce61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 183 additions and 19 deletions

View File

@ -56,6 +56,10 @@ export default defineNuxtConfig({
compatibilityVersion: 4,
},
// To re-enable _all_ Nuxt v3 behaviour, set the following options:
srcDir: '.',
dir: {
app: 'app'
},
experimental: {
compileTemplate: true,
templateUtils: true,

View File

@ -1,13 +1,15 @@
import { resolve } from 'pathe'
import type { JSValue } from 'untyped'
import { applyDefaults } from 'untyped'
import type { LoadConfigOptions } from 'c12'
import type { ConfigLayer, ConfigLayerMeta, LoadConfigOptions } from 'c12'
import { loadConfig } from 'c12'
import type { NuxtConfig, NuxtOptions } from '@nuxt/schema'
import { NuxtConfigSchema } from '@nuxt/schema'
export interface LoadNuxtConfigOptions extends LoadConfigOptions<NuxtConfig> {}
const layerSchemaKeys = ['future', 'srcDir', 'rootDir', 'dir']
const layerSchema = Object.fromEntries(Object.entries(NuxtConfigSchema).filter(([key]) => layerSchemaKeys.includes(key)))
export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<NuxtOptions> {
(globalThis as any).defineNuxtConfig = (c: any) => c
const result = await loadConfig<NuxtConfig>({
@ -28,15 +30,20 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
nuxtConfig._nuxtConfigFile = configFile
nuxtConfig._nuxtConfigFiles = [configFile]
// Resolve `rootDir` & `srcDir` of layers
const _layers: ConfigLayer<NuxtConfig, ConfigLayerMeta>[] = []
for (const layer of layers) {
// Resolve `rootDir` & `srcDir` of layers
layer.config = layer.config || {}
layer.config.rootDir = layer.config.rootDir ?? layer.cwd
layer.config.srcDir = resolve(layer.config.rootDir!, layer.config.srcDir!)
}
// Normalise layer directories
layer.config = await applyDefaults(layerSchema, layer.config as NuxtConfig & Record<string, JSValue>) as unknown as NuxtConfig
// Filter layers
const _layers = layers.filter(layer => layer.configFile && !layer.configFile.endsWith('.nuxtrc'))
if (!layer.configFile || layer.configFile.endsWith('.nuxtrc')) { continue }
_layers.push(layer)
}
;(nuxtConfig as any)._layers = _layers
// Ensure at least one layer remains (without nuxt.config)

View File

@ -154,7 +154,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
baseURL: nuxt.options.app.buildAssetsDir,
},
...nuxt.options._layers
.map(layer => join(layer.config.srcDir, (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.public || 'public'))
.map(layer => resolve(layer.config.srcDir, (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.public || 'public'))
.filter(dir => existsSync(dir))
.map(dir => ({ dir })),
],
@ -570,9 +570,9 @@ async function spaLoadingTemplatePath (nuxt: Nuxt) {
return resolve(nuxt.options.srcDir, nuxt.options.spaLoadingTemplate)
}
const possiblePaths = nuxt.options._layers.map(layer => join(layer.config.srcDir, 'app/spa-loading-template.html'))
const possiblePaths = nuxt.options._layers.map(layer => resolve(layer.config.srcDir, layer.config.dir?.app || 'app', 'spa-loading-template.html'))
return await findPath(possiblePaths) ?? resolve(nuxt.options.srcDir, 'app/spa-loading-template.html')
return await findPath(possiblePaths) ?? resolve(nuxt.options.srcDir, nuxt.options.dir?.app || 'app', 'spa-loading-template.html')
}
async function spaLoadingTemplate (nuxt: Nuxt) {

View File

@ -491,6 +491,12 @@ async function initNuxt (nuxt: Nuxt) {
}
}
// Restart Nuxt when new `app/` dir is added
if (event === 'addDir' && path === resolve(nuxt.options.srcDir, 'app')) {
logger.info(`\`${path}/\` ${event === 'addDir' ? 'created' : 'removed'}`)
return nuxt.callHook('restart', { hard: true })
}
// Core Nuxt files: app.vue, error.vue and app.config.ts
const isFileChange = ['add', 'unlink'].includes(event)
if (isFileChange && RESTART_RE.test(path)) {

View File

@ -34,7 +34,7 @@ export default defineNuxtModule({
}
for (const layer of nuxt.options._layers) {
const path = await findPath(resolve(layer.config.srcDir, 'app/router.options'))
const path = await findPath(resolve(layer.config.srcDir, layer.config.dir?.app || 'app', 'router.options'))
if (path) { context.files.unshift({ path }) }
}
@ -86,8 +86,8 @@ export default defineNuxtModule({
const restartPaths = nuxt.options._layers.flatMap((layer) => {
const pagesDir = (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.pages || 'pages'
return [
join(layer.config.srcDir || layer.cwd, 'app/router.options.ts'),
join(layer.config.srcDir || layer.cwd, pagesDir),
resolve(layer.config.srcDir || layer.cwd, layer.config.dir?.app || 'app', 'router.options.ts'),
resolve(layer.config.srcDir || layer.cwd, pagesDir),
]
})
@ -228,9 +228,9 @@ export default defineNuxtModule({
const updateTemplatePaths = nuxt.options._layers.flatMap((l) => {
const dir = (l.config.rootDir === nuxt.options.rootDir ? nuxt.options : l.config).dir
return [
join(l.config.srcDir || l.cwd, dir?.pages || 'pages') + '/',
join(l.config.srcDir || l.cwd, dir?.layouts || 'layouts') + '/',
join(l.config.srcDir || l.cwd, dir?.middleware || 'middleware') + '/',
resolve(l.config.srcDir || l.cwd, dir?.pages || 'pages') + '/',
resolve(l.config.srcDir || l.cwd, dir?.layouts || 'layouts') + '/',
resolve(l.config.srcDir || l.cwd, dir?.middleware || 'middleware') + '/',
]
})

View File

@ -1,3 +1,4 @@
import { existsSync } from 'node:fs'
import { defineUntypedSchema } from 'untyped'
import { join, relative, resolve } from 'pathe'
import { isDebug, isDevelopment, isTest } from 'std-env'
@ -88,7 +89,32 @@ export default defineUntypedSchema({
* ```
*/
srcDir: {
$resolve: async (val: string | undefined, get): Promise<string> => resolve(await get('rootDir') as string, val || '.'),
$resolve: async (val: string | undefined, get): Promise<string> => {
if (val) {
return resolve(await get('rootDir') as string, val)
}
const [rootDir, isV4] = await Promise.all([
get('rootDir') as Promise<string>,
(get('future') as Promise<Record<string, unknown>>).then(r => r.compatibilityVersion === 4),
])
if (!isV4) {
return rootDir
}
const srcDir = resolve(rootDir, 'app')
if (!existsSync(srcDir)) {
const keys = ['assets', 'layouts', 'middleware', 'pages', 'plugins'] as const
const dirs = await Promise.all(keys.map(key => get(`dir.${key}`) as Promise<string>))
for (const dir of dirs) {
if (existsSync(resolve(rootDir, dir))) {
return rootDir
}
}
}
return srcDir
},
},
/**
@ -99,7 +125,11 @@ export default defineUntypedSchema({
*
*/
serverDir: {
$resolve: async (val: string | undefined, get): Promise<string> => resolve(await get('rootDir') as string, val || resolve(await get('srcDir') as string, 'server')),
$resolve: async (val: string | undefined, get): Promise<string> => {
const isV4 = ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
return resolve(await get('rootDir') as string, (val || isV4) ? 'server' : resolve(await get('srcDir') as string, 'server'))
},
},
/**
@ -219,6 +249,15 @@ export default defineUntypedSchema({
* It is better to stick with defaults unless needed.
*/
dir: {
app: {
$resolve: async (val: string | undefined, get) => {
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4
if (isV4) {
return resolve(await get('srcDir') as string, val || '.')
}
return val || 'app'
},
},
/**
* The assets directory (aliased as `~assets` in your build).
*/
@ -237,7 +276,15 @@ export default defineUntypedSchema({
/**
* The modules directory, each file in which will be auto-registered as a Nuxt module.
*/
modules: 'modules',
modules: {
$resolve: async (val: string | undefined, get) => {
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4
if (isV4) {
return resolve(await get('rootDir') as string, val || 'modules')
}
return val || 'modules'
},
},
/**
* The directory which will be processed to auto-generate your application page routes.
@ -254,7 +301,13 @@ export default defineUntypedSchema({
* and copied across into your `dist` folder when your app is generated.
*/
public: {
$resolve: async (val, get) => val || await get('dir.static') || 'public',
$resolve: async (val: string | undefined, get) => {
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4
if (isV4) {
return resolve(await get('rootDir') as string, val || await get('dir.static') as string || 'public')
}
return val || await get('dir.static') as string || 'public'
},
},
static: {

View File

@ -21,6 +21,10 @@ export default defineUntypedSchema({
* compatibilityVersion: 4,
* },
* // To re-enable _all_ Nuxt v3 behaviour, set the following options:
* srcDir: '.',
* dir: {
* app: 'app'
* },
* experimental: {
* compileTemplate: true,
* templateUtils: true,

View File

@ -0,0 +1,90 @@
import { describe, expect, it } from 'vitest'
import { applyDefaults } from 'untyped'
import { NuxtConfigSchema } from '../src'
import type { NuxtOptions } from '../src'
describe('nuxt folder structure', () => {
it('should resolve directories for v3 setup correctly', async () => {
const result = await applyDefaults(NuxtConfigSchema, {})
expect(getDirs(result as unknown as NuxtOptions)).toMatchInlineSnapshot(`
{
"dir": {
"app": "app",
"modules": "modules",
"public": "public",
},
"rootDir": "<cwd>",
"serverDir": "<cwd>/server",
"srcDir": "<cwd>",
"workspaceDir": "<cwd>",
}
`)
})
it('should resolve directories with a custom `srcDir` and `rootDir`', async () => {
const result = await applyDefaults(NuxtConfigSchema, { srcDir: 'src/', rootDir: '/test' })
expect(getDirs(result as unknown as NuxtOptions)).toMatchInlineSnapshot(`
{
"dir": {
"app": "app",
"modules": "modules",
"public": "public",
},
"rootDir": "/test",
"serverDir": "/test/src/server",
"srcDir": "/test/src",
"workspaceDir": "/test",
}
`)
})
it('should resolve directories when opting-in to v4 schema', async () => {
const result = await applyDefaults(NuxtConfigSchema, { future: { compatibilityVersion: 4 } })
expect(getDirs(result as unknown as NuxtOptions)).toMatchInlineSnapshot(`
{
"dir": {
"app": "<cwd>/app",
"modules": "<cwd>/modules",
"public": "<cwd>/public",
},
"rootDir": "<cwd>",
"serverDir": "<cwd>/server",
"srcDir": "<cwd>/app",
"workspaceDir": "<cwd>",
}
`)
})
it('should resolve directories when opting-in to v4 schema with a custom `srcDir` and `rootDir`', async () => {
const result = await applyDefaults(NuxtConfigSchema, { future: { compatibilityVersion: 4 }, srcDir: 'customApp/', rootDir: '/test' })
expect(getDirs(result as unknown as NuxtOptions)).toMatchInlineSnapshot(`
{
"dir": {
"app": "/test/customApp",
"modules": "/test/modules",
"public": "/test/public",
},
"rootDir": "/test",
"serverDir": "/test/server",
"srcDir": "/test/customApp",
"workspaceDir": "/test",
}
`)
})
})
function getDirs (options: NuxtOptions) {
const stripRoot = (dir: string) => dir.replace(process.cwd(), '<cwd>')
return {
rootDir: stripRoot(options.rootDir),
serverDir: stripRoot(options.serverDir),
srcDir: stripRoot(options.srcDir),
dir: {
app: stripRoot(options.dir.app),
modules: stripRoot(options.dir.modules),
public: stripRoot(options.dir.public),
},
workspaceDir: stripRoot(options.workspaceDir!),
}
}