feat(schema): add runtime + internal type validation (#30844)

This commit is contained in:
Daniel Roe 2025-02-10 16:28:05 +00:00
parent 5a71ef8ace
commit 6a4b7232fc
No known key found for this signature in database
GPG Key ID: 3714AB03996F442B
20 changed files with 550 additions and 240 deletions

View File

@ -7,7 +7,7 @@ import escapeRE from 'escape-string-regexp'
import { hash } from 'ohash'
import { camelCase } from 'scule'
import { filename } from 'pathe/utils'
import type { NuxtTemplate, NuxtTypeTemplate } from 'nuxt/schema'
import type { NuxtOptions, NuxtTemplate, NuxtTypeTemplate } from 'nuxt/schema'
import type { Nitro } from 'nitropack'
import { annotatePlugins, checkForCircularDependencies } from './app'
@ -199,9 +199,18 @@ export const schemaTemplate: NuxtTemplate = {
const relativeRoot = relative(resolve(nuxt.options.buildDir, 'types'), nuxt.options.rootDir)
const getImportName = (name: string) => (name[0] === '.' ? './' + join(relativeRoot, name) : name).replace(IMPORT_NAME_RE, '')
const modules = nuxt.options._installedModules
.filter(m => m.meta && m.meta.configKey && m.meta.name && !m.meta.name.startsWith('nuxt:') && m.meta.name !== 'nuxt-config-schema')
.map(m => [genString(m.meta.configKey), getImportName(m.entryPath || m.meta.name), m] as const)
const modules: [string, string, NuxtOptions['_installedModules'][number]][] = []
for (const m of nuxt.options._installedModules) {
// modules without sufficient metadata
if (!m.meta || !m.meta.configKey || !m.meta.name) {
continue
}
// core nuxt modules
if (m.meta.name.startsWith('nuxt:') || m.meta.name === 'nuxt-config-schema') {
continue
}
modules.push([genString(m.meta.configKey), getImportName(m.entryPath || m.meta.name), m])
}
const privateRuntimeConfig = Object.create(null)
for (const key in nuxt.options.runtimeConfig) {
@ -224,7 +233,7 @@ export const schemaTemplate: NuxtTemplate = {
} else if (mod.meta?.repository) {
if (typeof mod.meta.repository === 'string') {
link = mod.meta.repository
} else if (typeof mod.meta.repository.url === 'string') {
} else if (typeof mod.meta.repository === 'object' && 'url' in mod.meta.repository && typeof mod.meta.repository.url === 'string') {
link = mod.meta.repository.url
}
if (link) {

View File

@ -37,6 +37,9 @@
},
"devDependencies": {
"@types/pug": "2.0.10",
"@types/rollup-plugin-visualizer": "4.2.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9",
"@unhead/schema": "1.11.18",
"@vitejs/plugin-vue": "5.2.1",
"@vitejs/plugin-vue-jsx": "4.1.1",
@ -46,14 +49,17 @@
"c12": "2.0.1",
"chokidar": "4.0.3",
"compatx": "0.1.8",
"css-minimizer-webpack-plugin": "7.0.0",
"esbuild-loader": "4.2.2",
"file-loader": "6.2.0",
"h3": "1.15.0",
"hookable": "5.5.3",
"ignore": "7.0.3",
"mini-css-extract-plugin": "2.9.2",
"nitropack": "2.10.4",
"ofetch": "1.4.1",
"pkg-types": "1.3.1",
"postcss": "8.5.1",
"sass-loader": "16.0.4",
"scule": "1.3.0",
"unbuild": "3.3.1",

View File

@ -1,6 +1,6 @@
import { defineUntypedSchema } from 'untyped'
import { defineResolvers } from '../utils/definition'
export default defineUntypedSchema({
export default defineResolvers({
/**
* Configure Nuxt component auto-registration.
*
@ -14,10 +14,13 @@ export default defineUntypedSchema({
if (Array.isArray(val)) {
return { dirs: val }
}
if (val === undefined || val === true) {
return { dirs: [{ path: '~/components/global', global: true }, '~/components'] }
if (val === false) {
return { dirs: [] }
}
return {
dirs: [{ path: '~/components/global', global: true }, '~/components'],
...typeof val === 'object' ? val : {},
}
return val
},
},

View File

@ -1,9 +1,10 @@
import { defineUntypedSchema } from 'untyped'
import { defu } from 'defu'
import { resolve } from 'pathe'
import { defineResolvers } from '../utils/definition'
import type { AppHeadMetaObject } from '../types/head'
import type { NuxtAppConfig } from '../types/config'
export default defineUntypedSchema({
export default defineResolvers({
/**
* Vue.js config
*/
@ -27,7 +28,18 @@ export default defineUntypedSchema({
* Include Vue compiler in runtime bundle.
*/
runtimeCompiler: {
$resolve: async (val, get) => val ?? await get('experimental.runtimeVueCompiler') ?? false,
$resolve: async (val, get) => {
if (typeof val === 'boolean') {
return val
}
// @ts-expect-error TODO: formally deprecate in v4
const legacyProperty = await get('experimental.runtimeVueCompiler') as unknown
if (typeof legacyProperty === 'boolean') {
return legacyProperty
}
return false
},
},
/**
@ -41,7 +53,7 @@ export default defineUntypedSchema({
* may be set in your `nuxt.config`. All other options should be set at runtime in a Nuxt plugin..
* @see [Vue app config documentation](https://vuejs.org/api/application.html#app-config)
*/
config: undefined,
config: {},
},
/**
@ -68,12 +80,22 @@ export default defineUntypedSchema({
* ```
*/
baseURL: {
$resolve: val => val || process.env.NUXT_APP_BASE_URL || '/',
$resolve: (val) => {
if (typeof val === 'string') {
return val
}
return process.env.NUXT_APP_BASE_URL || '/'
},
},
/** The folder name for the built site assets, relative to `baseURL` (or `cdnURL` if set). This is set at build time and should not be customized at runtime. */
buildAssetsDir: {
$resolve: val => val || process.env.NUXT_APP_BUILD_ASSETS_DIR || '/_nuxt/',
$resolve: (val) => {
if (typeof val === 'string') {
return val
}
return process.env.NUXT_APP_BUILD_ASSETS_DIR || '/_nuxt/'
},
},
/**
@ -96,7 +118,12 @@ export default defineUntypedSchema({
* ```
*/
cdnURL: {
$resolve: async (val, get) => (await get('dev')) ? '' : (process.env.NUXT_APP_CDN_URL ?? val) || '',
$resolve: async (val, get) => {
if (await get('dev')) {
return ''
}
return process.env.NUXT_APP_CDN_URL || (typeof val === 'string' ? val : '')
},
},
/**
@ -132,14 +159,20 @@ export default defineUntypedSchema({
* @type {typeof import('../src/types/config').NuxtAppConfig['head']}
*/
head: {
$resolve: async (val: Partial<AppHeadMetaObject> | undefined, get) => {
const resolved = defu(val, await get('meta') as Partial<AppHeadMetaObject>, {
$resolve: async (_val, get) => {
// @ts-expect-error TODO: remove in Nuxt v4
const legacyMetaValues = await get('meta') as Record<string, unknown>
const val: Partial<NuxtAppConfig['head']> = _val && typeof _val === 'object' ? _val : {}
type NormalizedMetaObject = Required<Pick<AppHeadMetaObject, 'meta' | 'link' | 'style' | 'script' | 'noscript'>>
const resolved: NuxtAppConfig['head'] & NormalizedMetaObject = defu(val, legacyMetaValues, {
meta: [],
link: [],
style: [],
script: [],
noscript: [],
} as Required<Pick<AppHeadMetaObject, 'meta' | 'link' | 'style' | 'script' | 'noscript'>>)
} satisfies NormalizedMetaObject)
// provides default charset and viewport if not set
if (!resolved.meta.find(m => m.charset)?.charset) {
@ -190,9 +223,13 @@ export default defineUntypedSchema({
* @type {typeof import('../src/types/config').NuxtAppConfig['viewTransition']}
*/
viewTransition: {
$resolve: async (val, get) => val ?? await (get('experimental') as Promise<Record<string, any>>).then(
e => e?.viewTransition,
) ?? false,
$resolve: async (val, get) => {
if (val === 'always' || typeof val === 'boolean') {
return val
}
return await get('experimental').then(e => e.viewTransition) ?? false
},
},
/**
@ -211,14 +248,14 @@ export default defineUntypedSchema({
* @deprecated Prefer `rootAttrs.id` instead
*/
rootId: {
$resolve: val => val === false ? false : (val || '__nuxt'),
$resolve: val => val === false ? false : (val && typeof val === 'string' ? val : '__nuxt'),
},
/**
* Customize Nuxt root element tag.
*/
rootTag: {
$resolve: val => val || 'div',
$resolve: val => val && typeof val === 'string' ? val : 'div',
},
/**
@ -226,11 +263,12 @@ export default defineUntypedSchema({
* @type {typeof import('@unhead/schema').HtmlAttributes}
*/
rootAttrs: {
$resolve: async (val: undefined | null | Record<string, unknown>, get) => {
$resolve: async (val, get) => {
const rootId = await get('app.rootId')
return defu(val, {
return {
id: rootId === false ? undefined : (rootId || '__nuxt'),
})
...typeof val === 'object' ? val : {},
}
},
},
@ -238,7 +276,7 @@ export default defineUntypedSchema({
* Customize Nuxt Teleport element tag.
*/
teleportTag: {
$resolve: val => val || 'div',
$resolve: val => val && typeof val === 'string' ? val : 'div',
},
/**
@ -247,7 +285,7 @@ export default defineUntypedSchema({
* @deprecated Prefer `teleportAttrs.id` instead
*/
teleportId: {
$resolve: val => val === false ? false : (val || 'teleports'),
$resolve: val => val === false ? false : (val && typeof val === 'string' ? val : 'teleports'),
},
/**
@ -255,11 +293,12 @@ export default defineUntypedSchema({
* @type {typeof import('@unhead/schema').HtmlAttributes}
*/
teleportAttrs: {
$resolve: async (val: undefined | null | Record<string, unknown>, get) => {
$resolve: async (val, get) => {
const teleportId = await get('app.teleportId')
return defu(val, {
return {
id: teleportId === false ? undefined : (teleportId || 'teleports'),
})
...typeof val === 'object' ? val : {},
}
},
},
@ -267,12 +306,12 @@ export default defineUntypedSchema({
* Customize Nuxt SpaLoader element tag.
*/
spaLoaderTag: {
$resolve: val => val || 'div',
$resolve: val => val && typeof val === 'string' ? val : 'div',
},
/**
* Customize Nuxt Nuxt SpaLoader element attributes.
* @type {typeof import('@unhead/schema').HtmlAttributes}
* @type {Partial<typeof import('@unhead/schema').HtmlAttributes>}
*/
spaLoaderAttrs: {
id: '__nuxt-loader',
@ -332,10 +371,18 @@ export default defineUntypedSchema({
* }
* </style>
* ```
* @type {string | boolean}
* @type {string | boolean | undefined}
*/
spaLoadingTemplate: {
$resolve: async (val: string | boolean | undefined, get) => typeof val === 'string' ? resolve(await get('srcDir') as string, val) : val ?? null,
$resolve: async (val, get) => {
if (typeof val === 'string') {
return resolve(await get('srcDir'), val)
}
if (typeof val === 'boolean') {
return val
}
return undefined
},
},
/**
@ -386,7 +433,21 @@ export default defineUntypedSchema({
* @type {string[]}
*/
css: {
$resolve: (val: string[] | undefined) => (val ?? []).map((c: any) => c.src || c),
$resolve: (val) => {
if (!Array.isArray(val)) {
return []
}
const css: string[] = []
for (const item of val) {
if (typeof item === 'string') {
css.push(item)
} else if (item && 'src' in item) {
// TODO: remove in Nuxt v4
css.push(item.src)
}
}
return css
},
},
/**
@ -410,12 +471,13 @@ export default defineUntypedSchema({
* @type {typeof import('@unhead/schema').RenderSSRHeadOptions}
*/
renderSSRHeadOptions: {
$resolve: async (val: Record<string, unknown> | undefined, get) => {
const isV4 = ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
$resolve: async (val, get) => {
const isV4 = (await get('future')).compatibilityVersion === 4
return defu(val, {
return {
...typeof val === 'object' ? val : {},
omitLineBreaks: isV4,
})
}
},
},
},

View File

@ -1,25 +1,35 @@
import { defineUntypedSchema } from 'untyped'
import { defu } from 'defu'
import { join } from 'pathe'
import { isTest } from 'std-env'
import { consola } from 'consola'
import type { Nuxt } from 'nuxt/schema'
import { defineResolvers } from '../utils/definition'
export default defineUntypedSchema({
export default defineResolvers({
/**
* The builder to use for bundling the Vue part of your application.
* @type {'vite' | 'webpack' | 'rspack' | { bundle: (nuxt: typeof import('../src/types/nuxt').Nuxt) => Promise<void> }}
*/
builder: {
$resolve: async (val: 'vite' | 'webpack' | 'rspack' | { bundle: (nuxt: unknown) => Promise<void> } | undefined = 'vite', get) => {
if (typeof val === 'object') {
return val
$resolve: async (val, get) => {
if (val && typeof val === 'object' && 'bundle' in val) {
return val as { bundle: (nuxt: Nuxt) => Promise<void> }
}
const map: Record<string, string> = {
const map = {
rspack: '@nuxt/rspack-builder',
vite: '@nuxt/vite-builder',
webpack: '@nuxt/webpack-builder',
}
return map[val] || val || (await get('vite') === false ? map.webpack : map.vite)
type Builder = 'vite' | 'webpack' | 'rspack'
if (typeof val === 'string' && val in map) {
// TODO: improve normalisation inference
return map[val as keyof typeof map] as Builder
}
// @ts-expect-error TODO: remove old, unsupported config in v4
if (await get('vite') === false) {
return map.webpack as Builder
}
return map.vite as Builder
},
},
@ -37,14 +47,15 @@ export default defineUntypedSchema({
* @type {boolean | { server?: boolean | 'hidden', client?: boolean | 'hidden' }}
*/
sourcemap: {
$resolve: async (val: boolean | { server?: boolean | 'hidden', client?: boolean | 'hidden' } | undefined, get) => {
$resolve: async (val, get) => {
if (typeof val === 'boolean') {
return { server: val, client: val }
}
return defu(val, {
return {
server: true,
client: await get('dev'),
})
...typeof val === 'object' ? val : {},
}
},
},
@ -56,11 +67,11 @@ export default defineUntypedSchema({
* @type {'silent' | 'info' | 'verbose'}
*/
logLevel: {
$resolve: (val: string | undefined) => {
if (val && !['silent', 'info', 'verbose'].includes(val)) {
$resolve: (val) => {
if (val && typeof val === 'string' && !['silent', 'info', 'verbose'].includes(val)) {
consola.warn(`Invalid \`logLevel\` option: \`${val}\`. Must be one of: \`silent\`, \`info\`, \`verbose\`.`)
}
return val ?? (isTest ? 'silent' : 'info')
return val && typeof val === 'string' ? val as 'silent' | 'info' | 'verbose' : (isTest ? 'silent' : 'info')
},
},
@ -81,7 +92,20 @@ export default defineUntypedSchema({
* @type {Array<string | RegExp | ((ctx: { isClient?: boolean; isServer?: boolean; isDev: boolean }) => string | RegExp | false)>}
*/
transpile: {
$resolve: (val: Array<string | RegExp | ((ctx: { isClient?: boolean, isServer?: boolean, isDev: boolean }) => string | RegExp | false)> | undefined) => (val || []).filter(Boolean),
$resolve: (val) => {
const transpile: Array<string | RegExp | ((ctx: { isClient?: boolean, isServer?: boolean, isDev: boolean }) => string | RegExp | false)> = []
if (Array.isArray(val)) {
for (const pattern of val) {
if (!pattern) {
continue
}
if (typeof pattern === 'string' || typeof pattern === 'function' || pattern instanceof RegExp) {
transpile.push(pattern)
}
}
}
return transpile
},
},
/**
@ -110,16 +134,17 @@ export default defineUntypedSchema({
* analyzerMode: 'static'
* }
* ```
* @type {boolean | { enabled?: boolean } & ((0 extends 1 & typeof import('webpack-bundle-analyzer').BundleAnalyzerPlugin.Options ? {} : typeof import('webpack-bundle-analyzer').BundleAnalyzerPlugin.Options) | typeof import('rollup-plugin-visualizer').PluginVisualizerOptions)}
* @type {boolean | { enabled?: boolean } & ((0 extends 1 & typeof import('webpack-bundle-analyzer').BundleAnalyzerPlugin.Options ? Record<string, unknown> : typeof import('webpack-bundle-analyzer').BundleAnalyzerPlugin.Options) | typeof import('rollup-plugin-visualizer').PluginVisualizerOptions)}
*/
analyze: {
$resolve: async (val: boolean | { enabled?: boolean } | Record<string, unknown>, get) => {
const [rootDir, analyzeDir] = await Promise.all([get('rootDir'), get('analyzeDir')]) as [string, string]
return defu(typeof val === 'boolean' ? { enabled: val } : val, {
$resolve: async (val, get) => {
const [rootDir, analyzeDir] = await Promise.all([get('rootDir'), get('analyzeDir')])
return {
template: 'treemap',
projectRoot: rootDir,
filename: join(analyzeDir, '{name}.html'),
})
...typeof val === 'boolean' ? { enabled: val } : typeof val === 'object' ? val : {},
}
},
},
},
@ -139,7 +164,7 @@ export default defineUntypedSchema({
* @type {Array<{ name: string, source?: string | RegExp, argumentLength: number }>}
*/
keyedComposables: {
$resolve: (val: Array<{ name: string, argumentLength: string }> | undefined) => [
$resolve: val => [
{ name: 'callOnce', argumentLength: 3 },
{ name: 'defineNuxtComponent', argumentLength: 2 },
{ name: 'useState', argumentLength: 2 },
@ -147,7 +172,7 @@ export default defineUntypedSchema({
{ name: 'useAsyncData', argumentLength: 3 },
{ name: 'useLazyAsyncData', argumentLength: 3 },
{ name: 'useLazyFetch', argumentLength: 3 },
...val || [],
...Array.isArray(val) ? val : [],
].filter(Boolean),
},

View File

@ -1,16 +1,16 @@
import { existsSync } from 'node:fs'
import { readdir } from 'node:fs/promises'
import { randomUUID } from 'node:crypto'
import { defineUntypedSchema } from 'untyped'
import { basename, join, relative, resolve } from 'pathe'
import { isDebug, isDevelopment, isTest } from 'std-env'
import { defu } from 'defu'
import { findWorkspaceDir } from 'pkg-types'
import type { RuntimeConfig } from '../types/config'
import type { NuxtDebugOptions } from '../types/debug'
import type { NuxtModule } from '../types/module'
import { defineResolvers } from '../utils/definition'
export default defineUntypedSchema({
export default defineResolvers({
/**
* Extend project from multiple local or remote sources.
*
@ -21,7 +21,7 @@ export default defineUntypedSchema({
* @see [`giget` documentation](https://github.com/unjs/giget)
* @type {string | [string, typeof import('c12').SourceOptions?] | (string | [string, typeof import('c12').SourceOptions?])[]}
*/
extends: null,
extends: undefined,
/**
* Specify a compatibility date for your app.
@ -43,7 +43,7 @@ export default defineUntypedSchema({
* You can use `github:`, `gitlab:`, `bitbucket:` or `https://` to extend from a remote git repository.
* @type {string}
*/
theme: null,
theme: undefined,
/**
* Define the root directory of your application.
@ -67,9 +67,9 @@ export default defineUntypedSchema({
* It is normally not needed to configure this option.
*/
workspaceDir: {
$resolve: async (val: string | undefined, get): Promise<string> => {
const rootDir = await get('rootDir') as string
return val ? resolve(rootDir, val) : await findWorkspaceDir(rootDir).catch(() => rootDir)
$resolve: async (val, get) => {
const rootDir = await get('rootDir')
return val && typeof val === 'string' ? resolve(rootDir, val) : await findWorkspaceDir(rootDir).catch(() => rootDir)
},
},
@ -105,14 +105,14 @@ export default defineUntypedSchema({
* ```
*/
srcDir: {
$resolve: async (val: string | undefined, get): Promise<string> => {
if (val) {
return resolve(await get('rootDir') as string, val)
$resolve: async (val, get) => {
if (val && typeof val === 'string') {
return resolve(await get('rootDir'), val)
}
const [rootDir, isV4] = await Promise.all([
get('rootDir') as Promise<string>,
(get('future') as Promise<Record<string, unknown>>).then(r => r.compatibilityVersion === 4),
get('rootDir'),
get('future').then(r => r.compatibilityVersion === 4),
])
if (!isV4) {
@ -138,7 +138,7 @@ export default defineUntypedSchema({
}
}
const keys = ['assets', 'layouts', 'middleware', 'pages', 'plugins'] as const
const dirs = await Promise.all(keys.map(key => get(`dir.${key}`) as Promise<string>))
const dirs = await Promise.all(keys.map(key => get(`dir.${key}`)))
for (const dir of dirs) {
if (existsSync(resolve(rootDir, dir))) {
return rootDir
@ -157,13 +157,13 @@ export default defineUntypedSchema({
*
*/
serverDir: {
$resolve: async (val: string | undefined, get): Promise<string> => {
if (val) {
const rootDir = await get('rootDir') as string
$resolve: async (val, get) => {
if (val && typeof val === 'string') {
const rootDir = await get('rootDir')
return resolve(rootDir, val)
}
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4
return join(isV4 ? await get('rootDir') as string : await get('srcDir') as string, 'server')
const isV4 = (await get('future')).compatibilityVersion === 4
return join(isV4 ? await get('rootDir') : await get('srcDir'), 'server')
},
},
@ -180,7 +180,10 @@ export default defineUntypedSchema({
* ```
*/
buildDir: {
$resolve: async (val: string | undefined, get): Promise<string> => resolve(await get('rootDir') as string, val || '.nuxt'),
$resolve: async (val, get) => {
const rootDir = await get('rootDir')
return resolve(rootDir, val && typeof val === 'string' ? val : '.nuxt')
},
},
/**
@ -189,14 +192,14 @@ export default defineUntypedSchema({
* Defaults to `nuxt-app`.
*/
appId: {
$resolve: (val: string) => val ?? 'nuxt-app',
$resolve: val => val && typeof val === 'string' ? val : 'nuxt-app',
},
/**
* A unique identifier matching the build. This may contain the hash of the current state of the project.
*/
buildId: {
$resolve: async (val: string | undefined, get): Promise<string> => {
$resolve: async (val, get): Promise<string> => {
if (typeof val === 'string') { return val }
const [isDev, isTest] = await Promise.all([get('dev') as Promise<boolean>, get('test') as Promise<boolean>])
@ -220,12 +223,17 @@ export default defineUntypedSchema({
*/
modulesDir: {
$default: ['node_modules'],
$resolve: async (val: string[] | undefined, get): Promise<string[]> => {
const rootDir = await get('rootDir') as string
return [...new Set([
...(val || []).map((dir: string) => resolve(rootDir, dir)),
resolve(rootDir, 'node_modules'),
])]
$resolve: async (val, get) => {
const rootDir = await get('rootDir')
const modulesDir = new Set<string>([resolve(rootDir, 'node_modules')])
if (Array.isArray(val)) {
for (const dir of val) {
if (dir && typeof dir === 'string') {
modulesDir.add(resolve(rootDir, dir))
}
}
}
return [...modulesDir]
},
},
@ -235,9 +243,9 @@ export default defineUntypedSchema({
* If a relative path is specified, it will be relative to your `rootDir`.
*/
analyzeDir: {
$resolve: async (val: string | undefined, get): Promise<string> => val
? resolve(await get('rootDir') as string, val)
: resolve(await get('buildDir') as string, 'analyze'),
$resolve: async (val, get) => val && typeof val === 'string'
? resolve(await get('rootDir'), val)
: resolve(await get('buildDir'), 'analyze'),
},
/**
@ -246,14 +254,14 @@ export default defineUntypedSchema({
* Normally, you should not need to set this.
*/
dev: {
$resolve: val => val ?? Boolean(isDevelopment),
$resolve: val => typeof val === 'boolean' ? val : Boolean(isDevelopment),
},
/**
* Whether your app is being unit tested.
*/
test: {
$resolve: val => val ?? Boolean(isTest),
$resolve: val => typeof val === 'boolean' ? val : Boolean(isTest),
},
/**
@ -267,11 +275,8 @@ export default defineUntypedSchema({
* @type {boolean | (typeof import('../src/types/debug').NuxtDebugOptions) | undefined}
*/
debug: {
$resolve: (val: boolean | NuxtDebugOptions | undefined) => {
$resolve: (val) => {
val ??= isDebug
if (val === false) {
return val
}
if (val === true) {
return {
templates: true,
@ -286,7 +291,10 @@ export default defineUntypedSchema({
hydration: true,
} satisfies Required<NuxtDebugOptions>
}
return val
if (val && typeof val === 'object') {
return val
}
return false
},
},
@ -295,7 +303,7 @@ export default defineUntypedSchema({
* If set to `false` generated pages will have no content.
*/
ssr: {
$resolve: val => val ?? true,
$resolve: val => typeof val === 'boolean' ? val : true,
},
/**
@ -324,7 +332,20 @@ export default defineUntypedSchema({
* @type {(typeof import('../src/types/module').NuxtModule<any> | string | [typeof import('../src/types/module').NuxtModule | string, Record<string, any>] | undefined | null | false)[]}
*/
modules: {
$resolve: (val: string[] | undefined): string[] => (val || []).filter(Boolean),
$resolve: (val) => {
const modules: Array<string | NuxtModule | [NuxtModule | string, Record<string, any>]> = []
if (Array.isArray(val)) {
for (const mod of val) {
if (!mod) {
continue
}
if (typeof mod === 'string' || typeof mod === 'function' || (Array.isArray(mod) && mod[0])) {
modules.push(mod)
}
}
}
return modules
},
},
/**
@ -334,13 +355,13 @@ export default defineUntypedSchema({
*/
dir: {
app: {
$resolve: async (val: string | undefined, get) => {
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4
$resolve: async (val, get) => {
const isV4 = (await get('future')).compatibilityVersion === 4
if (isV4) {
const [srcDir, rootDir] = await Promise.all([get('srcDir') as Promise<string>, get('rootDir') as Promise<string>])
return resolve(await get('srcDir') as string, val || (srcDir === rootDir ? 'app' : '.'))
const [srcDir, rootDir] = await Promise.all([get('srcDir'), get('rootDir')])
return resolve(await get('srcDir'), val && typeof val === 'string' ? val : (srcDir === rootDir ? 'app' : '.'))
}
return val || 'app'
return val && typeof val === 'string' ? val : 'app'
},
},
/**
@ -362,12 +383,12 @@ export default defineUntypedSchema({
* The modules directory, each file in which will be auto-registered as a Nuxt module.
*/
modules: {
$resolve: async (val: string | undefined, get) => {
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4
$resolve: async (val, get) => {
const isV4 = (await get('future')).compatibilityVersion === 4
if (isV4) {
return resolve(await get('rootDir') as string, val || 'modules')
return resolve(await get('rootDir'), val && typeof val === 'string' ? val : 'modules')
}
return val || 'modules'
return val && typeof val === 'string' ? val : 'modules'
},
},
@ -391,18 +412,25 @@ export default defineUntypedSchema({
* and copied across into your `dist` folder when your app is generated.
*/
public: {
$resolve: async (val: string | undefined, get) => {
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4
$resolve: async (val, get) => {
const isV4 = (await get('future')).compatibilityVersion === 4
if (isV4) {
return resolve(await get('rootDir') as string, val || await get('dir.static') as string || 'public')
return resolve(await get('rootDir'), val && typeof val === 'string' ? val : (await get('dir.static') || 'public'))
}
return val || await get('dir.static') as string || 'public'
return val && typeof val === 'string' ? val : (await get('dir.static') || 'public')
},
},
// TODO: remove in v4
static: {
// @ts-expect-error schema has invalid types
$schema: { deprecated: 'use `dir.public` option instead' },
$resolve: async (val, get) => val || await get('dir.public') || 'public',
$resolve: async (val, get) => {
if (val && typeof val === 'string') {
return val
}
return await get('dir.public') || 'public'
},
},
},
@ -410,7 +438,17 @@ export default defineUntypedSchema({
* The extensions that should be resolved by the Nuxt resolver.
*/
extensions: {
$resolve: (val: string[] | undefined): string[] => ['.js', '.jsx', '.mjs', '.ts', '.tsx', '.vue', ...val || []].filter(Boolean),
$resolve: (val): string[] => {
const extensions = ['.js', '.jsx', '.mjs', '.ts', '.tsx', '.vue']
if (Array.isArray(val)) {
for (const item of val) {
if (item && typeof item === 'string') {
extensions.push(item)
}
}
}
return extensions
},
},
/**
@ -454,8 +492,8 @@ export default defineUntypedSchema({
* @type {Record<string, string>}
*/
alias: {
$resolve: async (val: Record<string, string>, get): Promise<Record<string, string>> => {
const [srcDir, rootDir, assetsDir, publicDir, buildDir, sharedDir] = await Promise.all([get('srcDir'), get('rootDir'), get('dir.assets'), get('dir.public'), get('buildDir'), get('dir.shared')]) as [string, string, string, string, string, string]
$resolve: async (val, get) => {
const [srcDir, rootDir, assetsDir, publicDir, buildDir, sharedDir] = await Promise.all([get('srcDir'), get('rootDir'), get('dir.assets'), get('dir.public'), get('buildDir'), get('dir.shared')])
return {
'~': srcDir,
'@': srcDir,
@ -466,7 +504,7 @@ export default defineUntypedSchema({
[basename(publicDir)]: resolve(srcDir, publicDir),
'#build': buildDir,
'#internal/nuxt/paths': resolve(buildDir, 'paths.mjs'),
...val,
...typeof val === 'object' ? val : {},
}
},
},
@ -491,7 +529,7 @@ export default defineUntypedSchema({
* By default, the `ignorePrefix` is set to '-', ignoring any files starting with '-'.
*/
ignorePrefix: {
$resolve: val => val ?? '-',
$resolve: val => val && typeof val === 'string' ? val : '-',
},
/**
@ -499,18 +537,27 @@ export default defineUntypedSchema({
* inside the `ignore` array will be ignored in building.
*/
ignore: {
$resolve: async (val: string[] | undefined, get): Promise<string[]> => {
const [rootDir, ignorePrefix, analyzeDir, buildDir] = await Promise.all([get('rootDir'), get('ignorePrefix'), get('analyzeDir'), get('buildDir')]) as [string, string, string, string]
return [
$resolve: async (val, get): Promise<string[]> => {
const [rootDir, ignorePrefix, analyzeDir, buildDir] = await Promise.all([get('rootDir'), get('ignorePrefix'), get('analyzeDir'), get('buildDir')])
const ignore = new Set<string>([
'**/*.stories.{js,cts,mts,ts,jsx,tsx}', // ignore storybook files
'**/*.{spec,test}.{js,cts,mts,ts,jsx,tsx}', // ignore tests
'**/*.d.{cts,mts,ts}', // ignore type declarations
'**/.{pnpm-store,vercel,netlify,output,git,cache,data}',
relative(rootDir, analyzeDir),
relative(rootDir, buildDir),
ignorePrefix && `**/${ignorePrefix}*.*`,
...val || [],
].filter(Boolean)
])
if (ignorePrefix) {
ignore.add(`**/${ignorePrefix}*.*`)
}
if (Array.isArray(val)) {
for (const pattern in val) {
if (pattern) {
ignore.add(pattern)
}
}
}
return [...ignore]
},
},
@ -523,8 +570,11 @@ export default defineUntypedSchema({
* @type {Array<string | RegExp>}
*/
watch: {
$resolve: (val: Array<unknown> | undefined) => {
return (val || []).filter((b: unknown) => typeof b === 'string' || b instanceof RegExp)
$resolve: (val) => {
if (Array.isArray(val)) {
return val.filter((b: unknown) => typeof b === 'string' || b instanceof RegExp)
}
return []
},
},
@ -580,7 +630,7 @@ export default defineUntypedSchema({
* ```
* @type {typeof import('../src/types/hooks').NuxtHooks}
*/
hooks: null,
hooks: undefined,
/**
* Runtime config allows passing dynamic config and environment variables to the Nuxt app context.
@ -608,8 +658,9 @@ export default defineUntypedSchema({
* @type {typeof import('../src/types/config').RuntimeConfig}
*/
runtimeConfig: {
$resolve: async (val: RuntimeConfig, get): Promise<Record<string, unknown>> => {
const [app, buildId] = await Promise.all([get('app') as Promise<Record<string, string>>, get('buildId') as Promise<string>])
$resolve: async (_val, get) => {
const val = _val && typeof _val === 'object' ? _val : {}
const [app, buildId] = await Promise.all([get('app'), get('buildId')])
provideFallbackValues(val)
return defu(val, {
public: {},

View File

@ -1,7 +1,7 @@
import { defineUntypedSchema } from 'untyped'
import { defineResolvers } from '../utils/definition'
import { template as loadingTemplate } from '../../../ui-templates/dist/templates/loading'
export default defineUntypedSchema({
export default defineResolvers({
devServer: {
/**
* Whether to enable HTTPS.
@ -21,9 +21,12 @@ export default defineUntypedSchema({
https: false,
/** Dev server listening port */
port: process.env.NUXT_PORT || process.env.NITRO_PORT || process.env.PORT || 3000,
port: Number(process.env.NUXT_PORT || process.env.NITRO_PORT || process.env.PORT || 3000),
/** Dev server listening host */
/**
* Dev server listening host
* @type {string | undefined}
*/
host: process.env.NUXT_HOST || process.env.NITRO_HOST || process.env.HOST || undefined,
/**

View File

@ -1,6 +1,6 @@
import { defineUntypedSchema } from 'untyped'
import { defineResolvers } from '../utils/definition'
export default defineUntypedSchema({
export default defineResolvers({
/**
* `future` is for early opting-in to new features that will become default in a future
* (possibly major) version of the framework.
@ -63,10 +63,10 @@ export default defineUntypedSchema({
*/
typescriptBundlerResolution: {
async $resolve (val, get) {
// TODO: remove in v3.10
val = val ?? await (get('experimental') as Promise<Record<string, any>>).then(e => e?.typescriptBundlerResolution)
// @ts-expect-error TODO: remove in v3.10
val = typeof val === 'boolean' ? val : await (get('experimental')).then(e => e?.typescriptBundlerResolution as string | undefined)
if (typeof val === 'boolean') { return val }
const setting = await get('typescript.tsConfig.compilerOptions.moduleResolution') as string | undefined
const setting = await get('typescript.tsConfig').then(r => r?.compilerOptions?.moduleResolution)
if (setting) {
return setting.toLowerCase() === 'bundler'
}
@ -86,14 +86,22 @@ export default defineUntypedSchema({
* @type {boolean | ((id?: string) => boolean)}
*/
inlineStyles: {
async $resolve (val, get) {
// TODO: remove in v3.10
val = val ?? await (get('experimental') as Promise<Record<string, any>>).then((e: Record<string, any>) => e?.inlineSSRStyles)
if (val === false || (await get('dev')) || (await get('ssr')) === false || (await get('builder')) === '@nuxt/webpack-builder') {
async $resolve (_val, get) {
const val = typeof _val === 'boolean' || typeof _val === 'function'
? _val
// @ts-expect-error TODO: legacy property - remove in v3.10
: await (get('experimental')).then(e => e?.inlineSSRStyles) as undefined | boolean
if (
val === false ||
(await get('dev')) ||
(await get('ssr')) === false ||
// @ts-expect-error TODO: handled normalised types
(await get('builder')) === '@nuxt/webpack-builder'
) {
return false
}
// Enabled by default for vite prod with ssr (for vue components)
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? (id: string) => id && id.includes('.vue') : true)
return val ?? ((await get('future')).compatibilityVersion === 4 ? (id?: string) => !!id && id.includes('.vue') : true)
},
},
@ -106,7 +114,9 @@ export default defineUntypedSchema({
*/
devLogs: {
async $resolve (val, get) {
if (val !== undefined) { return val }
if (typeof val === 'boolean' || val === 'silent') {
return val
}
const [isDev, isTest] = await Promise.all([get('dev'), get('test')])
return isDev && !isTest
},
@ -118,8 +128,10 @@ export default defineUntypedSchema({
*/
noScripts: {
async $resolve (val, get) {
// TODO: remove in v3.10
return val ?? await (get('experimental') as Promise<Record<string, any>>).then((e: Record<string, any>) => e?.noScripts) ?? false
return typeof val === 'boolean'
? val
// @ts-expect-error TODO: legacy property - remove in v3.10
: (await (get('experimental')).then(e => e?.noScripts as boolean | undefined) ?? false)
},
},
},
@ -128,7 +140,7 @@ export default defineUntypedSchema({
* Set to true to generate an async entry point for the Vue bundle (for module federation support).
*/
asyncEntry: {
$resolve: val => val ?? false,
$resolve: val => typeof val === 'boolean' ? val : false,
},
// TODO: Remove when nitro has support for mocking traced dependencies
@ -178,7 +190,17 @@ export default defineUntypedSchema({
if (val === 'reload') {
return 'automatic'
}
return val ?? 'automatic'
if (val === false) {
return false
}
const validOptions = ['manual', 'automatic', 'automatic-immediate'] as const
type EmitRouteChunkError = typeof validOptions[number]
if (typeof val === 'string' && validOptions.includes(val as EmitRouteChunkError)) {
return val as EmitRouteChunkError
}
return 'automatic'
},
},
@ -347,14 +369,16 @@ export default defineUntypedSchema({
*/
watcher: {
$resolve: async (val, get) => {
if (val) {
return val
const validOptions = ['chokidar', 'parcel', 'chokidar-granular'] as const
type WatcherOption = typeof validOptions[number]
if (typeof val === 'string' && validOptions.includes(val as WatcherOption)) {
return val as WatcherOption
}
const [srcDir, rootDir] = await Promise.all([get('srcDir'), get('rootDir')]) as [string, string]
const [srcDir, rootDir] = await Promise.all([get('srcDir'), get('rootDir')])
if (srcDir === rootDir) {
return 'chokidar-granular'
return 'chokidar-granular' as const
}
return 'chokidar'
return 'chokidar' as const
},
},
@ -396,7 +420,7 @@ export default defineUntypedSchema({
*/
scanPageMeta: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? 'after-resolve' : true)
return typeof val === 'boolean' || val === 'after-resolve' ? val : ((await get('future')).compatibilityVersion === 4 ? 'after-resolve' : true)
},
},
@ -435,7 +459,7 @@ export default defineUntypedSchema({
*/
sharedPrerenderData: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
return typeof val === 'boolean' ? val : ((await get('future')).compatibilityVersion === 4)
},
},
@ -569,7 +593,7 @@ export default defineUntypedSchema({
*/
normalizeComponentNames: {
$resolve: async (val, get) => {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
return typeof val === 'boolean' ? val : ((await get('future')).compatibilityVersion === 4)
},
},
@ -580,7 +604,9 @@ export default defineUntypedSchema({
*/
spaLoadingTemplateLocation: {
$resolve: async (val, get) => {
return val ?? (((await get('future') as Record<string, unknown>).compatibilityVersion === 4) ? 'body' : 'within')
const validOptions = ['body', 'within'] as const
type SpaLoadingTemplateLocation = typeof validOptions[number]
return typeof val === 'string' && validOptions.includes(val as SpaLoadingTemplateLocation) ? val as SpaLoadingTemplateLocation : (((await get('future')).compatibilityVersion === 4) ? 'body' : 'within')
},
},
@ -590,7 +616,7 @@ export default defineUntypedSchema({
* @see [the Chrome DevTools extensibility API](https://developer.chrome.com/docs/devtools/performance/extension#tracks)
*/
browserDevtoolsTiming: {
$resolve: async (val, get) => val ?? await get('dev'),
$resolve: async (val, get) => typeof val === 'boolean' ? val : await get('dev'),
},
/**
@ -598,7 +624,7 @@ export default defineUntypedSchema({
*/
debugModuleMutation: {
$resolve: async (val, get) => {
return val ?? Boolean(await get('debug'))
return typeof val === 'boolean' ? val : Boolean(await get('debug'))
},
},
},

View File

@ -1,6 +1,6 @@
import { defineUntypedSchema } from 'untyped'
import { defineResolvers } from '../utils/definition'
export default defineUntypedSchema({
export default defineResolvers({
generate: {
/**
* The routes to generate.

View File

@ -1,3 +1,5 @@
import type { ResolvableConfigSchema } from '../utils/definition'
import adhoc from './adhoc'
import app from './app'
import build from './build'
@ -28,4 +30,4 @@ export default {
...typescript,
...vite,
...webpack,
}
} satisfies ResolvableConfigSchema

View File

@ -1,6 +1,6 @@
import { defineUntypedSchema } from 'untyped'
import { defineResolvers } from '../utils/definition'
export default defineUntypedSchema({
export default defineResolvers({
/** @private */
_majorVersion: 3,
/** @private */
@ -25,7 +25,7 @@ export default defineUntypedSchema({
appDir: '',
/**
* @private
* @type {Array<{ meta: ModuleMeta; module: NuxtModule, timings?: Record<string, number | undefined>; entryPath?: string }>}
* @type {Array<{ meta: typeof import('../src/types/module').ModuleMeta; module: typeof import('../src/types/module').NuxtModule, timings?: Record<string, number | undefined>; entryPath?: string }>}
*/
_installedModules: [],
/** @private */

View File

@ -1,7 +1,6 @@
import { defineUntypedSchema } from 'untyped'
import type { RuntimeConfig } from '../types/config'
import { defineResolvers } from '../utils/definition'
export default defineUntypedSchema({
export default defineResolvers({
/**
* Configuration for Nitro.
* @see [Nitro configuration docs](https://nitro.unjs.io/config/)
@ -9,8 +8,8 @@ export default defineUntypedSchema({
*/
nitro: {
runtimeConfig: {
$resolve: async (val: Record<string, any> | undefined, get) => {
const runtimeConfig = await get('runtimeConfig') as RuntimeConfig
$resolve: async (val, get) => {
const runtimeConfig = await get('runtimeConfig')
return {
...runtimeConfig,
app: {
@ -27,10 +26,12 @@ export default defineUntypedSchema({
},
},
routeRules: {
$resolve: async (val: Record<string, any> | undefined, get) => ({
...await get('routeRules') as Record<string, any>,
...val,
}),
$resolve: async (val, get) => {
return {
...await get('routeRules'),
...(val && typeof val === 'object' ? val : {}),
}
},
},
},

View File

@ -1,4 +1,4 @@
import { defineUntypedSchema } from 'untyped'
import { defineResolvers } from '../utils/definition'
const ensureItemIsLast = (item: string) => (arr: string[]) => {
const index = arr.indexOf(item)
@ -17,7 +17,7 @@ const orderPresets = {
},
}
export default defineUntypedSchema({
export default defineResolvers({
postcss: {
/**
* A strategy for ordering PostCSS plugins.
@ -25,14 +25,20 @@ export default defineUntypedSchema({
* @type {'cssnanoLast' | 'autoprefixerLast' | 'autoprefixerAndCssnanoLast' | string[] | ((names: string[]) => string[])}
*/
order: {
$resolve: (val: string | string[] | ((plugins: string[]) => string[])): string[] | ((plugins: string[]) => string[]) => {
$resolve: (val) => {
if (typeof val === 'string') {
if (!(val in orderPresets)) {
throw new Error(`[nuxt] Unknown PostCSS order preset: ${val}`)
}
return orderPresets[val as keyof typeof orderPresets]
}
return val ?? orderPresets.autoprefixerAndCssnanoLast
if (typeof val === 'function') {
return val as (names: string[]) => string[]
}
if (Array.isArray(val)) {
return val
}
return orderPresets.autoprefixerAndCssnanoLast
},
},
/**

View File

@ -1,6 +1,6 @@
import { defineUntypedSchema } from 'untyped'
import { defineResolvers } from '../utils/definition'
export default defineUntypedSchema({
export default defineResolvers({
router: {
/**
* Additional router options passed to `vue-router`. On top of the options for `vue-router`,

View File

@ -1,6 +1,6 @@
import { defineUntypedSchema } from 'untyped'
import { defineResolvers } from '../utils/definition'
export default defineUntypedSchema({
export default defineResolvers({
/**
* Configuration for Nuxt's TypeScript integration.
*
@ -23,7 +23,17 @@ export default defineUntypedSchema({
* @type {'vite' | 'webpack' | 'rspack' | 'shared' | false | undefined}
*/
builder: {
$resolve: val => val ?? null,
$resolve: (val) => {
const validBuilderTypes = ['vite', 'webpack', 'rspack', 'shared'] as const
type ValidBuilderType = typeof validBuilderTypes[number]
if (typeof val === 'string' && validBuilderTypes.includes(val as ValidBuilderType)) {
return val as ValidBuilderType
}
if (val === false) {
return false
}
return undefined
},
},
/**

View File

@ -1,10 +1,9 @@
import { consola } from 'consola'
import { resolve } from 'pathe'
import { isTest } from 'std-env'
import { defineUntypedSchema } from 'untyped'
import type { NuxtDebugOptions } from '../types/debug'
import { defineResolvers } from '../utils/definition'
export default defineUntypedSchema({
export default defineResolvers({
/**
* Configuration that will be passed directly to Vite.
*
@ -14,22 +13,21 @@ export default defineUntypedSchema({
*/
vite: {
root: {
$resolve: async (val, get) => val ?? (await get('srcDir')),
$resolve: async (val, get) => typeof val === 'string' ? val : (await get('srcDir')),
},
mode: {
$resolve: async (val, get) => val ?? (await get('dev') ? 'development' : 'production'),
$resolve: async (val, get) => typeof val === 'string' ? val : (await get('dev') ? 'development' : 'production'),
},
define: {
$resolve: async (val: Record<string, any> | undefined, get) => {
const [isDev, debug] = await Promise.all([get('dev'), get('debug')]) as [boolean, boolean | NuxtDebugOptions]
$resolve: async (_val, get) => {
const [isDev, isDebug] = await Promise.all([get('dev'), get('debug')])
return {
'__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': Boolean(debug && (debug === true || debug.hydration)),
'__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': Boolean(isDebug && (isDebug === true || isDebug.hydration)),
'process.dev': isDev,
'import.meta.dev': isDev,
'process.test': isTest,
'import.meta.test': isTest,
...val,
..._val && typeof _val === 'object' ? _val : {},
}
},
},
@ -37,6 +35,7 @@ export default defineUntypedSchema({
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
},
publicDir: {
// @ts-expect-error this is missing from our `vite` types deliberately, so users do not configure it
$resolve: (val) => {
if (val) {
consola.warn('Directly configuring the `vite.publicDir` option is not supported. Instead, set `dir.public`. You can read more in `https://nuxt.com/docs/api/nuxt-config#public`.')
@ -46,46 +45,51 @@ export default defineUntypedSchema({
},
vue: {
isProduction: {
$resolve: async (val, get) => val ?? !(await get('dev')),
$resolve: async (val, get) => typeof val === 'boolean' ? val : !(await get('dev')),
},
template: {
compilerOptions: {
$resolve: async (val, get) => val ?? (await get('vue') as Record<string, any>).compilerOptions,
$resolve: async (val, get) => val ?? (await get('vue')).compilerOptions,
},
transformAssetUrls: {
$resolve: async (val, get) => val ?? (await get('vue') as Record<string, any>).transformAssetUrls,
$resolve: async (val, get) => val ?? (await get('vue')).transformAssetUrls,
},
},
script: {
hoistStatic: {
$resolve: async (val, get) => val ?? (await get('vue') as Record<string, any>).compilerOptions?.hoistStatic,
$resolve: async (val, get) => typeof val === 'boolean' ? val : (await get('vue')).compilerOptions?.hoistStatic,
},
},
features: {
propsDestructure: {
$resolve: async (val, get) => {
if (val !== undefined && val !== null) {
if (typeof val === 'boolean') {
return val
}
const vueOptions = await get('vue') as Record<string, any> || {}
return Boolean(vueOptions.script?.propsDestructure ?? vueOptions.propsDestructure)
const vueOptions = await get('vue') || {}
return Boolean(
// @ts-expect-error TODO: remove in future: supporting a legacy schema
vueOptions.script?.propsDestructure
?? vueOptions.propsDestructure,
)
},
},
},
},
vueJsx: {
$resolve: async (val: Record<string, any>, get) => {
$resolve: async (val, get) => {
return {
isCustomElement: (await get('vue') as Record<string, any>).compilerOptions?.isCustomElement,
...val,
// TODO: investigate type divergence between types for @vue/compiler-core and @vue/babel-plugin-jsx
isCustomElement: (await get('vue')).compilerOptions?.isCustomElement as undefined | ((tag: string) => boolean),
...typeof val === 'object' ? val : {},
}
},
},
optimizeDeps: {
exclude: {
$resolve: async (val: string[] | undefined, get) => [
...val || [],
...(await get('build.transpile') as Array<string | RegExp | ((ctx: { isClient?: boolean, isServer?: boolean, isDev: boolean }) => string | RegExp | false)>).filter(i => typeof i === 'string'),
$resolve: async (val, get) => [
...Array.isArray(val) ? val : [],
...(await get('build.transpile')).filter(i => typeof i === 'string'),
'vue-demi',
],
},
@ -98,29 +102,29 @@ export default defineUntypedSchema({
clearScreen: true,
build: {
assetsDir: {
$resolve: async (val, get) => val ?? (await get('app') as Record<string, string>).buildAssetsDir?.replace(/^\/+/, ''),
$resolve: async (val, get) => typeof val === 'string' ? val : (await get('app')).buildAssetsDir?.replace(/^\/+/, ''),
},
emptyOutDir: false,
},
server: {
fs: {
allow: {
$resolve: async (val: string[] | undefined, get) => {
const [buildDir, srcDir, rootDir, workspaceDir, modulesDir] = await Promise.all([get('buildDir'), get('srcDir'), get('rootDir'), get('workspaceDir'), get('modulesDir')]) as [string, string, string, string, string]
$resolve: async (val, get) => {
const [buildDir, srcDir, rootDir, workspaceDir, modulesDir] = await Promise.all([get('buildDir'), get('srcDir'), get('rootDir'), get('workspaceDir'), get('modulesDir')])
return [...new Set([
buildDir,
srcDir,
rootDir,
workspaceDir,
...(modulesDir),
...val ?? [],
...Array.isArray(val) ? val : [],
])]
},
},
},
},
cacheDir: {
$resolve: async (val, get) => val ?? resolve(await get('rootDir') as string, 'node_modules/.cache/vite'),
$resolve: async (val, get) => typeof val === 'string' ? val : resolve(await get('rootDir'), 'node_modules/.cache/vite'),
},
},
})

View File

@ -1,8 +1,7 @@
import { defu } from 'defu'
import { defineUntypedSchema } from 'untyped'
import type { VueLoaderOptions } from 'vue-loader'
import { defineResolvers } from '../utils/definition'
export default defineUntypedSchema({
export default defineResolvers({
webpack: {
/**
* Nuxt uses `webpack-bundle-analyzer` to visualize your bundles and how to optimize them.
@ -17,8 +16,8 @@ export default defineUntypedSchema({
* @type {boolean | { enabled?: boolean } & typeof import('webpack-bundle-analyzer').BundleAnalyzerPlugin.Options}
*/
analyze: {
$resolve: async (val: boolean | { enabled?: boolean } | Record<string, unknown>, get) => {
const value = typeof val === 'boolean' ? { enabled: val } : val
$resolve: async (val, get) => {
const value = typeof val === 'boolean' ? { enabled: val } : (val && typeof val === 'object' ? val : {})
return defu(value, await get('build.analyze') as { enabled?: boolean } | Record<string, unknown>)
},
},
@ -83,7 +82,7 @@ export default defineUntypedSchema({
* Enables CSS source map support (defaults to `true` in development).
*/
cssSourceMap: {
$resolve: async (val, get) => val ?? await get('dev'),
$resolve: async (val, get) => typeof val === 'boolean' ? val : await get('dev'),
},
/**
@ -147,7 +146,10 @@ export default defineUntypedSchema({
for (const name of styleLoaders) {
const loader = loaders[name]
if (loader && loader.sourceMap === undefined) {
loader.sourceMap = Boolean(await get('build.cssSourceMap'))
loader.sourceMap = Boolean(
// @ts-expect-error TODO: remove legacay configuration
await get('build.cssSourceMap'),
)
}
}
return loaders
@ -165,30 +167,27 @@ export default defineUntypedSchema({
/**
* @see [`file-loader` Options](https://github.com/webpack-contrib/file-loader#options)
* @type {Omit<typeof import('file-loader')['Options'], 'name'>}
* @default
* ```ts
* { esModule: false }
* ```
*/
file: { esModule: false },
file: { esModule: false, limit: 1000 },
/**
* @see [`file-loader` Options](https://github.com/webpack-contrib/file-loader#options)
* @type {Omit<typeof import('file-loader')['Options'], 'name'>}
* @default
* ```ts
* { esModule: false, limit: 1000 }
* { esModule: false }
* ```
*/
fontUrl: { esModule: false, limit: 1000 },
/**
* @see [`file-loader` Options](https://github.com/webpack-contrib/file-loader#options)
* @type {Omit<typeof import('file-loader')['Options'], 'name'>}
* @default
* ```ts
* { esModule: false, limit: 1000 }
* { esModule: false }
* ```
*/
imgUrl: { esModule: false, limit: 1000 },
@ -205,26 +204,38 @@ export default defineUntypedSchema({
*/
vue: {
transformAssetUrls: {
$resolve: async (val, get) => (val ?? (await get('vue.transformAssetUrls'))) as VueLoaderOptions['transformAssetUrls'],
$resolve: async (val, get) => (val ?? (await get('vue.transformAssetUrls'))),
},
compilerOptions: {
$resolve: async (val, get) => (val ?? (await get('vue.compilerOptions'))) as VueLoaderOptions['compilerOptions'],
$resolve: async (val, get) => (val ?? (await get('vue.compilerOptions'))),
},
propsDestructure: {
$resolve: async (val, get) => Boolean(val ?? await get('vue.propsDestructure')),
},
} satisfies { [K in keyof VueLoaderOptions]: { $resolve: (val: unknown, get: (id: string) => Promise<unknown>) => Promise<VueLoaderOptions[K]> } },
},
/**
* See [css-loader](https://github.com/webpack-contrib/css-loader) for available options.
*/
css: {
importLoaders: 0,
/**
* @type {boolean | { filter: (url: string, resourcePath: string) => boolean }}
*/
url: {
filter: (url: string, _resourcePath: string) => url[0] !== '/',
},
esModule: false,
},
/**
* See [css-loader](https://github.com/webpack-contrib/css-loader) for available options.
*/
cssModules: {
importLoaders: 0,
/**
* @type {boolean | { filter: (url: string, resourcePath: string) => boolean }}
*/
url: {
filter: (url: string, _resourcePath: string) => url[0] !== '/',
},
@ -241,7 +252,6 @@ export default defineUntypedSchema({
/**
* @see [`sass-loader` Options](https://github.com/webpack-contrib/sass-loader#options)
* @type {typeof import('sass-loader')['Options']}
* @default
* ```ts
* {
@ -259,7 +269,6 @@ export default defineUntypedSchema({
/**
* @see [`sass-loader` Options](https://github.com/webpack-contrib/sass-loader#options)
* @type {typeof import('sass-loader')['Options']}
*/
scss: {},
@ -300,7 +309,14 @@ export default defineUntypedSchema({
* @type {false | typeof import('css-minimizer-webpack-plugin').BasePluginOptions & typeof import('css-minimizer-webpack-plugin').DefinedDefaultMinimizerAndOptions<any>}
*/
optimizeCSS: {
$resolve: async (val, get) => val ?? (await get('build.extractCSS') ? {} : false),
$resolve: async (val, get) => {
if (val === false || (val && typeof val === 'object')) {
return val
}
// @ts-expect-error TODO: remove legacy configuration
const extractCSS = await get('build.extractCSS')
return extractCSS ? {} : false
},
},
/**
@ -310,7 +326,9 @@ export default defineUntypedSchema({
optimization: {
runtimeChunk: 'single',
/** Set minimize to `false` to disable all minimizers. (It is disabled in development by default). */
minimize: { $resolve: async (val, get) => val ?? !(await get('dev')) },
minimize: {
$resolve: async (val, get) => typeof val === 'boolean' ? val : !(await get('dev')),
},
/** You can set minimizer to a customized array of plugins. */
minimizer: undefined,
splitChunks: {
@ -323,15 +341,12 @@ export default defineUntypedSchema({
/**
* Customize PostCSS Loader.
* same options as [`postcss-loader` options](https://github.com/webpack-contrib/postcss-loader#options)
* @type {{ execute?: boolean, postcssOptions: typeof import('postcss').ProcessOptions, sourceMap?: boolean, implementation?: any }}
* @type {{ execute?: boolean, postcssOptions: typeof import('postcss').ProcessOptions & { plugins: Record<string, unknown> & { autoprefixer?: typeof import('autoprefixer').Options; cssnano?: typeof import('cssnano').Options } }, sourceMap?: boolean, implementation?: any }}
*/
postcss: {
postcssOptions: {
config: {
$resolve: async (val, get) => val ?? (await get('postcss.config')),
},
plugins: {
$resolve: async (val, get) => val ?? (await get('postcss.plugins')),
$resolve: async (val, get) => val && typeof val === 'object' ? val : (await get('postcss.plugins')),
},
},
},

View File

@ -21,6 +21,12 @@ export interface ModuleMeta {
*/
compatibility?: NuxtCompatibility
/**
* Fully resolved path used internally by Nuxt. Do not depend on this value.
* @internal
*/
rawPath?: string
[key: string]: unknown
}

View File

@ -0,0 +1,56 @@
import type { InputObject } from 'untyped'
import { defineUntypedSchema } from 'untyped'
import type { ConfigSchema } from '../../schema/config'
type KeysOf<T, Prefix extends string | unknown = unknown> = keyof T extends string
?
{
[K in keyof T]: K extends string
? string extends K
? never // exclude generic 'string' type
: unknown extends Prefix
? `${K | KeysOf<T[K], K>}`
: Prefix extends string
? `${Prefix}.${K | KeysOf<T[K], `hey.${Prefix}.${K}`>}`
: never
: never
}[keyof T]
: never
type ReturnFromKey<T, K extends string> = keyof T extends string
? K extends keyof T
? T[K]
: K extends `${keyof T}.${string}`
? K extends `${infer Prefix}.${string}`
? Prefix extends keyof T
? K extends `${Prefix}.${infer Suffix}`
? ReturnFromKey<T[Prefix], Suffix>
: never
: never
: never
: never
: never
type Awaitable<T> = T | Promise<T>
interface Resolvers<ReturnValue> {
$resolve: (val: unknown, get: <K extends KeysOf<ConfigSchema>>(key: K) => Promise<ReturnFromKey<ConfigSchema, K>>) => Awaitable<ReturnValue>
$schema?: InputObject['$schema']
$default?: ReturnValue
}
type Resolvable<Namespace> = keyof Exclude<NonNullable<Namespace>, boolean | string | (() => any)> extends string
? {
[K in keyof Namespace]: Partial<Resolvable<Namespace[K]>> | Resolvers<Namespace[K]>
} | Namespace
: Namespace | Resolvers<Namespace>
export function defineResolvers<C extends Partial<Resolvable<ConfigSchema>>> (config: C) {
return defineUntypedSchema(config) /* as C */
}
export type ResolvableConfigSchema = Partial<Resolvable<ConfigSchema>>
export { defineUntypedSchema } from 'untyped'

View File

@ -708,6 +708,15 @@ importers:
'@types/pug':
specifier: 2.0.10
version: 2.0.10
'@types/rollup-plugin-visualizer':
specifier: 4.2.4
version: 4.2.4
'@types/webpack-bundle-analyzer':
specifier: 4.7.0
version: 4.7.0
'@types/webpack-hot-middleware':
specifier: 2.25.9
version: 2.25.9
'@unhead/schema':
specifier: 1.11.18
version: 1.11.18
@ -735,6 +744,9 @@ importers:
compatx:
specifier: 0.1.8
version: 0.1.8
css-minimizer-webpack-plugin:
specifier: 7.0.0
version: 7.0.0(webpack@5.96.1)
esbuild-loader:
specifier: 4.2.2
version: 4.2.2(webpack@5.96.1)
@ -750,6 +762,9 @@ importers:
ignore:
specifier: 7.0.3
version: 7.0.3
mini-css-extract-plugin:
specifier: 2.9.2
version: 2.9.2(webpack@5.96.1)
nitropack:
specifier: 2.10.4
version: 2.10.4(typescript@5.7.3)
@ -759,6 +774,9 @@ importers:
pkg-types:
specifier: 1.3.1
version: 1.3.1
postcss:
specifier: 8.5.1
version: 8.5.1
sass-loader:
specifier: 16.0.4
version: 16.0.4(@rspack/core@1.2.2)(webpack@5.96.1)
@ -2966,6 +2984,9 @@ packages:
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
'@types/rollup-plugin-visualizer@4.2.4':
resolution: {integrity: sha512-BW4Q6D1Qy5gno5qHWrnMDC2dOe/TAKXvqCpckOggCCu+XpS+ZZJJ1lq1+K3bvYccoO3Y7f5kglbFAgYGqCgULg==}
'@types/semver@7.5.8':
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
@ -9892,6 +9913,10 @@ snapshots:
'@types/resolve@1.20.2': {}
'@types/rollup-plugin-visualizer@4.2.4':
dependencies:
rollup: 4.34.6
'@types/semver@7.5.8': {}
'@types/unist@2.0.11': {}