Merge branch 'main' of github.com:nuxt/nuxt into feat/unhead-v2

This commit is contained in:
harlan 2025-02-12 12:17:57 +11:00
commit ec6dfd2c49
76 changed files with 1745 additions and 1215 deletions

14
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,14 @@
# Core Requirements
- The end goal is stability, speed and great user experience.
## Code Quality Requirements
- Follow standard TypeScript conventions and best practices
- Use the Composition API when creating Vue components
- Use clear, descriptive variable and function names
- Add comments to explain complex logic or non-obvious implementations
- Write unit tests for core functionality using `vitest`
- Write end-to-end tests using Playwright and `@nuxt/test-utils`
- Keep functions focused and manageable (generally under 50 lines)
- Use error handling patterns consistently

View File

@ -90,22 +90,35 @@ Read more about `$fetch`.
### Pass Client Headers to the API ### Pass Client Headers to the API
During server-side-rendering, since the `$fetch` request takes place 'internally' within the server, it won't include the user's browser cookies. When calling `useFetch` on the server, Nuxt will use [`useRequestFetch`](/docs/api/composables/use-request-fetch) to proxy client headers and cookies (with the exception of headers not meant to be forwarded, like `host`).
We can use [`useRequestHeaders`](/docs/api/composables/use-request-headers) to access and proxy cookies to the API from server-side. ```vue
<script setup lang="ts">
const { data } = await useFetch('/api/echo');
</script>
```
The example below adds the request headers to an isomorphic `$fetch` call to ensure that the API endpoint has access to the same `cookie` header originally sent by the user. ```ts
// /api/echo.ts
export default defineEventHandler(event => parseCookies(event))
```
Alternatively, the example below shows how to use [`useRequestHeaders`](/docs/api/composables/use-request-headers) to access and send cookies to the API from a server-side request (originating on the client). Using an isomorphic `$fetch` call, we ensure that the API endpoint has access to the same `cookie` header originally sent by the user's browser. This is only necessary if you aren't using `useFetch`.
```vue ```vue
<script setup lang="ts"> <script setup lang="ts">
const headers = useRequestHeaders(['cookie']) const headers = useRequestHeaders(['cookie'])
async function getCurrentUser() { async function getCurrentUser() {
return await $fetch('/api/me', { headers: headers.value }) return await $fetch('/api/me', { headers })
} }
</script> </script>
``` ```
::tip
You can also use [`useRequestFetch`](/docs/api/composables/use-request-fetch) to proxy headers to the call automatically.
::
::caution ::caution
Be very careful before proxying headers to an external API and just include headers that you need. Not all headers are safe to be bypassed and might introduce unwanted behavior. Here is a list of common headers that are NOT to be proxied: Be very careful before proxying headers to an external API and just include headers that you need. Not all headers are safe to be bypassed and might introduce unwanted behavior. Here is a list of common headers that are NOT to be proxied:
@ -115,10 +128,6 @@ Be very careful before proxying headers to an external API and just include head
- `cf-connecting-ip`, `cf-ray` - `cf-connecting-ip`, `cf-ray`
:: ::
::tip
You can also use [`useRequestFetch`](/docs/api/composables/use-request-fetch) to proxy headers to the call automatically.
::
## `useFetch` ## `useFetch`
The [`useFetch`](/docs/api/composables/use-fetch) composable uses `$fetch` under-the-hood to make SSR-safe network calls in the setup function. The [`useFetch`](/docs/api/composables/use-fetch) composable uses `$fetch` under-the-hood to make SSR-safe network calls in the setup function.

View File

@ -18,10 +18,16 @@ One of the core features of Nuxt is the layers and extending support. You can ex
## Usage ## Usage
By default, any layers within your project in the `~/layers` directory will be automatically registered as layers in your project By default, any layers within your project in the `~~/layers` directory will be automatically registered as layers in your project.
::note ::note
Layer auto-registration was introduced in Nuxt v3.12.0 Layer auto-registration was introduced in Nuxt v3.12.0.
::
In addition, named layer aliases to the `srcDir` of each of these layers will automatically be created. For example, you will be able to access the `~~/layers/test` layer via `#layers/test`.
::note
Named layer aliases were introduced in Nuxt v3.16.0.
:: ::
In addition, you can extend from a layer by adding the [extends](/docs/api/nuxt-config#extends) property to your [`nuxt.config`](/docs/guide/directory-structure/nuxt-config) file. In addition, you can extend from a layer by adding the [extends](/docs/api/nuxt-config#extends) property to your [`nuxt.config`](/docs/guide/directory-structure/nuxt-config) file.

View File

@ -472,3 +472,41 @@ Alternatively, you can render the template alongside the Nuxt app root by settin
``` ```
This avoids a white flash when hydrating a client-only page. This avoids a white flash when hydrating a client-only page.
## decorators
This option enables enabling decorator syntax across your entire Nuxt/Nitro app, powered by [esbuild](https://github.com/evanw/esbuild/releases/tag/v0.21.3).
For a long time, TypeScript has had support for decorators via `compilerOptions.experimentalDecorators`. This implementation predated the TC39 standardization process. Now, decorators are a [Stage 3 Proposal](https://github.com/tc39/proposal-decorators), and supported without special configuration in TS 5.0+ (see https://github.com/microsoft/TypeScript/pull/52582 and https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta/#decorators).
Enabling `experimental.decorators` enables support for the TC39 proposal, **NOT** for TypeScript's previous `compilerOptions.experimentalDecorators` implementation.
::warning
Note that there may be changes before this finally lands in the JS standard.
::
### Usage
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
experimental: {
decorators: true,
},
})
```
```ts [app.vue]
function something (_method: () => unknown) {
return () => 'decorated'
}
class SomeClass {
@something
public someMethod () {
return 'initial'
}
}
const value = new SomeClass().someMethod()
// this will return 'decorated'
```

View File

@ -164,9 +164,25 @@ When publishing the layer as a private npm package, you need to make sure you lo
## Tips ## Tips
### Named Layer Aliases
Auto-scanned layers (from your `~~/layers` directory) automatically create aliases. For example, you can access your `~~/layers/test` layer via `#layers/test`.
If you want to create named layer aliases for other layers, you can specify a name in the configuration of the layer.
```ts [nuxt.config.ts]
export default defineNuxtConfig({
$meta: {
name: 'example',
},
})
```
This will produce an alias of `#layers/example` which points to your layer.
### Relative Paths and Aliases ### Relative Paths and Aliases
When importing using aliases (such as `~/` and `@/`) in a layer components and composables, note that aliases are resolved relative to the user's project paths. As a workaround, you can **use relative paths** to import them. We are working on a better solution for named layer aliases. When importing using global aliases (such as `~/` and `@/`) in a layer components and composables, note that these aliases are resolved relative to the user's project paths. As a workaround, you can **use relative paths** to import them, or use named layer aliases.
Also when using relative paths in `nuxt.config` file of a layer, (with exception of nested `extends`) they are resolved relative to user's project instead of the layer. As a workaround, use full resolved paths in `nuxt.config`: Also when using relative paths in `nuxt.config` file of a layer, (with exception of nested `extends`) they are resolved relative to user's project instead of the layer. As a workaround, use full resolved paths in `nuxt.config`:

View File

@ -11,7 +11,7 @@
"build": "pnpm --filter './packages/**' prepack", "build": "pnpm --filter './packages/**' prepack",
"build:stub": "pnpm dev:prepare", "build:stub": "pnpm dev:prepare",
"dev": "pnpm play", "dev": "pnpm play",
"dev:prepare": "pnpm --filter './packages/**' prepack --stub && pnpm --filter './packages/ui-templates' build", "dev:prepare": "pnpm --filter './packages/**' prepack --stub && pnpm --filter './packages/ui-templates' build && nuxi prepare",
"debug:prepare": "TIMINGS_DEBUG=true pnpm dev:prepare", "debug:prepare": "TIMINGS_DEBUG=true pnpm dev:prepare",
"debug:build": "TIMINGS_DEBUG=true pnpm build", "debug:build": "TIMINGS_DEBUG=true pnpm build",
"debug:dev": "rm -rf **/node_modules/.cache/jiti && pnpm nuxi dev", "debug:dev": "rm -rf **/node_modules/.cache/jiti && pnpm nuxi dev",
@ -52,21 +52,21 @@
"@vue/compiler-core": "3.5.13", "@vue/compiler-core": "3.5.13",
"@vue/compiler-dom": "3.5.13", "@vue/compiler-dom": "3.5.13",
"@vue/shared": "3.5.13", "@vue/shared": "3.5.13",
"c12": "2.0.1", "c12": "2.0.2",
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703", "h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"jiti": "2.4.2", "jiti": "2.4.2",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"nitro": "npm:nitro-nightly@3.0.0-beta-28969273.f7aa9de6", "nitro": "npm:nitro-nightly@3.0.0-beta-28969273.f7aa9de6",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"ohash": "1.1.4", "ohash": "1.1.4",
"postcss": "8.5.1", "postcss": "8.5.2",
"rollup": "4.34.6", "rollup": "4.34.6",
"send": ">=1.1.0", "send": ">=1.1.0",
"typescript": "5.7.3", "typescript": "5.7.3",
"ufo": "1.5.4", "ufo": "1.5.4",
"unbuild": "3.3.1", "unbuild": "3.3.1",
"unhead": "2.0.0-alpha.9", "unhead": "2.0.0-alpha.9",
"unimport": "4.1.0", "unimport": "4.1.1",
"vite": "6.1.0", "vite": "6.1.0",
"vue": "3.5.13" "vue": "3.5.13"
}, },
@ -98,16 +98,16 @@
"cssnano": "7.0.6", "cssnano": "7.0.6",
"destr": "2.0.3", "destr": "2.0.3",
"devalue": "5.1.1", "devalue": "5.1.1",
"eslint": "9.20.0", "eslint": "9.20.1",
"eslint-plugin-no-only-tests": "3.3.0", "eslint-plugin-no-only-tests": "3.3.0",
"eslint-plugin-perfectionist": "4.8.0", "eslint-plugin-perfectionist": "4.8.0",
"eslint-typegen": "1.0.0", "eslint-typegen": "1.0.0",
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703", "h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"happy-dom": "17.0.2", "happy-dom": "17.0.4",
"installed-check": "9.3.0", "installed-check": "9.3.0",
"jiti": "2.4.2", "jiti": "2.4.2",
"knip": "5.43.6", "knip": "5.44.0",
"magic-string": "0.30.17", "magic-string": "0.30.17",
"markdownlint-cli": "0.44.0", "markdownlint-cli": "0.44.0",
"memfs": "4.17.0", "memfs": "4.17.0",
@ -134,6 +134,6 @@
"vue-tsc": "2.2.0", "vue-tsc": "2.2.0",
"webpack": "5.97.1" "webpack": "5.97.1"
}, },
"packageManager": "pnpm@10.2.1", "packageManager": "pnpm@10.3.0",
"version": "" "version": ""
} }

View File

@ -27,7 +27,7 @@
"test:attw": "attw --pack" "test:attw": "attw --pack"
}, },
"dependencies": { "dependencies": {
"c12": "^2.0.1", "c12": "^2.0.2",
"consola": "^3.4.0", "consola": "^3.4.0",
"defu": "^6.1.4", "defu": "^6.1.4",
"destr": "^2.0.3", "destr": "^2.0.3",
@ -45,12 +45,12 @@
"std-env": "^3.8.0", "std-env": "^3.8.0",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"unctx": "^2.4.1", "unctx": "^2.4.1",
"unimport": "^4.1.0", "unimport": "^4.1.1",
"untyped": "^1.5.2" "untyped": "^1.5.2"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@rspack/core": "1.2.2", "@rspack/core": "1.2.3",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"nitro": "npm:nitro-nightly@3.0.0-beta-28969273.f7aa9de6", "nitro": "npm:nitro-nightly@3.0.0-beta-28969273.f7aa9de6",
"unbuild": "3.3.1", "unbuild": "3.3.1",
@ -59,6 +59,6 @@
"webpack": "5.97.1" "webpack": "5.97.1"
}, },
"engines": { "engines": {
"node": ">=18.20.6" "node": ">=18.12.0"
} }
} }

View File

