mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-29 00:52:01 +00:00
feat(nuxt,schema): support new Nuxt folder structure (#27029)
This commit is contained in:
parent
061fbd4bd6
commit
2c39b3ce61
@ -56,6 +56,10 @@ export default defineNuxtConfig({
|
|||||||
compatibilityVersion: 4,
|
compatibilityVersion: 4,
|
||||||
},
|
},
|
||||||
// To re-enable _all_ Nuxt v3 behaviour, set the following options:
|
// To re-enable _all_ Nuxt v3 behaviour, set the following options:
|
||||||
|
srcDir: '.',
|
||||||
|
dir: {
|
||||||
|
app: 'app'
|
||||||
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
compileTemplate: true,
|
compileTemplate: true,
|
||||||
templateUtils: true,
|
templateUtils: true,
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { resolve } from 'pathe'
|
|
||||||
import type { JSValue } from 'untyped'
|
import type { JSValue } from 'untyped'
|
||||||
import { applyDefaults } from 'untyped'
|
import { applyDefaults } from 'untyped'
|
||||||
import type { LoadConfigOptions } from 'c12'
|
import type { ConfigLayer, ConfigLayerMeta, LoadConfigOptions } from 'c12'
|
||||||
import { loadConfig } from 'c12'
|
import { loadConfig } from 'c12'
|
||||||
import type { NuxtConfig, NuxtOptions } from '@nuxt/schema'
|
import type { NuxtConfig, NuxtOptions } from '@nuxt/schema'
|
||||||
import { NuxtConfigSchema } from '@nuxt/schema'
|
import { NuxtConfigSchema } from '@nuxt/schema'
|
||||||
|
|
||||||
export interface LoadNuxtConfigOptions extends LoadConfigOptions<NuxtConfig> {}
|
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> {
|
export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<NuxtOptions> {
|
||||||
(globalThis as any).defineNuxtConfig = (c: any) => c
|
(globalThis as any).defineNuxtConfig = (c: any) => c
|
||||||
const result = await loadConfig<NuxtConfig>({
|
const result = await loadConfig<NuxtConfig>({
|
||||||
@ -28,15 +30,20 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
|
|||||||
nuxtConfig._nuxtConfigFile = configFile
|
nuxtConfig._nuxtConfigFile = configFile
|
||||||
nuxtConfig._nuxtConfigFiles = [configFile]
|
nuxtConfig._nuxtConfigFiles = [configFile]
|
||||||
|
|
||||||
// Resolve `rootDir` & `srcDir` of layers
|
const _layers: ConfigLayer<NuxtConfig, ConfigLayerMeta>[] = []
|
||||||
for (const layer of layers) {
|
for (const layer of layers) {
|
||||||
|
// Resolve `rootDir` & `srcDir` of layers
|
||||||
layer.config = layer.config || {}
|
layer.config = layer.config || {}
|
||||||
layer.config.rootDir = layer.config.rootDir ?? layer.cwd
|
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
|
||||||
|
if (!layer.configFile || layer.configFile.endsWith('.nuxtrc')) { continue }
|
||||||
|
_layers.push(layer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter layers
|
|
||||||
const _layers = layers.filter(layer => layer.configFile && !layer.configFile.endsWith('.nuxtrc'))
|
|
||||||
;(nuxtConfig as any)._layers = _layers
|
;(nuxtConfig as any)._layers = _layers
|
||||||
|
|
||||||
// Ensure at least one layer remains (without nuxt.config)
|
// Ensure at least one layer remains (without nuxt.config)
|
||||||
|
@ -154,7 +154,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
|||||||
baseURL: nuxt.options.app.buildAssetsDir,
|
baseURL: nuxt.options.app.buildAssetsDir,
|
||||||
},
|
},
|
||||||
...nuxt.options._layers
|
...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))
|
.filter(dir => existsSync(dir))
|
||||||
.map(dir => ({ dir })),
|
.map(dir => ({ dir })),
|
||||||
],
|
],
|
||||||
@ -570,9 +570,9 @@ async function spaLoadingTemplatePath (nuxt: Nuxt) {
|
|||||||
return resolve(nuxt.options.srcDir, nuxt.options.spaLoadingTemplate)
|
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) {
|
async function spaLoadingTemplate (nuxt: Nuxt) {
|
||||||
|
@ -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
|
// Core Nuxt files: app.vue, error.vue and app.config.ts
|
||||||
const isFileChange = ['add', 'unlink'].includes(event)
|
const isFileChange = ['add', 'unlink'].includes(event)
|
||||||
if (isFileChange && RESTART_RE.test(path)) {
|
if (isFileChange && RESTART_RE.test(path)) {
|
||||||
|
@ -34,7 +34,7 @@ export default defineNuxtModule({
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const layer of nuxt.options._layers) {
|
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 }) }
|
if (path) { context.files.unshift({ path }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,8 +86,8 @@ export default defineNuxtModule({
|
|||||||
const restartPaths = nuxt.options._layers.flatMap((layer) => {
|
const restartPaths = nuxt.options._layers.flatMap((layer) => {
|
||||||
const pagesDir = (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.pages || 'pages'
|
const pagesDir = (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.pages || 'pages'
|
||||||
return [
|
return [
|
||||||
join(layer.config.srcDir || layer.cwd, 'app/router.options.ts'),
|
resolve(layer.config.srcDir || layer.cwd, layer.config.dir?.app || 'app', 'router.options.ts'),
|
||||||
join(layer.config.srcDir || layer.cwd, pagesDir),
|
resolve(layer.config.srcDir || layer.cwd, pagesDir),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -228,9 +228,9 @@ export default defineNuxtModule({
|
|||||||
const updateTemplatePaths = nuxt.options._layers.flatMap((l) => {
|
const updateTemplatePaths = nuxt.options._layers.flatMap((l) => {
|
||||||
const dir = (l.config.rootDir === nuxt.options.rootDir ? nuxt.options : l.config).dir
|
const dir = (l.config.rootDir === nuxt.options.rootDir ? nuxt.options : l.config).dir
|
||||||
return [
|
return [
|
||||||
join(l.config.srcDir || l.cwd, dir?.pages || 'pages') + '/',
|
resolve(l.config.srcDir || l.cwd, dir?.pages || 'pages') + '/',
|
||||||
join(l.config.srcDir || l.cwd, dir?.layouts || 'layouts') + '/',
|
resolve(l.config.srcDir || l.cwd, dir?.layouts || 'layouts') + '/',
|
||||||
join(l.config.srcDir || l.cwd, dir?.middleware || 'middleware') + '/',
|
resolve(l.config.srcDir || l.cwd, dir?.middleware || 'middleware') + '/',
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { existsSync } from 'node:fs'
|
||||||
import { defineUntypedSchema } from 'untyped'
|
import { defineUntypedSchema } from 'untyped'
|
||||||
import { join, relative, resolve } from 'pathe'
|
import { join, relative, resolve } from 'pathe'
|
||||||
import { isDebug, isDevelopment, isTest } from 'std-env'
|
import { isDebug, isDevelopment, isTest } from 'std-env'
|
||||||
@ -88,7 +89,32 @@ export default defineUntypedSchema({
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
srcDir: {
|
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: {
|
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.
|
* It is better to stick with defaults unless needed.
|
||||||
*/
|
*/
|
||||||
dir: {
|
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).
|
* 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.
|
* 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.
|
* 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.
|
* and copied across into your `dist` folder when your app is generated.
|
||||||
*/
|
*/
|
||||||
public: {
|
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: {
|
static: {
|
||||||
|
@ -21,6 +21,10 @@ export default defineUntypedSchema({
|
|||||||
* compatibilityVersion: 4,
|
* compatibilityVersion: 4,
|
||||||
* },
|
* },
|
||||||
* // To re-enable _all_ Nuxt v3 behaviour, set the following options:
|
* // To re-enable _all_ Nuxt v3 behaviour, set the following options:
|
||||||
|
* srcDir: '.',
|
||||||
|
* dir: {
|
||||||
|
* app: 'app'
|
||||||
|
* },
|
||||||
* experimental: {
|
* experimental: {
|
||||||
* compileTemplate: true,
|
* compileTemplate: true,
|
||||||
* templateUtils: true,
|
* templateUtils: true,
|
||||||
|
90
packages/schema/test/folder-structure.spec.ts
Normal file
90
packages/schema/test/folder-structure.spec.ts
Normal 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!),
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user