@ -1,9 +1,22 @@
import { getContext } from 'unctx' import { AsyncLocalStorage } from 'node:async_hooks'
import { createContext, getContext } from 'unctx'
import type { Nuxt } from '@nuxt/schema' import type { Nuxt } from '@nuxt/schema'
/** Direct access to the Nuxt context - see https://github.com/unjs/unctx. */ /**
* Direct access to the Nuxt global context - see https://github.com/unjs/unctx.
* @deprecated Use `getNuxtCtx` instead
*/
export const nuxtCtx = getContext<Nuxt>('nuxt') export const nuxtCtx = getContext<Nuxt>('nuxt')
/** async local storage for the name of the current nuxt instance */
const asyncNuxtStorage = createContext<Nuxt>({
asyncContext: true,
AsyncLocalStorage,
})
/** Direct access to the Nuxt context with asyncLocalStorage - see https://github.com/unjs/unctx. */
export const getNuxtCtx = () => asyncNuxtStorage.tryUse()
// TODO: Use use/tryUse from unctx. https://github.com/unjs/unctx/issues/6 // TODO: Use use/tryUse from unctx. https://github.com/unjs/unctx/issues/6
/** /**
@ -16,7 +29,7 @@ export const nuxtCtx = getContext<Nuxt>('nuxt')
* ``` * ```
*/ */
export function useNuxt (): Nuxt { export function useNuxt (): Nuxt {
const instance = nuxtCtx.tryUse() const instance = asyncNuxtStorage.tryUse() || nuxtCtx.tryUse()
if (!instance) { if (!instance) {
throw new Error('Nuxt instance is unavailable!') throw new Error('Nuxt instance is unavailable!')
} }
@ -36,5 +49,9 @@ export function useNuxt (): Nuxt {
* ``` * ```
*/ */
export function tryUseNuxt (): Nuxt | null { export function tryUseNuxt (): Nuxt | null {
return nuxtCtx.tryUse() return asyncNuxtStorage.tryUse() || nuxtCtx.tryUse()
}
export function runWithNuxtContext<T extends (...args: any[]) => any> (nuxt: Nuxt, fn: T) {
return asyncNuxtStorage.call(nuxt, fn) as ReturnType<T>
} }

View File

@ -18,7 +18,7 @@ export type { ExtendConfigOptions, ExtendViteConfigOptions, ExtendWebpackConfigO
export { assertNuxtCompatibility, checkNuxtCompatibility, getNuxtVersion, hasNuxtCompatibility, isNuxtMajorVersion, normalizeSemanticVersion, isNuxt2, isNuxt3 } from './compatibility' export { assertNuxtCompatibility, checkNuxtCompatibility, getNuxtVersion, hasNuxtCompatibility, isNuxtMajorVersion, normalizeSemanticVersion, isNuxt2, isNuxt3 } from './compatibility'
export { addComponent, addComponentsDir } from './components' export { addComponent, addComponentsDir } from './components'
export type { AddComponentOptions } from './components' export type { AddComponentOptions } from './components'
export { nuxtCtx, tryUseNuxt, useNuxt } from './context' export { getNuxtCtx, runWithNuxtContext, tryUseNuxt, useNuxt, nuxtCtx } from './context'
export { createIsIgnored, isIgnored, resolveIgnorePatterns } from './ignore' export { createIsIgnored, isIgnored, resolveIgnorePatterns } from './ignore'
export { addLayout } from './layout' export { addLayout } from './layout'
export { addRouteMiddleware, extendPages, extendRouteRules } from './pages' export { addRouteMiddleware, extendPages, extendRouteRules } from './pages'

View File

@ -7,7 +7,7 @@ import { loadConfig } from 'c12'
import type { NuxtConfig, NuxtOptions } from '@nuxt/schema' import type { NuxtConfig, NuxtOptions } from '@nuxt/schema'
import { globby } from 'globby' import { globby } from 'globby'
import defu from 'defu' import defu from 'defu'
import { join } from 'pathe' import { basename, join, relative } from 'pathe'
import { isWindows } from 'std-env' import { isWindows } from 'std-env'
import { tryResolveModule } from '../internal/esm' import { tryResolveModule } from '../internal/esm'
@ -18,14 +18,11 @@ export interface LoadNuxtConfigOptions extends Omit<LoadConfigOptions<NuxtConfig
export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<NuxtOptions> { export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<NuxtOptions> {
// Automatically detect and import layers from `~~/layers/` directory // Automatically detect and import layers from `~~/layers/` directory
opts.overrides = defu(opts.overrides, { const localLayers = await globby('layers/*', { onlyDirectories: true, cwd: opts.cwd || process.cwd() })
_extends: await globby('layers/*', { opts.overrides = defu(opts.overrides, { _extends: localLayers });
onlyDirectories: true,
cwd: opts.cwd || process.cwd(),
}),
});
(globalThis as any).defineNuxtConfig = (c: any) => c (globalThis as any).defineNuxtConfig = (c: any) => c
const result = await loadConfig<NuxtConfig>({ const { configFile, layers = [], cwd, config: nuxtConfig, meta } = await loadConfig<NuxtConfig>({
name: 'nuxt', name: 'nuxt',
configFile: 'nuxt.config', configFile: 'nuxt.config',
rcFile: '.nuxtrc', rcFile: '.nuxtrc',
@ -35,13 +32,17 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
...opts, ...opts,
}) })
delete (globalThis as any).defineNuxtConfig delete (globalThis as any).defineNuxtConfig
const { configFile, layers = [], cwd } = result
const nuxtConfig = result.config!
// Fill config // Fill config
nuxtConfig.rootDir = nuxtConfig.rootDir || cwd nuxtConfig.rootDir = nuxtConfig.rootDir || cwd
nuxtConfig._nuxtConfigFile = configFile nuxtConfig._nuxtConfigFile = configFile
nuxtConfig._nuxtConfigFiles = [configFile] nuxtConfig._nuxtConfigFiles = [configFile]
nuxtConfig.alias ||= {}
if (meta?.name) {
const alias = `#layers/${meta.name}`
nuxtConfig.alias[alias] ||= nuxtConfig.rootDir
}
const defaultBuildDir = join(nuxtConfig.rootDir!, '.nuxt') const defaultBuildDir = join(nuxtConfig.rootDir!, '.nuxt')
if (!opts.overrides?._prepare && !nuxtConfig.dev && !nuxtConfig.buildDir && existsSync(defaultBuildDir)) { if (!opts.overrides?._prepare && !nuxtConfig.dev && !nuxtConfig.buildDir && existsSync(defaultBuildDir)) {
@ -74,6 +75,18 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
// Filter layers // Filter layers
if (!layer.configFile || layer.configFile.endsWith('.nuxtrc')) { continue } if (!layer.configFile || layer.configFile.endsWith('.nuxtrc')) { continue }
// Add layer name for local layers
if (layer.cwd && cwd && localLayers.includes(relative(cwd, layer.cwd))) {
layer.meta ||= {}
layer.meta.name ||= basename(layer.cwd)
}
// Add layer alias
if (layer.meta?.name) {
const alias = `#layers/${layer.meta.name}`
nuxtConfig.alias[alias] ||= layer.config.rootDir || layer.cwd
}
_layers.push(layer) _layers.push(layer)
} }

View File

@ -3,6 +3,7 @@ import { readPackageJSON, resolvePackageJSON } from 'pkg-types'
import type { Nuxt, NuxtConfig } from '@nuxt/schema' import type { Nuxt, NuxtConfig } from '@nuxt/schema'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import { importModule, tryImportModule } from '../internal/esm' import { importModule, tryImportModule } from '../internal/esm'
import { runWithNuxtContext } from '../context'
import type { LoadNuxtConfigOptions } from './config' import type { LoadNuxtConfigOptions } from './config'
export interface LoadNuxtOptions extends LoadNuxtConfigOptions { export interface LoadNuxtOptions extends LoadNuxtConfigOptions {
@ -40,5 +41,5 @@ export async function buildNuxt (nuxt: Nuxt): Promise<any> {
const rootDir = pathToFileURL(nuxt.options.rootDir).href const rootDir = pathToFileURL(nuxt.options.rootDir).href
const { build } = await tryImportModule<typeof import('nuxt')>('nuxt-nightly', { paths: rootDir }) || await importModule<typeof import('nuxt')>('nuxt', { paths: rootDir }) const { build } = await tryImportModule<typeof import('nuxt')>('nuxt-nightly', { paths: rootDir }) || await importModule<typeof import('nuxt')>('nuxt', { paths: rootDir })
return build(nuxt) return runWithNuxtContext(nuxt, () => build(nuxt))
} }

View File

@ -20,7 +20,7 @@ const nuxt = await loadNuxt({
describe('resolvePath', () => { describe('resolvePath', () => {
it('should resolve paths correctly', async () => { it('should resolve paths correctly', async () => {
expect(await resolvePath('.nuxt/app.config')).toBe(resolve(nuxt.options.buildDir, 'app.config')) expect(await resolvePath('.nuxt/app.config')).toBe(resolve('.nuxt/app.config.mjs'))
}) })
}) })

View File

@ -180,6 +180,8 @@ export async function _generateTypes (nuxt: Nuxt) {
.then(r => r?.version && gte(r.version, '5.4.0')) .then(r => r?.version && gte(r.version, '5.4.0'))
.catch(() => isV4) .catch(() => isV4)
const useDecorators = Boolean(nuxt.options.experimental?.decorators)
// https://www.totaltypescript.com/tsconfig-cheat-sheet // https://www.totaltypescript.com/tsconfig-cheat-sheet
const tsConfig: TSConfig = defu(nuxt.options.typescript?.tsConfig, { const tsConfig: TSConfig = defu(nuxt.options.typescript?.tsConfig, {
compilerOptions: { compilerOptions: {
@ -197,12 +199,20 @@ export async function _generateTypes (nuxt: Nuxt) {
noUncheckedIndexedAccess: isV4, noUncheckedIndexedAccess: isV4,
forceConsistentCasingInFileNames: true, forceConsistentCasingInFileNames: true,
noImplicitOverride: true, noImplicitOverride: true,
/* Decorator support */
...useDecorators
? {
useDefineForClassFields: false,
experimentalDecorators: false,
}
: {},
/* If NOT transpiling with TypeScript: */ /* If NOT transpiling with TypeScript: */
module: hasTypescriptVersionWithModulePreserve ? 'preserve' : 'ESNext', module: hasTypescriptVersionWithModulePreserve ? 'preserve' : 'ESNext',
noEmit: true, noEmit: true,
/* If your code runs in the DOM: */ /* If your code runs in the DOM: */
lib: [ lib: [
'ESNext', 'ESNext',
...useDecorators ? ['esnext.decorators'] : [],
'dom', 'dom',
'dom.iterable', 'dom.iterable',
'webworker', 'webworker',

View File

@ -0,0 +1 @@
export const foo = 'bar'

View File

@ -0,0 +1 @@
export default defineNuxtConfig({})

View File

@ -0,0 +1,5 @@
export default defineNuxtConfig({
$meta: {
name: 'layer-fixture',
},
})

View File

@ -0,0 +1,28 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { loadNuxtConfig } from '@nuxt/kit'
describe('loadNuxtConfig', () => {
it('should add named aliases for local layers', async () => {
const cwd = fileURLToPath(new URL('./layer-fixture', import.meta.url))
const config = await loadNuxtConfig({ cwd })
for (const alias in config.alias) {
config.alias[alias] = config.alias[alias]!.replace(cwd, '<rootDir>')
}
expect(config.alias).toMatchInlineSnapshot(`
{
"#build": "<rootDir>/.nuxt",
"#internal/nuxt/paths": "<rootDir>/.nuxt/paths.mjs",
"#layers/layer-fixture": "<rootDir>",
"#layers/test": "<rootDir>/layers/test",
"#shared": "<rootDir>/shared",
"@": "<rootDir>",
"@@": "<rootDir>",
"assets": "<rootDir>/assets",
"public": "<rootDir>/public",
"~": "<rootDir>",
"~~": "<rootDir>",
}
`)
})
})

View File

@ -69,12 +69,12 @@
"@nuxt/devtools": "^2.0.0", "@nuxt/devtools": "^2.0.0",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.6.4", "@nuxt/telemetry": "^2.6.5",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@unhead/vue": "^2.0.0-alpha.8", "@unhead/vue": "^2.0.0-alpha.8",
"@vue/shared": "^3.5.13", "@vue/shared": "^3.5.13",
"acorn": "8.14.0", "acorn": "8.14.0",
"c12": "^2.0.1", "c12": "^2.0.2",
"chokidar": "^4.0.3", "chokidar": "^4.0.3",
"compatx": "^0.1.8", "compatx": "^0.1.8",
"consola": "^3.4.0", "consola": "^3.4.0",
@ -116,8 +116,8 @@
"uncrypto": "^0.1.3", "uncrypto": "^0.1.3",
"unctx": "^2.4.1", "unctx": "^2.4.1",
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unimport": "^4.1.0", "unimport": "^4.1.1",
"unplugin": "^2.1.2", "unplugin": "^2.2.0",
"unplugin-vue-router": "^0.11.2", "unplugin-vue-router": "^0.11.2",
"unstorage": "^1.14.4", "unstorage": "^1.14.4",
"untyped": "^1.5.2", "untyped": "^1.5.2",
@ -149,6 +149,6 @@
} }
}, },
"engines": { "engines": {
"node": "^18.20.6 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -54,6 +54,13 @@ export function useAppConfig (): AppConfig {
return nuxtApp._appConfig return nuxtApp._appConfig
} }
export function _replaceAppConfig (newConfig: AppConfig) {
const appConfig = useAppConfig()
deepAssign(appConfig, newConfig)
deepDelete(appConfig, newConfig)
}
/** /**
* Deep assign the current appConfig with the new one. * Deep assign the current appConfig with the new one.
* *

View File

@ -1,11 +1,12 @@
import { existsSync } from 'node:fs' import { existsSync } from 'node:fs'
import { rm } from 'node:fs/promises' import { rm } from 'node:fs/promises'
import { randomUUID } from 'node:crypto'
import { AsyncLocalStorage } from 'node:async_hooks' import { AsyncLocalStorage } from 'node:async_hooks'
import { join, normalize, relative, resolve } from 'pathe' import { join, normalize, relative, resolve } from 'pathe'
import { createDebugger, createHooks } from 'hookable' import { createDebugger, createHooks } from 'hookable'
import ignore from 'ignore' import ignore from 'ignore'
import type { LoadNuxtOptions } from '@nuxt/kit' import type { LoadNuxtOptions } from '@nuxt/kit'
import { addBuildPlugin, addComponent, addPlugin, addPluginTemplate, addRouteMiddleware, addServerPlugin, addTypeTemplate, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, nuxtCtx, resolveAlias, resolveFiles, resolveIgnorePatterns, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit' import { addBuildPlugin, addComponent, addPlugin, addPluginTemplate, addRouteMiddleware, addServerPlugin, addTypeTemplate, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, nuxtCtx, resolveAlias, resolveFiles, resolveIgnorePatterns, resolvePath, runWithNuxtContext, tryResolveModule, useNitro } from '@nuxt/kit'
import type { Nuxt, NuxtHooks, NuxtModule, NuxtOptions } from 'nuxt/schema' import type { Nuxt, NuxtHooks, NuxtModule, NuxtOptions } from 'nuxt/schema'
import type { PackageJson } from 'pkg-types' import type { PackageJson } from 'pkg-types'
import { readPackageJSON } from 'pkg-types' import { readPackageJSON } from 'pkg-types'
@ -53,17 +54,24 @@ import { VirtualFSPlugin } from './plugins/virtual'
export function createNuxt (options: NuxtOptions): Nuxt { export function createNuxt (options: NuxtOptions): Nuxt {
const hooks = createHooks<NuxtHooks>() const hooks = createHooks<NuxtHooks>()
const { callHook, callHookParallel, callHookWith } = hooks
hooks.callHook = (...args) => runWithNuxtContext(nuxt, () => callHook(...args))
hooks.callHookParallel = (...args) => runWithNuxtContext(nuxt, () => callHookParallel(...args))
hooks.callHookWith = (...args) => runWithNuxtContext(nuxt, () => callHookWith(...args))
const nuxt: Nuxt = { const nuxt: Nuxt = {
__name: randomUUID(),
_version: version, _version: version,
_asyncLocalStorageModule: options.experimental.debugModuleMutation ? new AsyncLocalStorage() : undefined, _asyncLocalStorageModule: options.experimental.debugModuleMutation ? new AsyncLocalStorage() : undefined,
hooks, hooks,
callHook: hooks.callHook, callHook: hooks.callHook,
addHooks: hooks.addHooks, addHooks: hooks.addHooks,
hook: hooks.hook, hook: hooks.hook,
ready: () => initNuxt(nuxt), ready: () => runWithNuxtContext(nuxt, () => initNuxt(nuxt)),
close: () => hooks.callHook('close', nuxt), close: () => hooks.callHook('close', nuxt),
vfs: {}, vfs: {},
apps: {}, apps: {},
runWithContext: fn => runWithNuxtContext(nuxt, fn),
options, options,
} }
@ -115,6 +123,14 @@ export function createNuxt (options: NuxtOptions): Nuxt {
}) })
} }
if (!nuxtCtx.tryUse()) {
// backward compatibility with 3.x
nuxtCtx.set(nuxt)
nuxt.hook('close', () => {
nuxtCtx.unset()
})
}
hooks.hookOnce('close', () => { hooks.removeAllHooks() }) hooks.hookOnce('close', () => { hooks.removeAllHooks() })
return nuxt return nuxt
@ -223,11 +239,6 @@ async function initNuxt (nuxt: Nuxt) {
} }
} }
}) })
// Set nuxt instance for useNuxt
nuxtCtx.set(nuxt)
nuxt.hook('close', () => nuxtCtx.unset())
const coreTypePackages = nuxt.options.typescript.hoist || [] const coreTypePackages = nuxt.options.typescript.hoist || []
// Disable environment types entirely if `typescript.builder` is false // Disable environment types entirely if `typescript.builder` is false
@ -454,7 +465,7 @@ async function initNuxt (nuxt: Nuxt) {
...nuxt.options._layers.filter(i => i.cwd.includes('node_modules')).map(i => i.cwd as string), ...nuxt.options._layers.filter(i => i.cwd.includes('node_modules')).map(i => i.cwd as string),
) )
// Ensure we can resolve dependencies within layers - filtering out local `~/layers` directories // Ensure we can resolve dependencies within layers - filtering out local `~~/layers` directories
const locallyScannedLayersDirs = nuxt.options._layers.map(l => resolve(l.cwd, 'layers').replace(/\/?$/, '/')) const locallyScannedLayersDirs = nuxt.options._layers.map(l => resolve(l.cwd, 'layers').replace(/\/?$/, '/'))
nuxt.options.modulesDir.push(...nuxt.options._layers nuxt.options.modulesDir.push(...nuxt.options._layers
.filter(l => l.cwd !== nuxt.options.rootDir && locallyScannedLayersDirs.every(dir => !l.cwd.startsWith(dir))) .filter(l => l.cwd !== nuxt.options.rootDir && locallyScannedLayersDirs.every(dir => !l.cwd.startsWith(dir)))
@ -861,6 +872,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
const nuxt = createNuxt(options) const nuxt = createNuxt(options)
nuxt.runWithContext(() => {
if (nuxt.options.dev && !nuxt.options.test) { if (nuxt.options.dev && !nuxt.options.test) {
nuxt.hooks.hookOnce('build:done', () => { nuxt.hooks.hookOnce('build:done', () => {
for (const dep of keyDependencies) { for (const dep of keyDependencies) {
@ -882,6 +894,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
) { ) {
createDebugger(nuxt.hooks, { tag: 'nuxt' }) createDebugger(nuxt.hooks, { tag: 'nuxt' })
} }
})
if (opts.ready !== false) { if (opts.ready !== false) {
await nuxt.ready() await nuxt.ready()

View File

@ -1,5 +1,4 @@
import type { Literal, Property, SpreadElement } from 'estree' import type { Literal, Property, SpreadElement } from 'estree'
import { transform } from 'esbuild'
import { defu } from 'defu' import { defu } from 'defu'
import { findExports } from 'mlly' import { findExports } from 'mlly'
import type { Nuxt } from '@nuxt/schema' import type { Nuxt } from '@nuxt/schema'
@ -8,7 +7,7 @@ import MagicString from 'magic-string'
import { normalize } from 'pathe' import { normalize } from 'pathe'
import type { ObjectPlugin, PluginMeta } from 'nuxt/app' import type { ObjectPlugin, PluginMeta } from 'nuxt/app'
import { parseAndWalk, withLocations } from '../../core/utils/parse' import { parseAndWalk, transform, withLocations } from '../../core/utils/parse'
import { logger } from '../../utils' import { logger } from '../../utils'
const internalOrderMap = { const internalOrderMap = {

View File

@ -1,9 +1,8 @@
import { transform } from 'esbuild'
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { hash } from 'ohash' import { hash } from 'ohash'
import { parseAndWalk, withLocations } from '../../core/utils/parse' import { parseAndWalk, transform, withLocations } from '../../core/utils/parse'
import { isJS, isVue } from '../utils' import { isJS, isVue } from '../utils'
export function PrehydrateTransformPlugin (options: { sourcemap?: boolean } = {}) { export function PrehydrateTransformPlugin (options: { sourcemap?: boolean } = {}) {

View File

@ -7,7 +7,7 @@ import escapeRE from 'escape-string-regexp'
import { hash } from 'ohash' import { hash } from 'ohash'
import { camelCase } from 'scule' import { camelCase } from 'scule'
import { filename } from 'pathe/utils' import { filename } from 'pathe/utils'
import type { NuxtTemplate } from 'nuxt/schema' import type { NuxtOptions, NuxtTemplate } from 'nuxt/schema'
import type { Nitro } from 'nitro/types' import type { Nitro } from 'nitro/types'
import { annotatePlugins, checkForCircularDependencies } from './app' import { annotatePlugins, checkForCircularDependencies } from './app'
@ -185,9 +185,18 @@ export const schemaTemplate: NuxtTemplate = {
const relativeRoot = relative(resolve(nuxt.options.buildDir, 'types'), nuxt.options.rootDir) 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 getImportName = (name: string) => (name[0] === '.' ? './' + join(relativeRoot, name) : name).replace(IMPORT_NAME_RE, '')
const modules = nuxt.options._installedModules const modules: [string, string, NuxtOptions['_installedModules'][number]][] = []
.filter(m => m.meta && m.meta.configKey && m.meta.name && !m.meta.name.startsWith('nuxt:') && m.meta.name !== 'nuxt-config-schema') for (const m of nuxt.options._installedModules) {
.map(m => [genString(m.meta.configKey), getImportName(m.entryPath || m.meta.name), m] as const) // 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) const privateRuntimeConfig = Object.create(null)
for (const key in nuxt.options.runtimeConfig) { for (const key in nuxt.options.runtimeConfig) {
@ -210,7 +219,7 @@ export const schemaTemplate: NuxtTemplate = {
} else if (mod.meta?.repository) { } else if (mod.meta?.repository) {
if (typeof mod.meta.repository === 'string') { if (typeof mod.meta.repository === 'string') {
link = mod.meta.repository 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 link = mod.meta.repository.url
} }
if (link) { if (link) {
@ -428,12 +437,12 @@ import { defuFn } from 'defu'
const inlineConfig = ${JSON.stringify(nuxt.options.appConfig, null, 2)} const inlineConfig = ${JSON.stringify(nuxt.options.appConfig, null, 2)}
/** client **/ /** client **/
import { updateAppConfig } from '#app/config' import { _replaceAppConfig } from '#app/config'
// Vite - webpack is handled directly in #app/config // Vite - webpack is handled directly in #app/config
if (import.meta.dev && !import.meta.nitro && import.meta.hot) { if (import.meta.dev && !import.meta.nitro && import.meta.hot) {
import.meta.hot.accept((newModule) => { import.meta.hot.accept((newModule) => {
updateAppConfig(newModule.default) _replaceAppConfig(newModule.default)
}) })
} }
/** client-end **/ /** client-end **/

View File

@ -1,22 +1,17 @@
import { walk as _walk } from 'estree-walker' import { walk as _walk } from 'estree-walker'
import type { Node, SyncHandler } from 'estree-walker' import type { Node, SyncHandler } from 'estree-walker'
import type { import type { ArrowFunctionExpression, CatchClause, Program as ESTreeProgram, FunctionDeclaration, FunctionExpression, Identifier, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, VariableDeclaration } from 'estree'
ArrowFunctionExpression,
CatchClause,
Program as ESTreeProgram,
FunctionDeclaration,
FunctionExpression,
Identifier,
ImportDefaultSpecifier,
ImportNamespaceSpecifier,
ImportSpecifier,
VariableDeclaration,
} from 'estree'
import { parse } from 'acorn' import { parse } from 'acorn'
import type { Program } from 'acorn' import type { Program } from 'acorn'
import { type SameShape, type TransformOptions, type TransformResult, transform as esbuildTransform } from 'esbuild'
import { tryUseNuxt } from '@nuxt/kit'
export type { Node } export type { Node }
export async function transform<T extends TransformOptions> (input: string | Uint8Array, options?: SameShape<TransformOptions, T>): Promise<TransformResult<T>> {
return await esbuildTransform(input, { ...tryUseNuxt()?.options.esbuild.options, ...options })
}
type WithLocations<T> = T & { start: number, end: number } type WithLocations<T> = T & { start: number, end: number }
type WalkerCallback = (this: ThisParameterType<SyncHandler>, node: WithLocations<Node>, parent: WithLocations<Node> | null, ctx: { key: string | number | symbol | null | undefined, index: number | null | undefined, ast: Program | Node }) => void type WalkerCallback = (this: ThisParameterType<SyncHandler>, node: WithLocations<Node>, parent: WithLocations<Node> | null, ctx: { key: string | number | symbol | null | undefined, index: number | null | undefined, ast: Program | Node }) => void

View File

@ -9,7 +9,7 @@ import escapeRE from 'escape-string-regexp'
import { lookupNodeModuleSubpath, parseNodeModulePath } from 'mlly' import { lookupNodeModuleSubpath, parseNodeModulePath } from 'mlly'
import { isDirectory, logger } from '../utils' import { isDirectory, logger } from '../utils'
import { TransformPlugin } from './transform' import { TransformPlugin } from './transform'
import { defaultPresets } from './presets' import { appCompatPresets, defaultPresets } from './presets'
export default defineNuxtModule<Partial<ImportsOptions>>({ export default defineNuxtModule<Partial<ImportsOptions>>({
meta: { meta: {
@ -30,11 +30,16 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
exclude: undefined, exclude: undefined,
}, },
virtualImports: ['#imports'], virtualImports: ['#imports'],
polyfills: true,
}), }),
async setup (options, nuxt) { async setup (options, nuxt) {
// TODO: fix sharing of defaults between invocations of modules // TODO: fix sharing of defaults between invocations of modules
const presets = JSON.parse(JSON.stringify(options.presets)) as ImportPresetWithDeprecation[] const presets = JSON.parse(JSON.stringify(options.presets)) as ImportPresetWithDeprecation[]
if (nuxt.options.imports.polyfills) {
presets.push(...appCompatPresets)
}
// Allow modules extending sources // Allow modules extending sources
await nuxt.callHook('imports:sources', presets) await nuxt.callHook('imports:sources', presets)

View File

@ -21,14 +21,6 @@ const granularAppPresets: InlinePreset[] = [
imports: ['useNuxtApp', 'tryUseNuxtApp', 'defineNuxtPlugin', 'definePayloadPlugin', 'useRuntimeConfig', 'defineAppConfig'], imports: ['useNuxtApp', 'tryUseNuxtApp', 'defineNuxtPlugin', 'definePayloadPlugin', 'useRuntimeConfig', 'defineAppConfig'],
from: '#app/nuxt', from: '#app/nuxt',
}, },
{
imports: ['requestIdleCallback', 'cancelIdleCallback'],
from: '#app/compat/idle-callback',
},
{
imports: ['setInterval'],
from: '#app/compat/interval',
},
{ {
imports: ['useAppConfig', 'updateAppConfig'], imports: ['useAppConfig', 'updateAppConfig'],
from: '#app/config', from: '#app/config',
@ -262,6 +254,17 @@ const vueTypesPreset = defineUnimportPreset({
], ],
}) })
export const appCompatPresets: InlinePreset[] = [
{
imports: ['requestIdleCallback', 'cancelIdleCallback'],
from: '#app/compat/idle-callback',
},
{
imports: ['setInterval'],
from: '#app/compat/interval',
},
]
export const defaultPresets: InlinePreset[] = [ export const defaultPresets: InlinePreset[] = [
...commonPresets, ...commonPresets,
...granularAppPresets, ...granularAppPresets,

View File

@ -1,11 +1,10 @@
import { runInNewContext } from 'node:vm' import { runInNewContext } from 'node:vm'
import { transform } from 'esbuild'
import type { NuxtPage } from '@nuxt/schema' import type { NuxtPage } from '@nuxt/schema'
import type { NitroRouteConfig } from 'nitro/types' import type { NitroRouteConfig } from 'nitro/types'
import { normalize } from 'pathe' import { normalize } from 'pathe'
import { getLoader } from '../core/utils' import { getLoader } from '../core/utils'
import { parseAndWalk } from '../core/utils/parse' import { parseAndWalk, transform } from '../core/utils/parse'
import { extractScriptContent, pathToNitroGlob } from './utils' import { extractScriptContent, pathToNitroGlob } from './utils'
const ROUTE_RULE_RE = /\bdefineRouteRules\(/ const ROUTE_RULE_RE = /\bdefineRouteRules\(/

View File

@ -7,12 +7,11 @@ import { genArrayFromRaw, genDynamicImport, genImport, genSafeVariableName } fro
import escapeRE from 'escape-string-regexp' import escapeRE from 'escape-string-regexp'
import { filename } from 'pathe/utils' import { filename } from 'pathe/utils'
import { hash } from 'ohash' import { hash } from 'ohash'
import { transform } from 'esbuild'
import type { Property } from 'estree' import type { Property } from 'estree'
import type { NuxtPage } from 'nuxt/schema' import type { NuxtPage } from 'nuxt/schema'
import { klona } from 'klona' import { klona } from 'klona'
import { parseAndWalk, withLocations } from '../core/utils/parse' import { parseAndWalk, transform, withLocations } from '../core/utils/parse'
import { getLoader, uniqueBy } from '../core/utils' import { getLoader, uniqueBy } from '../core/utils'
import { logger, toArray } from '../utils' import { logger, toArray } from '../utils'

View File

@ -1,7 +1,8 @@
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { afterEach, describe, expect, it, vi } from 'vitest' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { normalize } from 'pathe' import { normalize } from 'pathe'
import { withoutTrailingSlash } from 'ufo' import { withoutTrailingSlash } from 'ufo'
import { logger, tryUseNuxt, useNuxt } from '@nuxt/kit'
import { loadNuxt } from '../src' import { loadNuxt } from '../src'
const repoRoot = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../', import.meta.url)))) const repoRoot = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../', import.meta.url))))
@ -12,6 +13,7 @@ vi.stubGlobal('console', {
warn: vi.fn(console.warn), warn: vi.fn(console.warn),
}) })
const loggerWarn = vi.spyOn(logger, 'warn')
vi.mock('pkg-types', async (og) => { vi.mock('pkg-types', async (og) => {
const originalPkgTypes = (await og<typeof import('pkg-types')>()) const originalPkgTypes = (await og<typeof import('pkg-types')>())
return { return {
@ -20,6 +22,9 @@ vi.mock('pkg-types', async (og) => {
} }
}) })
beforeEach(() => {
loggerWarn.mockClear()
})
afterEach(() => { afterEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
}) })
@ -41,4 +46,41 @@ describe('loadNuxt', () => {
await nuxt.close() await nuxt.close()
expect(hookRan).toBe(true) expect(hookRan).toBe(true)
}) })
it('load multiple nuxt', async () => {
await Promise.all([
loadNuxt({
cwd: repoRoot,
}),
loadNuxt({
cwd: repoRoot,
}),
])
expect(loggerWarn).not.toHaveBeenCalled()
})
it('expect hooks to get the correct context outside of initNuxt', async () => {
const nuxt = await loadNuxt({
cwd: repoRoot,
})
// @ts-expect-error - random hook
nuxt.hook('test', () => {
expect(useNuxt().__name).toBe(nuxt.__name)
})
expect(tryUseNuxt()?.__name).not.toBe(nuxt.__name)
// second nuxt context
const second = await loadNuxt({
cwd: repoRoot,
})
expect(second.__name).not.toBe(nuxt.__name)
expect(tryUseNuxt()?.__name).not.toBe(nuxt.__name)
// @ts-expect-error - random hook
await nuxt.callHook('test')
expect(loggerWarn).not.toHaveBeenCalled()
})
}) })

View File

@ -32,13 +32,13 @@
"dependencies": { "dependencies": {
"@nuxt/friendly-errors-webpack-plugin": "^2.6.0", "@nuxt/friendly-errors-webpack-plugin": "^2.6.0",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@rspack/core": "^1.2.2", "@rspack/core": "^1.2.3",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0", "css-minimizer-webpack-plugin": "^7.0.0",
"cssnano": "^7.0.6", "cssnano": "^7.0.6",
"defu": "^6.1.4", "defu": "^6.1.4",
"esbuild-loader": "^4.2.2", "esbuild-loader": "^4.3.0",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.2", "fork-ts-checker-webpack-plugin": "^9.0.2",
@ -51,7 +51,7 @@
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^2.0.2", "pathe": "^2.0.2",
"pify": "^6.1.0", "pify": "^6.1.0",
"postcss": "^8.5.1", "postcss": "^8.5.2",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-import-resolver": "^2.0.0", "postcss-import-resolver": "^2.0.0",
"postcss-loader": "^8.1.1", "postcss-loader": "^8.1.1",
@ -61,7 +61,7 @@
"time-fix-plugin": "^2.0.7", "time-fix-plugin": "^2.0.7",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unplugin": "^2.1.2", "unplugin": "^2.2.0",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.1", "vue-bundle-renderer": "^2.1.1",
"vue-loader": "^17.4.2", "vue-loader": "^17.4.2",
@ -83,6 +83,6 @@
"vue": "^3.3.4" "vue": "^3.3.4"
}, },
"engines": { "engines": {
"node": "^18.20.6 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -39,6 +39,7 @@ export default defineBuildConfig({
'consola', 'consola',
'css-minimizer-webpack-plugin', 'css-minimizer-webpack-plugin',
'cssnano', 'cssnano',
'esbuild',
'esbuild-loader', 'esbuild-loader',
'file-loader', 'file-loader',
'h3', 'h3',

View File

@ -37,28 +37,35 @@
}, },
"devDependencies": { "devDependencies": {
"@types/pug": "2.0.10", "@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": "2.0.0-alpha.9", "@unhead/schema": "2.0.0-alpha.9",
"@vitejs/plugin-vue": "5.2.1", "@vitejs/plugin-vue": "5.2.1",
"@vitejs/plugin-vue-jsx": "4.1.1", "@vitejs/plugin-vue-jsx": "4.1.1",
"@vue/compiler-core": "3.5.13", "@vue/compiler-core": "3.5.13",
"@vue/compiler-sfc": "3.5.13", "@vue/compiler-sfc": "3.5.13",
"@vue/language-core": "2.2.0", "@vue/language-core": "2.2.0",
"c12": "2.0.1", "c12": "2.0.2",
"chokidar": "4.0.3", "chokidar": "4.0.3",
"compatx": "0.1.8", "compatx": "0.1.8",
"esbuild-loader": "4.2.2", "css-minimizer-webpack-plugin": "7.0.0",
"esbuild": "0.25.0",
"esbuild-loader": "4.3.0",
"file-loader": "6.2.0", "file-loader": "6.2.0",
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703", "h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"hookable": "5.5.3", "hookable": "5.5.3",
"ignore": "7.0.3", "ignore": "7.0.3",
"mini-css-extract-plugin": "2.9.2",
"nitro": "npm:nitro-nightly@3.0.0-beta-28969273.f7aa9de6", "nitro": "npm:nitro-nightly@3.0.0-beta-28969273.f7aa9de6",
"ofetch": "1.4.1", "ofetch": "1.4.1",
"pkg-types": "1.3.1", "pkg-types": "1.3.1",
"postcss": "8.5.2",
"sass-loader": "16.0.4", "sass-loader": "16.0.4",
"scule": "1.3.0", "scule": "1.3.0",
"unbuild": "3.3.1", "unbuild": "3.3.1",
"unctx": "2.4.1", "unctx": "2.4.1",
"unimport": "4.1.0", "unimport": "4.1.1",
"untyped": "1.5.2", "untyped": "1.5.2",
"vite": "6.1.0", "vite": "6.1.0",
"vue": "3.5.13", "vue": "3.5.13",

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. * Configure Nuxt component auto-registration.
* *
@ -14,10 +14,13 @@ export default defineUntypedSchema({
if (Array.isArray(val)) { if (Array.isArray(val)) {
return { dirs: val } return { dirs: val }
} }
if (val === undefined || val === true) { if (val === false) {
return { dirs: [{ path: '~/components/global', global: true }, '~/components'] } 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 { defu } from 'defu'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import { defineResolvers } from '../utils/definition'
import type { AppHeadMetaObject } from '../types/head' import type { AppHeadMetaObject } from '../types/head'
import type { NuxtAppConfig } from '../types/config'
export default defineUntypedSchema({ export default defineResolvers({
/** /**
* Vue.js config * Vue.js config
*/ */
@ -27,7 +28,18 @@ export default defineUntypedSchema({
* Include Vue compiler in runtime bundle. * Include Vue compiler in runtime bundle.
*/ */
runtimeCompiler: { 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.. * 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) * @see [Vue app config documentation](https://vuejs.org/api/application.html#app-config)
*/ */
config: undefined, config: {},
}, },
/** /**
@ -68,12 +80,22 @@ export default defineUntypedSchema({
* ``` * ```
*/ */
baseURL: { 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. */ /** 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: { 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: { 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']} * @type {typeof import('../src/types/config').NuxtAppConfig['head']}
*/ */
head: { head: {
$resolve: async (val: Partial<AppHeadMetaObject> | undefined, get) => { $resolve: async (_val, get) => {
const resolved = defu(val, await get('meta') as Partial<AppHeadMetaObject>, { // @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: [], meta: [],
link: [], link: [],
style: [], style: [],
script: [], script: [],
noscript: [], noscript: [],
} as Required<Pick<AppHeadMetaObject, 'meta' | 'link' | 'style' | 'script' | 'noscript'>>) } satisfies NormalizedMetaObject)
// provides default charset and viewport if not set // provides default charset and viewport if not set
if (!resolved.meta.find(m => m.charset)?.charset) { if (!resolved.meta.find(m => m.charset)?.charset) {
@ -190,9 +223,13 @@ export default defineUntypedSchema({
* @type {typeof import('../src/types/config').NuxtAppConfig['viewTransition']} * @type {typeof import('../src/types/config').NuxtAppConfig['viewTransition']}
*/ */
viewTransition: { viewTransition: {
$resolve: async (val, get) => val ?? await (get('experimental') as Promise<Record<string, any>>).then( $resolve: async (val, get) => {
e => e?.viewTransition, if (val === 'always' || typeof val === 'boolean') {
) ?? false, return val
}
return await get('experimental').then(e => e.viewTransition) ?? false
},
}, },
/** /**
@ -211,14 +248,14 @@ export default defineUntypedSchema({
* @deprecated Prefer `rootAttrs.id` instead * @deprecated Prefer `rootAttrs.id` instead
*/ */
rootId: { rootId: {
$resolve: val => val === false ? false : (val || '__nuxt'), $resolve: val => val === false ? false : (val && typeof val === 'string' ? val : '__nuxt'),
}, },
/** /**
* Customize Nuxt root element tag. * Customize Nuxt root element tag.
*/ */
rootTag: { 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} * @type {typeof import('@unhead/schema').HtmlAttributes}
*/ */
rootAttrs: { rootAttrs: {
$resolve: async (val: undefined | null | Record<string, unknown>, get) => { $resolve: async (val, get) => {
const rootId = await get('app.rootId') const rootId = await get('app.rootId')
return defu(val, { return {
id: rootId === false ? undefined : (rootId || '__nuxt'), id: rootId === false ? undefined : (rootId || '__nuxt'),
}) ...typeof val === 'object' ? val : {},
}
}, },
}, },
@ -238,7 +276,7 @@ export default defineUntypedSchema({
* Customize Nuxt Teleport element tag. * Customize Nuxt Teleport element tag.
*/ */
teleportTag: { 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 * @deprecated Prefer `teleportAttrs.id` instead
*/ */
teleportId: { 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} * @type {typeof import('@unhead/schema').HtmlAttributes}
*/ */
teleportAttrs: { teleportAttrs: {
$resolve: async (val: undefined | null | Record<string, unknown>, get) => { $resolve: async (val, get) => {
const teleportId = await get('app.teleportId') const teleportId = await get('app.teleportId')
return defu(val, { return {
id: teleportId === false ? undefined : (teleportId || 'teleports'), id: teleportId === false ? undefined : (teleportId || 'teleports'),
}) ...typeof val === 'object' ? val : {},
}
}, },
}, },
@ -267,12 +306,12 @@ export default defineUntypedSchema({
* Customize Nuxt SpaLoader element tag. * Customize Nuxt SpaLoader element tag.
*/ */
spaLoaderTag: { spaLoaderTag: {
$resolve: val => val || 'div', $resolve: val => val && typeof val === 'string' ? val : 'div',
}, },
/** /**
* Customize Nuxt Nuxt SpaLoader element attributes. * Customize Nuxt Nuxt SpaLoader element attributes.
* @type {typeof import('@unhead/schema').HtmlAttributes} * @type {Partial<typeof import('@unhead/schema').HtmlAttributes>}
*/ */
spaLoaderAttrs: { spaLoaderAttrs: {
id: '__nuxt-loader', id: '__nuxt-loader',
@ -332,10 +371,18 @@ export default defineUntypedSchema({
* } * }
* </style> * </style>
* ``` * ```
* @type {string | boolean} * @type {string | boolean | undefined | null}
*/ */
spaLoadingTemplate: { 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 null
},
}, },
/** /**
@ -386,7 +433,21 @@ export default defineUntypedSchema({
* @type {string[]} * @type {string[]}
*/ */
css: { 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} * @type {typeof import('@unhead/schema').RenderSSRHeadOptions}
*/ */
renderSSRHeadOptions: { renderSSRHeadOptions: {
$resolve: async (val: Record<string, unknown> | undefined, get) => { $resolve: async (val, get) => {
const isV4 = ((await get('future') as Record<string, unknown>).compatibilityVersion === 4) const isV4 = (await get('future')).compatibilityVersion === 4
return defu(val, { return {
...typeof val === 'object' ? val : {},
omitLineBreaks: isV4, omitLineBreaks: isV4,
}) }
}, },
}, },
}, },

View File

@ -1,25 +1,35 @@
import { defineUntypedSchema } from 'untyped'
import { defu } from 'defu' import { defu } from 'defu'
import { join } from 'pathe' import { join } from 'pathe'
import { isTest } from 'std-env' import { isTest } from 'std-env'
import { consola } from 'consola' 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. * 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> }} * @type {'vite' | 'webpack' | 'rspack' | { bundle: (nuxt: typeof import('../src/types/nuxt').Nuxt) => Promise<void> }}
*/ */
builder: { builder: {
$resolve: async (val: 'vite' | 'webpack' | 'rspack' | { bundle: (nuxt: unknown) => Promise<void> } | undefined = 'vite', get) => { $resolve: async (val, get) => {
if (typeof val === 'object') { if (val && typeof val === 'object' && 'bundle' in val) {
return val return val as { bundle: (nuxt: Nuxt) => Promise<void> }
} }
const map: Record<string, string> = { const map = {
rspack: '@nuxt/rspack-builder', rspack: '@nuxt/rspack-builder',
vite: '@nuxt/vite-builder', vite: '@nuxt/vite-builder',
webpack: '@nuxt/webpack-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' }} * @type {boolean | { server?: boolean | 'hidden', client?: boolean | 'hidden' }}
*/ */
sourcemap: { sourcemap: {
$resolve: async (val: boolean | { server?: boolean | 'hidden', client?: boolean | 'hidden' } | undefined, get) => { $resolve: async (val, get) => {
if (typeof val === 'boolean') { if (typeof val === 'boolean') {
return { server: val, client: val } return { server: val, client: val }
} }
return defu(val, { return {
server: true, server: true,
client: await get('dev'), client: await get('dev'),
}) ...typeof val === 'object' ? val : {},
}
}, },
}, },
@ -56,11 +67,11 @@ export default defineUntypedSchema({
* @type {'silent' | 'info' | 'verbose'} * @type {'silent' | 'info' | 'verbose'}
*/ */
logLevel: { logLevel: {
$resolve: (val: string | undefined) => { $resolve: (val) => {
if (val && !['silent', 'info', 'verbose'].includes(val)) { if (val && typeof val === 'string' && !['silent', 'info', 'verbose'].includes(val)) {
consola.warn(`Invalid \`logLevel\` option: \`${val}\`. Must be one of: \`silent\`, \`info\`, \`verbose\`.`) 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)>} * @type {Array<string | RegExp | ((ctx: { isClient?: boolean; isServer?: boolean; isDev: boolean }) => string | RegExp | false)>}
*/ */
transpile: { 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' * 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: { analyze: {
$resolve: async (val: boolean | { enabled?: boolean } | Record<string, unknown>, get) => { $resolve: async (val, get) => {
const [rootDir, analyzeDir] = await Promise.all([get('rootDir'), get('analyzeDir')]) as [string, string] const [rootDir, analyzeDir] = await Promise.all([get('rootDir'), get('analyzeDir')])
return defu(typeof val === 'boolean' ? { enabled: val } : val, { return {
template: 'treemap', template: 'treemap',
projectRoot: rootDir, projectRoot: rootDir,
filename: join(analyzeDir, '{name}.html'), 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 }>} * @type {Array<{ name: string, source?: string | RegExp, argumentLength: number }>}
*/ */
keyedComposables: { keyedComposables: {
$resolve: (val: Array<{ name: string, argumentLength: string }> | undefined) => [ $resolve: val => [
{ name: 'callOnce', argumentLength: 3 }, { name: 'callOnce', argumentLength: 3 },
{ name: 'defineNuxtComponent', argumentLength: 2 }, { name: 'defineNuxtComponent', argumentLength: 2 },
{ name: 'useState', argumentLength: 2 }, { name: 'useState', argumentLength: 2 },
@ -147,7 +172,7 @@ export default defineUntypedSchema({
{ name: 'useAsyncData', argumentLength: 3 }, { name: 'useAsyncData', argumentLength: 3 },
{ name: 'useLazyAsyncData', argumentLength: 3 }, { name: 'useLazyAsyncData', argumentLength: 3 },
{ name: 'useLazyFetch', argumentLength: 3 }, { name: 'useLazyFetch', argumentLength: 3 },
...val || [], ...Array.isArray(val) ? val : [],
].filter(Boolean), ].filter(Boolean),
}, },

View File

@ -1,16 +1,16 @@
import { existsSync } from 'node:fs' import { existsSync } from 'node:fs'
import { readdir } from 'node:fs/promises' import { readdir } from 'node:fs/promises'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { defineUntypedSchema } from 'untyped'
import { basename, join, relative, resolve } from 'pathe' import { basename, join, relative, resolve } from 'pathe'
import { isDebug, isDevelopment, isTest } from 'std-env' import { isDebug, isDevelopment, isTest } from 'std-env'
import { defu } from 'defu' import { defu } from 'defu'
import { findWorkspaceDir } from 'pkg-types' import { findWorkspaceDir } from 'pkg-types'
import type { RuntimeConfig } from '../types/config'
import type { NuxtDebugOptions } from '../types/debug' 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. * Extend project from multiple local or remote sources.
* *
@ -21,7 +21,7 @@ export default defineUntypedSchema({
* @see [`giget` documentation](https://github.com/unjs/giget) * @see [`giget` documentation](https://github.com/unjs/giget)
* @type {string | [string, typeof import('c12').SourceOptions?] | (string | [string, typeof import('c12').SourceOptions?])[]} * @type {string | [string, typeof import('c12').SourceOptions?] | (string | [string, typeof import('c12').SourceOptions?])[]}
*/ */
extends: null, extends: undefined,
/** /**
* Specify a compatibility date for your app. * 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. * You can use `github:`, `gitlab:`, `bitbucket:` or `https://` to extend from a remote git repository.
* @type {string} * @type {string}
*/ */
theme: null, theme: undefined,
/** /**
* Define the root directory of your application. * Define the root directory of your application.
@ -67,9 +67,9 @@ export default defineUntypedSchema({
* It is normally not needed to configure this option. * It is normally not needed to configure this option.
*/ */
workspaceDir: { workspaceDir: {
$resolve: async (val: string | undefined, get): Promise<string> => { $resolve: async (val, get) => {
const rootDir = await get('rootDir') as string const rootDir = await get('rootDir')
return val ? resolve(rootDir, val) : await findWorkspaceDir(rootDir).catch(() => rootDir) return val && typeof val === 'string' ? resolve(rootDir, val) : await findWorkspaceDir(rootDir).catch(() => rootDir)
}, },
}, },
@ -105,14 +105,14 @@ export default defineUntypedSchema({
* ``` * ```
*/ */
srcDir: { srcDir: {
$resolve: async (val: string | undefined, get): Promise<string> => { $resolve: async (val, get) => {
if (val) { if (val && typeof val === 'string') {
return resolve(await get('rootDir') as string, val) return resolve(await get('rootDir'), val)
} }
const [rootDir, isV4] = await Promise.all([ const [rootDir, isV4] = await Promise.all([
get('rootDir') as Promise<string>, get('rootDir'),
(get('future') as Promise<Record<string, unknown>>).then(r => r.compatibilityVersion === 4), get('future').then(r => r.compatibilityVersion === 4),
]) ])
if (!isV4) { if (!isV4) {
@ -138,7 +138,7 @@ export default defineUntypedSchema({
} }
} }
const keys = ['assets', 'layouts', 'middleware', 'pages', 'plugins'] as const 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) { for (const dir of dirs) {
if (existsSync(resolve(rootDir, dir))) { if (existsSync(resolve(rootDir, dir))) {
return rootDir return rootDir
@ -157,13 +157,13 @@ export default defineUntypedSchema({
* *
*/ */
serverDir: { serverDir: {
$resolve: async (val: string | undefined, get): Promise<string> => { $resolve: async (val, get) => {
if (val) { if (val && typeof val === 'string') {
const rootDir = await get('rootDir') as string const rootDir = await get('rootDir')
return resolve(rootDir, val) return resolve(rootDir, val)
} }
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4 const isV4 = (await get('future')).compatibilityVersion === 4
return join(isV4 ? await get('rootDir') as string : await get('srcDir') as string, 'server') return join(isV4 ? await get('rootDir') : await get('srcDir'), 'server')
}, },
}, },
@ -180,9 +180,9 @@ export default defineUntypedSchema({
* ``` * ```
*/ */
buildDir: { buildDir: {
$resolve: async (val: string | undefined, get) => { $resolve: async (val, get) => {
const rootDir = await get('rootDir') as string const rootDir = await get('rootDir')
return resolve(rootDir, val ?? '.nuxt') return resolve(rootDir, val && typeof val === 'string' ? val : '.nuxt')
}, },
}, },
@ -192,14 +192,14 @@ export default defineUntypedSchema({
* Defaults to `nuxt-app`. * Defaults to `nuxt-app`.
*/ */
appId: { 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. * A unique identifier matching the build. This may contain the hash of the current state of the project.
*/ */
buildId: { buildId: {
$resolve: async (val: string | undefined, get): Promise<string> => { $resolve: async (val, get): Promise<string> => {
if (typeof val === 'string') { return val } if (typeof val === 'string') { return val }
const [isDev, isTest] = await Promise.all([get('dev') as Promise<boolean>, get('test') as Promise<boolean>]) const [isDev, isTest] = await Promise.all([get('dev') as Promise<boolean>, get('test') as Promise<boolean>])
@ -223,12 +223,17 @@ export default defineUntypedSchema({
*/ */
modulesDir: { modulesDir: {
$default: ['node_modules'], $default: ['node_modules'],
$resolve: async (val: string[] | undefined, get): Promise<string[]> => { $resolve: async (val, get) => {
const rootDir = await get('rootDir') as string const rootDir = await get('rootDir')
return [...new Set([ const modulesDir = new Set<string>([resolve(rootDir, 'node_modules')])
...(val || []).map((dir: string) => resolve(rootDir, dir)), if (Array.isArray(val)) {
resolve(rootDir, 'node_modules'), for (const dir of val) {
])] if (dir && typeof dir === 'string') {
modulesDir.add(resolve(rootDir, dir))
}
}
}
return [...modulesDir]
}, },
}, },
@ -238,9 +243,9 @@ export default defineUntypedSchema({
* If a relative path is specified, it will be relative to your `rootDir`. * If a relative path is specified, it will be relative to your `rootDir`.
*/ */
analyzeDir: { analyzeDir: {
$resolve: async (val: string | undefined, get): Promise<string> => val $resolve: async (val, get) => val && typeof val === 'string'
? resolve(await get('rootDir') as string, val) ? resolve(await get('rootDir'), val)
: resolve(await get('buildDir') as string, 'analyze'), : resolve(await get('buildDir'), 'analyze'),
}, },
/** /**
@ -249,14 +254,14 @@ export default defineUntypedSchema({
* Normally, you should not need to set this. * Normally, you should not need to set this.
*/ */
dev: { dev: {
$resolve: val => val ?? Boolean(isDevelopment), $resolve: val => typeof val === 'boolean' ? val : Boolean(isDevelopment),
}, },
/** /**
* Whether your app is being unit tested. * Whether your app is being unit tested.
*/ */
test: { test: {
$resolve: val => val ?? Boolean(isTest), $resolve: val => typeof val === 'boolean' ? val : Boolean(isTest),
}, },
/** /**
@ -270,11 +275,8 @@ export default defineUntypedSchema({
* @type {boolean | (typeof import('../src/types/debug').NuxtDebugOptions) | undefined} * @type {boolean | (typeof import('../src/types/debug').NuxtDebugOptions) | undefined}
*/ */
debug: { debug: {
$resolve: (val: boolean | NuxtDebugOptions | undefined) => { $resolve: (val) => {
val ??= isDebug val ??= isDebug
if (val === false) {
return val
}
if (val === true) { if (val === true) {
return { return {
templates: true, templates: true,
@ -289,7 +291,10 @@ export default defineUntypedSchema({
hydration: true, hydration: true,
} satisfies Required<NuxtDebugOptions> } satisfies Required<NuxtDebugOptions>
} }
if (val && typeof val === 'object') {
return val return val
}
return false
}, },
}, },
@ -298,7 +303,7 @@ export default defineUntypedSchema({
* If set to `false` generated pages will have no content. * If set to `false` generated pages will have no content.
*/ */
ssr: { ssr: {
$resolve: val => val ?? true, $resolve: val => typeof val === 'boolean' ? val : true,
}, },
/** /**
@ -327,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)[]} * @type {(typeof import('../src/types/module').NuxtModule<any> | string | [typeof import('../src/types/module').NuxtModule | string, Record<string, any>] | undefined | null | false)[]}
*/ */
modules: { 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
},
}, },
/** /**
@ -337,13 +355,13 @@ export default defineUntypedSchema({
*/ */
dir: { dir: {
app: { app: {
$resolve: async (val: string | undefined, get) => { $resolve: async (val, get) => {
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4 const isV4 = (await get('future')).compatibilityVersion === 4
if (isV4) { if (isV4) {
const [srcDir, rootDir] = await Promise.all([get('srcDir') as Promise<string>, get('rootDir') as Promise<string>]) const [srcDir, rootDir] = await Promise.all([get('srcDir'), get('rootDir')])
return resolve(await get('srcDir') as string, val || (srcDir === rootDir ? 'app' : '.')) return resolve(await get('srcDir'), val && typeof val === 'string' ? val : (srcDir === rootDir ? 'app' : '.'))
} }
return val || 'app' return val && typeof val === 'string' ? val : 'app'
}, },
}, },
/** /**
@ -365,12 +383,12 @@ 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: {
$resolve: async (val: string | undefined, get) => { $resolve: async (val, get) => {
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4 const isV4 = (await get('future')).compatibilityVersion === 4
if (isV4) { 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'
}, },
}, },
@ -394,18 +412,25 @@ 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: string | undefined, get) => { $resolve: async (val, get) => {
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4 const isV4 = (await get('future')).compatibilityVersion === 4
if (isV4) { 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: { static: {
// @ts-expect-error schema has invalid types
$schema: { deprecated: 'use `dir.public` option instead' }, $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'
},
}, },
}, },
@ -413,7 +438,17 @@ export default defineUntypedSchema({
* The extensions that should be resolved by the Nuxt resolver. * The extensions that should be resolved by the Nuxt resolver.
*/ */
extensions: { 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
},
}, },
/** /**
@ -457,8 +492,8 @@ export default defineUntypedSchema({
* @type {Record<string, string>} * @type {Record<string, string>}
*/ */
alias: { alias: {
$resolve: async (val: Record<string, string>, get): Promise<Record<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')]) as [string, string, string, string, 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')])
return { return {
'~': srcDir, '~': srcDir,
'@': srcDir, '@': srcDir,
@ -469,7 +504,7 @@ export default defineUntypedSchema({
[basename(publicDir)]: resolve(srcDir, publicDir), [basename(publicDir)]: resolve(srcDir, publicDir),
'#build': buildDir, '#build': buildDir,
'#internal/nuxt/paths': resolve(buildDir, 'paths.mjs'), '#internal/nuxt/paths': resolve(buildDir, 'paths.mjs'),
...val, ...typeof val === 'object' ? val : {},
} }
}, },
}, },
@ -494,7 +529,7 @@ export default defineUntypedSchema({
* By default, the `ignorePrefix` is set to '-', ignoring any files starting with '-'. * By default, the `ignorePrefix` is set to '-', ignoring any files starting with '-'.
*/ */
ignorePrefix: { ignorePrefix: {
$resolve: val => val ?? '-', $resolve: val => val && typeof val === 'string' ? val : '-',
}, },
/** /**
@ -502,18 +537,27 @@ export default defineUntypedSchema({
* inside the `ignore` array will be ignored in building. * inside the `ignore` array will be ignored in building.
*/ */
ignore: { ignore: {
$resolve: async (val: string[] | undefined, get): Promise<string[]> => { $resolve: async (val, get): Promise<string[]> => {
const [rootDir, ignorePrefix, analyzeDir, buildDir] = await Promise.all([get('rootDir'), get('ignorePrefix'), get('analyzeDir'), get('buildDir')]) as [string, string, string, string] const [rootDir, ignorePrefix, analyzeDir, buildDir] = await Promise.all([get('rootDir'), get('ignorePrefix'), get('analyzeDir'), get('buildDir')])
return [ const ignore = new Set<string>([
'**/*.stories.{js,cts,mts,ts,jsx,tsx}', // ignore storybook files '**/*.stories.{js,cts,mts,ts,jsx,tsx}', // ignore storybook files
'**/*.{spec,test}.{js,cts,mts,ts,jsx,tsx}', // ignore tests '**/*.{spec,test}.{js,cts,mts,ts,jsx,tsx}', // ignore tests
'**/*.d.{cts,mts,ts}', // ignore type declarations '**/*.d.{cts,mts,ts}', // ignore type declarations
'**/.{pnpm-store,vercel,netlify,output,git,cache,data}', '**/.{pnpm-store,vercel,netlify,output,git,cache,data}',
relative(rootDir, analyzeDir), relative(rootDir, analyzeDir),
relative(rootDir, buildDir), relative(rootDir, buildDir),
ignorePrefix && `**/${ignorePrefix}*.*`, ])
...val || [], if (ignorePrefix) {
].filter(Boolean) ignore.add(`**/${ignorePrefix}*.*`)
}
if (Array.isArray(val)) {
for (const pattern in val) {
if (pattern) {
ignore.add(pattern)
}
}
}
return [...ignore]
}, },
}, },
@ -526,8 +570,11 @@ export default defineUntypedSchema({
* @type {Array<string | RegExp>} * @type {Array<string | RegExp>}
*/ */
watch: { watch: {
$resolve: (val: Array<unknown> | undefined) => { $resolve: (val) => {
return (val || []).filter((b: unknown) => typeof b === 'string' || b instanceof RegExp) if (Array.isArray(val)) {
return val.filter((b: unknown) => typeof b === 'string' || b instanceof RegExp)
}
return []
}, },
}, },
@ -583,7 +630,7 @@ export default defineUntypedSchema({
* ``` * ```
* @type {typeof import('../src/types/hooks').NuxtHooks} * @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. * Runtime config allows passing dynamic config and environment variables to the Nuxt app context.
@ -611,8 +658,9 @@ export default defineUntypedSchema({
* @type {typeof import('../src/types/config').RuntimeConfig} * @type {typeof import('../src/types/config').RuntimeConfig}
*/ */
runtimeConfig: { runtimeConfig: {
$resolve: async (val: RuntimeConfig, get): Promise<Record<string, unknown>> => { $resolve: async (_val, get) => {
const [app, buildId] = await Promise.all([get('app') as Promise<Record<string, string>>, get('buildId') as Promise<string>]) const val = _val && typeof _val === 'object' ? _val : {}
const [app, buildId] = await Promise.all([get('app'), get('buildId')])
provideFallbackValues(val) provideFallbackValues(val)
return defu(val, { return defu(val, {
public: {}, 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' import { template as loadingTemplate } from '../../../ui-templates/dist/templates/loading'
export default defineUntypedSchema({ export default defineResolvers({
devServer: { devServer: {
/** /**
* Whether to enable HTTPS. * Whether to enable HTTPS.
@ -21,9 +21,12 @@ export default defineUntypedSchema({
https: false, https: false,
/** Dev server listening port */ /** 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, host: process.env.NUXT_HOST || process.env.NITRO_HOST || process.env.HOST || undefined,
/** /**

View File

@ -0,0 +1,32 @@
import { defu } from 'defu'
import type { TransformOptions } from 'esbuild'
import { defineResolvers } from '../utils/definition'
export default defineResolvers({
esbuild: {
/**
* Configure shared esbuild options used within Nuxt and passed to other builders, such as Vite or Webpack.
* @type {import('esbuild').TransformOptions}
*/
options: {
jsxFactory: 'h',
jsxFragment: 'Fragment',
tsconfigRaw: {
$resolve: async (_val, get) => {
const val: NonNullable<Exclude<TransformOptions['tsconfigRaw'], string>> = typeof _val === 'string' ? JSON.parse(_val) : (_val && typeof _val === 'object' ? _val : {})
const useDecorators = await get('experimental').then(r => r?.decorators === true)
if (!useDecorators) {
return val
}
return defu(val, {
compilerOptions: {
useDefineForClassFields: false,
experimentalDecorators: false,
},
} satisfies TransformOptions['tsconfigRaw'])
},
},
},
},
})

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 * `future` is for early opting-in to new features that will become default in a future
* (possibly major) version of the framework. * (possibly major) version of the framework.
@ -30,10 +30,10 @@ export default defineUntypedSchema({
*/ */
typescriptBundlerResolution: { typescriptBundlerResolution: {
async $resolve (val, get) { async $resolve (val, get) {
// TODO: remove in v3.10 // @ts-expect-error TODO: remove in v3.10
val = val ?? await (get('experimental') as Promise<Record<string, any>>).then(e => e?.typescriptBundlerResolution) val = typeof val === 'boolean' ? val : await (get('experimental')).then(e => e?.typescriptBundlerResolution as string | undefined)
if (typeof val === 'boolean') { return val } 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) { if (setting) {
return setting.toLowerCase() === 'bundler' return setting.toLowerCase() === 'bundler'
} }
@ -53,14 +53,22 @@ export default defineUntypedSchema({
* @type {boolean | ((id?: string) => boolean)} * @type {boolean | ((id?: string) => boolean)}
*/ */
inlineStyles: { inlineStyles: {
async $resolve (val, get) { async $resolve (_val, get) {
// TODO: remove in v3.10 const val = typeof _val === 'boolean' || typeof _val === 'function'
val = val ?? await (get('experimental') as Promise<Record<string, any>>).then((e: Record<string, any>) => e?.inlineSSRStyles) ? _val
if (val === false || (await get('dev')) || (await get('ssr')) === false || (await get('builder')) === '@nuxt/webpack-builder') { // @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 return false
} }
// Enabled by default for vite prod with ssr (for vue components) // 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)
}, },
}, },
@ -73,7 +81,9 @@ export default defineUntypedSchema({
*/ */
devLogs: { devLogs: {
async $resolve (val, get) { 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')]) const [isDev, isTest] = await Promise.all([get('dev'), get('test')])
return isDev && !isTest return isDev && !isTest
}, },
@ -85,17 +95,25 @@ export default defineUntypedSchema({
*/ */
noScripts: { noScripts: {
async $resolve (val, get) { async $resolve (val, get) {
// TODO: remove in v3.10 return typeof val === 'boolean'
return val ?? await (get('experimental') as Promise<Record<string, any>>).then((e: Record<string, any>) => e?.noScripts) ?? false ? val
// @ts-expect-error TODO: legacy property - remove in v3.10
: (await (get('experimental')).then(e => e?.noScripts as boolean | undefined) ?? false)
}, },
}, },
}, },
experimental: { experimental: {
/**
* Enable to use experimental decorators in Nuxt and Nitro.
*
* @see https://github.com/tc39/proposal-decorators
*/
decorators: false,
/** /**
* Set to true to generate an async entry point for the Vue bundle (for module federation support). * Set to true to generate an async entry point for the Vue bundle (for module federation support).
*/ */
asyncEntry: { asyncEntry: {
$resolve: val => val ?? false, $resolve: val => typeof val === 'boolean' ? val : false,
}, },
// TODO: Remove when nitro has support for mocking traced dependencies // TODO: Remove when nitro has support for mocking traced dependencies
@ -135,7 +153,17 @@ export default defineUntypedSchema({
if (val === 'reload') { if (val === 'reload') {
return 'automatic' 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'
}, },
}, },
@ -255,14 +283,16 @@ export default defineUntypedSchema({
*/ */
watcher: { watcher: {
$resolve: async (val, get) => { $resolve: async (val, get) => {
if (val) { const validOptions = ['chokidar', 'parcel', 'chokidar-granular'] as const
return val 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) { if (srcDir === rootDir) {
return 'chokidar-granular' return 'chokidar-granular' as const
} }
return 'chokidar' return 'chokidar' as const
}, },
}, },
@ -304,7 +334,7 @@ export default defineUntypedSchema({
*/ */
scanPageMeta: { scanPageMeta: {
async $resolve (val, get) { 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)
}, },
}, },
@ -343,7 +373,7 @@ export default defineUntypedSchema({
*/ */
sharedPrerenderData: { sharedPrerenderData: {
async $resolve (val, get) { 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)
}, },
}, },
@ -414,7 +444,7 @@ export default defineUntypedSchema({
*/ */
normalizeComponentNames: { normalizeComponentNames: {
$resolve: async (val, get) => { $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)
}, },
}, },
@ -425,7 +455,9 @@ export default defineUntypedSchema({
*/ */
spaLoadingTemplateLocation: { spaLoadingTemplateLocation: {
$resolve: async (val, get) => { $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')
}, },
}, },
@ -435,7 +467,7 @@ export default defineUntypedSchema({
* @see [the Chrome DevTools extensibility API](https://developer.chrome.com/docs/devtools/performance/extension#tracks) * @see [the Chrome DevTools extensibility API](https://developer.chrome.com/docs/devtools/performance/extension#tracks)
*/ */
browserDevtoolsTiming: { browserDevtoolsTiming: {
$resolve: async (val, get) => val ?? await get('dev'), $resolve: async (val, get) => typeof val === 'boolean' ? val : await get('dev'),
}, },
/** /**
@ -443,7 +475,7 @@ export default defineUntypedSchema({
*/ */
debugModuleMutation: { debugModuleMutation: {
$resolve: async (val, get) => { $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: { generate: {
/** /**
* The routes to generate. * The routes to generate.

View File

@ -1,8 +1,11 @@
import type { ResolvableConfigSchema } from '../utils/definition'
import adhoc from './adhoc' import adhoc from './adhoc'
import app from './app' import app from './app'
import build from './build' import build from './build'
import common from './common' import common from './common'
import dev from './dev' import dev from './dev'
import esbuild from './esbuild'
import experimental from './experimental' import experimental from './experimental'
import generate from './generate' import generate from './generate'
import internal from './internal' import internal from './internal'
@ -26,6 +29,7 @@ export default {
...postcss, ...postcss,
...router, ...router,
...typescript, ...typescript,
...esbuild,
...vite, ...vite,
...webpack, ...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 */ /** @private */
_majorVersion: 4, _majorVersion: 4,
/** @private */ /** @private */
@ -25,7 +25,7 @@ export default defineUntypedSchema({
appDir: '', appDir: '',
/** /**
* @private * @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: [], _installedModules: [],
/** @private */ /** @private */

View File

@ -1,7 +1,6 @@
import { defineUntypedSchema } from 'untyped' import { defineResolvers } from '../utils/definition'
import type { RuntimeConfig } from '../types/config'
export default defineUntypedSchema({ export default defineResolvers({
/** /**
* Configuration for Nitro. * Configuration for Nitro.
* @see [Nitro configuration docs](https://nitro.unjs.io/config/) * @see [Nitro configuration docs](https://nitro.unjs.io/config/)
@ -9,8 +8,8 @@ export default defineUntypedSchema({
*/ */
nitro: { nitro: {
runtimeConfig: { runtimeConfig: {
$resolve: async (val: Record<string, any> | undefined, get) => { $resolve: async (val, get) => {
const runtimeConfig = await get('runtimeConfig') as RuntimeConfig const runtimeConfig = await get('runtimeConfig')
return { return {
...runtimeConfig, ...runtimeConfig,
app: { app: {
@ -27,10 +26,12 @@ export default defineUntypedSchema({
}, },
}, },
routeRules: { routeRules: {
$resolve: async (val: Record<string, any> | undefined, get) => ({ $resolve: async (val, get) => {
...await get('routeRules') as Record<string, any>, return {
...val, ...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 ensureItemIsLast = (item: string) => (arr: string[]) => {
const index = arr.indexOf(item) const index = arr.indexOf(item)
@ -17,7 +17,7 @@ const orderPresets = {
}, },
} }
export default defineUntypedSchema({ export default defineResolvers({
postcss: { postcss: {
/** /**
* A strategy for ordering PostCSS plugins. * A strategy for ordering PostCSS plugins.
@ -25,14 +25,20 @@ export default defineUntypedSchema({
* @type {'cssnanoLast' | 'autoprefixerLast' | 'autoprefixerAndCssnanoLast' | string[] | ((names: string[]) => string[])} * @type {'cssnanoLast' | 'autoprefixerLast' | 'autoprefixerAndCssnanoLast' | string[] | ((names: string[]) => string[])}
*/ */
order: { order: {
$resolve: (val: string | string[] | ((plugins: string[]) => string[])): string[] | ((plugins: string[]) => string[]) => { $resolve: (val) => {
if (typeof val === 'string') { if (typeof val === 'string') {
if (!(val in orderPresets)) { if (!(val in orderPresets)) {
throw new Error(`[nuxt] Unknown PostCSS order preset: ${val}`) throw new Error(`[nuxt] Unknown PostCSS order preset: ${val}`)
} }
return orderPresets[val as keyof typeof orderPresets] 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: { router: {
/** /**
* Additional router options passed to `vue-router`. On top of the options for `vue-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. * Configuration for Nuxt's TypeScript integration.
* *
@ -20,10 +20,20 @@ export default defineUntypedSchema({
* builder environment types (with `false`) to handle this fully yourself, or opt for a 'shared' option. * builder environment types (with `false`) to handle this fully yourself, or opt for a 'shared' option.
* *
* The 'shared' option is advised for module authors, who will want to support multiple possible builders. * The 'shared' option is advised for module authors, who will want to support multiple possible builders.
* @type {'vite' | 'webpack' | 'rspack' | 'shared' | false | undefined} * @type {'vite' | 'webpack' | 'rspack' | 'shared' | false | undefined | null}
*/ */
builder: { 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 null
},
}, },
/** /**

View File

@ -1,10 +1,10 @@
import { consola } from 'consola' import { consola } from 'consola'
import defu from 'defu'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import { isTest } from 'std-env' import { isTest } from 'std-env'
import { defineUntypedSchema } from 'untyped' import { defineResolvers } from '../utils/definition'
import type { NuxtDebugOptions } from '../types/debug'
export default defineUntypedSchema({ export default defineResolvers({
/** /**
* Configuration that will be passed directly to Vite. * Configuration that will be passed directly to Vite.
* *
@ -14,22 +14,21 @@ export default defineUntypedSchema({
*/ */
vite: { vite: {
root: { root: {
$resolve: async (val, get) => val ?? (await get('srcDir')), $resolve: async (val, get) => typeof val === 'string' ? val : (await get('srcDir')),
}, },
mode: { 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: { define: {
$resolve: async (val: Record<string, any> | undefined, get) => { $resolve: async (_val, get) => {
const [isDev, debug] = await Promise.all([get('dev'), get('debug')]) as [boolean, boolean | NuxtDebugOptions] const [isDev, isDebug] = await Promise.all([get('dev'), get('debug')])
return { 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, 'process.dev': isDev,
'import.meta.dev': isDev, 'import.meta.dev': isDev,
'process.test': isTest, 'process.test': isTest,
'import.meta.test': isTest, 'import.meta.test': isTest,
...val, ..._val && typeof _val === 'object' ? _val : {},
} }
}, },
}, },
@ -37,6 +36,7 @@ export default defineUntypedSchema({
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'], extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
}, },
publicDir: { publicDir: {
// @ts-expect-error this is missing from our `vite` types deliberately, so users do not configure it
$resolve: (val) => { $resolve: (val) => {
if (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`.') 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,81 +46,89 @@ export default defineUntypedSchema({
}, },
vue: { vue: {
isProduction: { isProduction: {
$resolve: async (val, get) => val ?? !(await get('dev')), $resolve: async (val, get) => typeof val === 'boolean' ? val : !(await get('dev')),
}, },
template: { template: {
compilerOptions: { compilerOptions: {
$resolve: async (val, get) => val ?? (await get('vue') as Record<string, any>).compilerOptions, $resolve: async (val, get) => val ?? (await get('vue')).compilerOptions,
}, },
transformAssetUrls: { transformAssetUrls: {
$resolve: async (val, get) => val ?? (await get('vue') as Record<string, any>).transformAssetUrls, $resolve: async (val, get) => val ?? (await get('vue')).transformAssetUrls,
}, },
}, },
script: { script: {
hoistStatic: { 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: { features: {
propsDestructure: { propsDestructure: {
$resolve: async (val, get) => { $resolve: async (val, get) => {
if (val !== undefined && val !== null) { if (typeof val === 'boolean') {
return val return val
} }
const vueOptions = await get('vue') as Record<string, any> || {} const vueOptions = await get('vue') || {}
return Boolean(vueOptions.script?.propsDestructure ?? vueOptions.propsDestructure) return Boolean(
// @ts-expect-error TODO: remove in future: supporting a legacy schema
vueOptions.script?.propsDestructure
?? vueOptions.propsDestructure,
)
}, },
}, },
}, },
}, },
vueJsx: { vueJsx: {
$resolve: async (val: Record<string, any>, get) => { $resolve: async (val, get) => {
return { return {
isCustomElement: (await get('vue') as Record<string, any>).compilerOptions?.isCustomElement, // TODO: investigate type divergence between types for @vue/compiler-core and @vue/babel-plugin-jsx
...val, isCustomElement: (await get('vue')).compilerOptions?.isCustomElement as undefined | ((tag: string) => boolean),
...typeof val === 'object' ? val : {},
} }
}, },
}, },
optimizeDeps: { optimizeDeps: {
esbuildOptions: {
$resolve: async (val, get) => defu(val && typeof val === 'object' ? val : {}, await get('esbuild.options')),
},
exclude: { exclude: {
$resolve: async (val: string[] | undefined, get) => [ $resolve: async (val, get) => [
...val || [], ...Array.isArray(val) ? val : [],
...(await get('build.transpile') as Array<string | RegExp | ((ctx: { isClient?: boolean, isServer?: boolean, isDev: boolean }) => string | RegExp | false)>).filter(i => typeof i === 'string'), ...(await get('build.transpile')).filter(i => typeof i === 'string'),
'vue-demi', 'vue-demi',
], ],
}, },
}, },
esbuild: { esbuild: {
jsxFactory: 'h', $resolve: async (val, get) => {
jsxFragment: 'Fragment', return defu(val && typeof val === 'object' ? val : {}, await get('esbuild.options'))
tsconfigRaw: '{}', },
}, },
clearScreen: true, clearScreen: true,
build: { build: {
assetsDir: { 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, emptyOutDir: false,
}, },
server: { server: {
fs: { fs: {
allow: { allow: {
$resolve: async (val: string[] | undefined, get) => { $resolve: async (val, 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] const [buildDir, srcDir, rootDir, workspaceDir, modulesDir] = await Promise.all([get('buildDir'), get('srcDir'), get('rootDir'), get('workspaceDir'), get('modulesDir')])
return [...new Set([ return [...new Set([
buildDir, buildDir,
srcDir, srcDir,
rootDir, rootDir,
workspaceDir, workspaceDir,
...(modulesDir), ...(modulesDir),
...val ?? [], ...Array.isArray(val) ? val : [],
])] ])]
}, },
}, },
}, },
}, },
cacheDir: { 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 { defu } from 'defu'
import { defineUntypedSchema } from 'untyped' import { defineResolvers } from '../utils/definition'
import type { VueLoaderOptions } from 'vue-loader'
export default defineUntypedSchema({ export default defineResolvers({
webpack: { webpack: {
/** /**
* Nuxt uses `webpack-bundle-analyzer` to visualize your bundles and how to optimize them. * 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} * @type {boolean | { enabled?: boolean } & typeof import('webpack-bundle-analyzer').BundleAnalyzerPlugin.Options}
*/ */
analyze: { analyze: {
$resolve: async (val: boolean | { enabled?: boolean } | Record<string, unknown>, get) => { $resolve: async (val, get) => {
const value = typeof val === 'boolean' ? { enabled: val } : val 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>) 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). * Enables CSS source map support (defaults to `true` in development).
*/ */
cssSourceMap: { 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) { for (const name of styleLoaders) {
const loader = loaders[name] const loader = loaders[name]
if (loader && loader.sourceMap === undefined) { 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 return loaders
@ -158,37 +160,34 @@ export default defineUntypedSchema({
* @type {Omit<typeof import('esbuild-loader')['LoaderOptions'], 'loader'>} * @type {Omit<typeof import('esbuild-loader')['LoaderOptions'], 'loader'>}
*/ */
esbuild: { esbuild: {
jsxFactory: 'h', $resolve: async (val, get) => {
jsxFragment: 'Fragment', return defu(val && typeof val === 'object' ? val : {}, await get('esbuild.options'))
tsconfigRaw: '{}', },
}, },
/** /**
* @see [`file-loader` Options](https://github.com/webpack-contrib/file-loader#options) * @see [`file-loader` Options](https://github.com/webpack-contrib/file-loader#options)
* @type {Omit<typeof import('file-loader')['Options'], 'name'>}
* @default * @default
* ```ts * ```ts
* { esModule: false } * { esModule: false }
* ``` * ```
*/ */
file: { esModule: false }, file: { esModule: false, limit: 1000 },
/** /**
* @see [`file-loader` Options](https://github.com/webpack-contrib/file-loader#options) * @see [`file-loader` Options](https://github.com/webpack-contrib/file-loader#options)
* @type {Omit<typeof import('file-loader')['Options'], 'name'>}
* @default * @default
* ```ts * ```ts
* { esModule: false, limit: 1000 } * { esModule: false }
* ``` * ```
*/ */
fontUrl: { esModule: false, limit: 1000 }, fontUrl: { esModule: false, limit: 1000 },
/** /**
* @see [`file-loader` Options](https://github.com/webpack-contrib/file-loader#options) * @see [`file-loader` Options](https://github.com/webpack-contrib/file-loader#options)
* @type {Omit<typeof import('file-loader')['Options'], 'name'>}
* @default * @default
* ```ts * ```ts
* { esModule: false, limit: 1000 } * { esModule: false }
* ``` * ```
*/ */
imgUrl: { esModule: false, limit: 1000 }, imgUrl: { esModule: false, limit: 1000 },
@ -205,26 +204,38 @@ export default defineUntypedSchema({
*/ */
vue: { vue: {
transformAssetUrls: { transformAssetUrls: {
$resolve: async (val, get) => (val ?? (await get('vue.transformAssetUrls'))) as VueLoaderOptions['transformAssetUrls'], $resolve: async (val, get) => (val ?? (await get('vue.transformAssetUrls'))),
}, },
compilerOptions: { compilerOptions: {
$resolve: async (val, get) => (val ?? (await get('vue.compilerOptions'))) as VueLoaderOptions['compilerOptions'], $resolve: async (val, get) => (val ?? (await get('vue.compilerOptions'))),
}, },
propsDestructure: { propsDestructure: {
$resolve: async (val, get) => Boolean(val ?? await get('vue.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: { css: {
importLoaders: 0, importLoaders: 0,
/**
* @type {boolean | { filter: (url: string, resourcePath: string) => boolean }}
*/
url: { url: {
filter: (url: string, _resourcePath: string) => url[0] !== '/', filter: (url: string, _resourcePath: string) => url[0] !== '/',
}, },
esModule: false, esModule: false,
}, },
/**
* See [css-loader](https://github.com/webpack-contrib/css-loader) for available options.
*/
cssModules: { cssModules: {
importLoaders: 0, importLoaders: 0,
/**
* @type {boolean | { filter: (url: string, resourcePath: string) => boolean }}
*/
url: { url: {
filter: (url: string, _resourcePath: string) => url[0] !== '/', 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) * @see [`sass-loader` Options](https://github.com/webpack-contrib/sass-loader#options)
* @type {typeof import('sass-loader')['Options']}
* @default * @default
* ```ts * ```ts
* { * {
@ -259,7 +269,6 @@ export default defineUntypedSchema({
/** /**
* @see [`sass-loader` Options](https://github.com/webpack-contrib/sass-loader#options) * @see [`sass-loader` Options](https://github.com/webpack-contrib/sass-loader#options)
* @type {typeof import('sass-loader')['Options']}
*/ */
scss: {}, 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>} * @type {false | typeof import('css-minimizer-webpack-plugin').BasePluginOptions & typeof import('css-minimizer-webpack-plugin').DefinedDefaultMinimizerAndOptions<any>}
*/ */
optimizeCSS: { 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: { optimization: {
runtimeChunk: 'single', runtimeChunk: 'single',
/** Set minimize to `false` to disable all minimizers. (It is disabled in development by default). */ /** 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. */ /** You can set minimizer to a customized array of plugins. */
minimizer: undefined, minimizer: undefined,
splitChunks: { splitChunks: {
@ -323,15 +341,12 @@ export default defineUntypedSchema({
/** /**
* Customize PostCSS Loader. * Customize PostCSS Loader.
* same options as [`postcss-loader` options](https://github.com/webpack-contrib/postcss-loader#options) * 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: { postcss: {
postcssOptions: { postcssOptions: {
config: {
$resolve: async (val, get) => val ?? (await get('postcss.config')),
},
plugins: { 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

@ -32,4 +32,10 @@ export interface ImportsOptions extends UnimportOptions {
exclude?: RegExp[] exclude?: RegExp[]
include?: RegExp[] include?: RegExp[]
} }
/**
* Add polyfills for setInterval, requestIdleCallback, and others
* @default true
*/
polyfills?: boolean
} }

View File

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

View File

@ -83,6 +83,7 @@ export interface NuxtApp {
export interface Nuxt { export interface Nuxt {
// Private fields. // Private fields.
__name: string
_version: string _version: string
_ignore?: Ignore _ignore?: Ignore
_dependencies?: Set<string> _dependencies?: Set<string>
@ -96,6 +97,7 @@ export interface Nuxt {
hook: Nuxt['hooks']['hook'] hook: Nuxt['hooks']['hook']
callHook: Nuxt['hooks']['callHook'] callHook: Nuxt['hooks']['callHook']
addHooks: Nuxt['hooks']['addHooks'] addHooks: Nuxt['hooks']['addHooks']
runWithContext: <T extends (...args: any[]) => any>(fn: T) => ReturnType<T>
ready: () => Promise<void> ready: () => Promise<void>
close: () => Promise<void> close: () => Promise<void>

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

@ -24,7 +24,7 @@
"jiti": "2.4.2", "jiti": "2.4.2",
"knitwork": "1.2.0", "knitwork": "1.2.0",
"pathe": "2.0.2", "pathe": "2.0.2",
"prettier": "3.4.2", "prettier": "3.5.0",
"scule": "1.3.0", "scule": "1.3.0",
"svgo": "3.3.2", "svgo": "3.3.2",
"tinyexec": "0.3.2", "tinyexec": "0.3.2",

View File

@ -1243,8 +1243,8 @@ exports[`template > produces correct output for welcome template 1`] = `
--un-ring-shadow: var(--un-ring-inset) 0 0 0 --un-ring-shadow: var(--un-ring-inset) 0 0 0
calc(var(--un-ring-width) + var(--un-ring-offset-width)) calc(var(--un-ring-width) + var(--un-ring-offset-width))
var(--un-ring-color); var(--un-ring-color);
box-shadow: var(--un-ring-offset-shadow), var(--un-ring-shadow), box-shadow:
var(--un-shadow); var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);
} }
.transition-all { .transition-all {
transition-duration: 0.15s; transition-duration: 0.15s;

View File

@ -41,7 +41,6 @@
"defu": "^6.1.4", "defu": "^6.1.4",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"externality": "^1.0.2",
"get-port-please": "^3.1.2", "get-port-please": "^3.1.2",
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703", "h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"jiti": "^2.4.2", "jiti": "^2.4.2",
@ -50,12 +49,12 @@
"mlly": "^1.7.4", "mlly": "^1.7.4",
"pathe": "^2.0.2", "pathe": "^2.0.2",
"pkg-types": "^1.3.1", "pkg-types": "^1.3.1",
"postcss": "^8.5.1", "postcss": "^8.5.2",
"rollup-plugin-visualizer": "^5.14.0", "rollup-plugin-visualizer": "^5.14.0",
"std-env": "^3.8.0", "std-env": "^3.8.0",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unplugin": "^2.1.2", "unplugin": "^2.2.0",
"vite": "^6.1.0", "vite": "^6.1.0",
"vite-node": "^3.0.5", "vite-node": "^3.0.5",
"vite-plugin-checker": "^0.8.0", "vite-plugin-checker": "^0.8.0",
@ -65,6 +64,6 @@
"vue": "^3.3.4" "vue": "^3.3.4"
}, },
"engines": { "engines": {
"node": "^18.20.6 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -39,6 +39,9 @@ function createRunner () {
return new ViteNodeRunner({ return new ViteNodeRunner({
root: viteNodeOptions.root, // Equals to Nuxt `srcDir` root: viteNodeOptions.root, // Equals to Nuxt `srcDir`
base: viteNodeOptions.base, base: viteNodeOptions.base,
async resolveId (id, importer) {
return await viteNodeFetch('/resolve/' + encodeURIComponent(id) + (importer ? '?importer=' + encodeURIComponent(importer) : '')) ?? undefined
},
async fetchModule (id) { async fetchModule (id) {
id = id.replace(/\/\//g, '/') // TODO: fix in vite-node id = id.replace(/\/\//g, '/') // TODO: fix in vite-node
return await viteNodeFetch('/module/' + encodeURI(id)).catch((err) => { return await viteNodeFetch('/module/' + encodeURI(id)).catch((err) => {

View File

@ -1,36 +0,0 @@
import type { ExternalsOptions } from 'externality'
import { ExternalsDefaults, isExternal } from 'externality'
import type { ViteDevServer } from 'vite'
import escapeStringRegexp from 'escape-string-regexp'
import { withTrailingSlash } from 'ufo'
import type { Nuxt } from 'nuxt/schema'
import { resolve } from 'pathe'
import { toArray } from '.'
export function createIsExternal (viteServer: ViteDevServer, nuxt: Nuxt) {
const externalOpts: ExternalsOptions = {
inline: [
/virtual:/,
/\.ts$/,
...ExternalsDefaults.inline || [],
...(
viteServer.config.ssr.noExternal && viteServer.config.ssr.noExternal !== true
? toArray(viteServer.config.ssr.noExternal)
: []
),
],
external: [
'#shared',
new RegExp('^' + escapeStringRegexp(withTrailingSlash(resolve(nuxt.options.rootDir, nuxt.options.dir.shared)))),
...(viteServer.config.ssr.external as string[]) || [],
/node_modules/,
],
resolve: {
modules: nuxt.options.modulesDir,
type: 'module',
extensions: ['.ts', '.js', '.json', '.vue', '.mjs', '.jsx', '.tsx', '.wasm'],
},
}
return (id: string) => isExternal(id, nuxt.options.rootDir, externalOpts)
}

View File

@ -1,18 +1,16 @@
import { mkdir, writeFile } from 'node:fs/promises' import { mkdir, writeFile } from 'node:fs/promises'
import { pathToFileURL } from 'node:url' import { pathToFileURL } from 'node:url'
import { createApp, createError, defineEventHandler, defineLazyEventHandler, eventHandler, toNodeListener } from 'h3' import { createApp, createError, defineEventHandler, toNodeListener } from 'h3'
import { ViteNodeServer } from 'vite-node/server' import { ViteNodeServer } from 'vite-node/server'
import { isAbsolute, join, normalize, resolve } from 'pathe' import { isAbsolute, join, normalize, resolve } from 'pathe'
// import { addDevServerHandler } from '@nuxt/kit' // import { addDevServerHandler } from '@nuxt/kit'
import { isFileServingAllowed } from 'vite' import { isFileServingAllowed } from 'vite'
import type { ModuleNode, Plugin as VitePlugin } from 'vite' import type { ModuleNode, ViteDevServer, Plugin as VitePlugin } from 'vite'
import { getQuery } from 'ufo' import { getQuery } from 'ufo'
import { normalizeViteManifest } from 'vue-bundle-renderer' import { normalizeViteManifest } from 'vue-bundle-renderer'
import { resolve as resolveModule } from 'mlly'
import { distDir } from './dirs' import { distDir } from './dirs'
import type { ViteBuildContext } from './vite' import type { ViteBuildContext } from './vite'
import { isCSS } from './utils' import { isCSS } from './utils'
import { createIsExternal } from './utils/external'
// TODO: Remove this in favor of registerViteNodeMiddleware // TODO: Remove this in favor of registerViteNodeMiddleware
// after Nitropack or h3 allows adding middleware after setup // after Nitropack or h3 allows adding middleware after setup
@ -101,6 +99,19 @@ function getManifest (ctx: ViteBuildContext) {
function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = new Set()) { function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = new Set()) {
const app = createApp() const app = createApp()
let _node: ViteNodeServer | undefined
function getNode (server: ViteDevServer) {
return _node ||= new ViteNodeServer(server, {
deps: {
inline: [/^#/, /\?/],
},
transformMode: {
ssr: [/.*/],
web: [],
},
})
}
app.use('/manifest', defineEventHandler(() => { app.use('/manifest', defineEventHandler(() => {
const manifest = getManifest(ctx) const manifest = getManifest(ctx)
return manifest return manifest
@ -112,39 +123,24 @@ function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = ne
return ids return ids
})) }))
app.use('/module', defineLazyEventHandler(() => { const RESOLVE_RE = /^\/(?<id>[^?]+)(?:\?importer=(?<importer>.*))?$/
const viteServer = ctx.ssrServer! app.use('/resolve', defineEventHandler(async (event) => {
const node = new ViteNodeServer(viteServer, { const { id, importer } = event.path.match(RESOLVE_RE)?.groups || {}
deps: { if (!id || !ctx.ssrServer) {
inline: [
// Common
/^#/,
/\?/,
],
},
transformMode: {
ssr: [/.*/],
web: [],
},
})
const isExternal = createIsExternal(viteServer, ctx.nuxt)
node.shouldExternalize = async (id: string) => {
const result = await isExternal(id)
if (result?.external) {
return resolveModule(result.id, { url: ctx.nuxt.options.modulesDir }).catch(() => false)
}
return false
}
return eventHandler(async (event) => {
const moduleId = decodeURI(event.path).substring(1)
if (moduleId === '/') {
throw createError({ statusCode: 400 }) throw createError({ statusCode: 400 })
} }
if (isAbsolute(moduleId) && !isFileServingAllowed(moduleId, viteServer)) { return await getNode(ctx.ssrServer).resolveId(decodeURIComponent(id), importer ? decodeURIComponent(importer) : undefined).catch(() => null)
}))
app.use('/module', defineEventHandler(async (event) => {
const moduleId = decodeURI(event.path).substring(1)
if (moduleId === '/' || !ctx.ssrServer) {
throw createError({ statusCode: 400 })
}
if (isAbsolute(moduleId) && !isFileServingAllowed(ctx.ssrServer.config, moduleId)) {
throw createError({ statusCode: 403 /* Restricted */ }) throw createError({ statusCode: 403 /* Restricted */ })
} }
const node = getNode(ctx.ssrServer)
const module = await node.fetchModule(moduleId).catch(async (err) => { const module = await node.fetchModule(moduleId).catch(async (err) => {
const errorData = { const errorData = {
code: 'VITE_ERROR', code: 'VITE_ERROR',
@ -159,7 +155,6 @@ function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = ne
throw createError({ data: errorData }) throw createError({ data: errorData })
}) })
return module return module
})
})) }))
return app return app

View File

@ -37,7 +37,7 @@
"css-minimizer-webpack-plugin": "^7.0.0", "css-minimizer-webpack-plugin": "^7.0.0",
"cssnano": "^7.0.6", "cssnano": "^7.0.6",
"defu": "^6.1.4", "defu": "^6.1.4",
"esbuild-loader": "^4.2.2", "esbuild-loader": "^4.3.0",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.2", "fork-ts-checker-webpack-plugin": "^9.0.2",
@ -51,7 +51,7 @@
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^2.0.2", "pathe": "^2.0.2",
"pify": "^6.1.0", "pify": "^6.1.0",
"postcss": "^8.5.1", "postcss": "^8.5.2",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-import-resolver": "^2.0.0", "postcss-import-resolver": "^2.0.0",
"postcss-loader": "^8.1.1", "postcss-loader": "^8.1.1",
@ -61,7 +61,7 @@
"time-fix-plugin": "^2.0.7", "time-fix-plugin": "^2.0.7",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unplugin": "^2.1.2", "unplugin": "^2.2.0",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.1", "vue-bundle-renderer": "^2.1.1",
"vue-loader": "^17.4.2", "vue-loader": "^17.4.2",
@ -73,7 +73,7 @@
}, },
"devDependencies": { "devDependencies": {
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@rspack/core": "1.2.2", "@rspack/core": "1.2.3",
"@types/pify": "5.0.4", "@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0", "@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9", "@types/webpack-hot-middleware": "2.25.9",
@ -85,6 +85,6 @@
"vue": "^3.3.4" "vue": "^3.3.4"
}, },
"engines": { "engines": {
"node": "^18.20.6 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -10,6 +10,6 @@
"nuxt": "workspace:*" "nuxt": "workspace:*"
}, },
"engines": { "engines": {
"node": "^18.20.6 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -2781,8 +2781,14 @@ describe('teleports', () => {
}) })
}) })
describe('Node.js compatibility for client-side', () => { describe('experimental', () => {
it('should work', async () => { it('decorators support works', async () => {
const html = await $fetch('/experimental/decorators')
expect(html).toContain('decorated-decorated')
expectNoClientErrors('/experimental/decorators')
})
it('Node.js compatibility for client-side', async () => {
const { page } = await renderPage('/experimental/node-compat') const { page } = await renderPage('/experimental/node-compat')
await page.locator('body').getByText('Nuxt is Awesome!').waitFor() await page.locator('body').getByText('Nuxt is Awesome!').waitFor()
expect(await page.innerHTML('body')).toContain('CWD: [available]') expect(await page.innerHTML('body')).toContain('CWD: [available]')

View File

@ -16,6 +16,6 @@
"vue-router": "latest" "vue-router": "latest"
}, },
"engines": { "engines": {
"node": "^18.20.6 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -159,6 +159,7 @@ export default defineNuxtConfig({
inlineStyles: id => !!id && !id.includes('assets.vue'), inlineStyles: id => !!id && !id.includes('assets.vue'),
}, },
experimental: { experimental: {
decorators: true,
serverAppConfig: true, serverAppConfig: true,
typedPages: true, typedPages: true,
clientFallback: true, clientFallback: true,

View File

@ -17,6 +17,6 @@
"vue": "latest" "vue": "latest"
}, },
"engines": { "engines": {
"node": "^18.20.6 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
function something (_method: () => unknown) {
return () => 'decorated'
}
class SomeClass {
@something
public someMethod () {
return 'initial'
}
}
const value = new SomeClass().someMethod()
const { data } = await useFetch('/api/experimental/decorators')
</script>
<template>
<div>
{{ value }}-{{ data }}
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,14 @@
export default eventHandler((_event) => {
function something (_method: () => unknown) {
return () => 'decorated'
}
class SomeClass {
@something
public someMethod () {
return 'initial'
}
}
return new SomeClass().someMethod()
})

View File

@ -8,6 +8,6 @@
"nuxt": "workspace:*" "nuxt": "workspace:*"
}, },
"engines": { "engines": {
"node": "^18.20.6 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -8,6 +8,6 @@
"nuxt": "workspace:*" "nuxt": "workspace:*"
}, },
"engines": { "engines": {
"node": "^18.20.6 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -9,6 +9,6 @@
"nuxt": "workspace:*" "nuxt": "workspace:*"
}, },
"engines": { "engines": {
"node": "^18.20.6 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -8,6 +8,6 @@
"nuxt": "workspace:*" "nuxt": "workspace:*"
}, },
"engines": { "engines": {
"node": "^18.20.6 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -8,6 +8,6 @@
"nuxt": "workspace:*" "nuxt": "workspace:*"
}, },
"engines": { "engines": {
"node": "^18.20.6 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -10,6 +10,6 @@
"nuxt": "workspace:*" "nuxt": "workspace:*"
}, },
"engines": { "engines": {
"node": "^18.20.6 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -11,6 +11,6 @@
"typescript": "latest" "typescript": "latest"
}, },
"engines": { "engines": {
"node": "^18.20.6 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -0,0 +1,29 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { defineComponent, h } from 'vue'
describe('app/compat', () => {
const Component = defineComponent({
setup () {
const visible = ref(false)
setInterval(() => {
visible.value = true
}, 1000)
return () => h('div', {}, visible.value ? h('span', { id: 'child' }) : {})
},
})
it('setInterval is not auto-imported', async () => {
vi.useFakeTimers()
const wrapper = mount(Component)
vi.advanceTimersByTime(1000)
await wrapper.vm.$nextTick()
expect(wrapper.find('#child').exists()).toBe(true)
vi.useRealTimers()
})
})

View File

@ -22,6 +22,9 @@ export default defineVitestConfig({
experimental: { experimental: {
appManifest: process.env.TEST_MANIFEST !== 'manifest-off', appManifest: process.env.TEST_MANIFEST !== 'manifest-off',
}, },
imports: {
polyfills: false,
},
}, },
}, },
}, },