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
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
<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])
async function getCurrentUser() {
return await $fetch('/api/me', { headers: headers.value })
return await $fetch('/api/me', { headers })
}
</script>
```
::tip
You can also use [`useRequestFetch`](/docs/api/composables/use-request-fetch) to proxy headers to the call automatically.
::
::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:
@ -115,10 +128,6 @@ Be very careful before proxying headers to an external API and just include head
- `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`
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
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
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.

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.
## 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
### 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
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`:

View File

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

View File

@ -27,7 +27,7 @@
"test:attw": "attw --pack"
},
"dependencies": {
"c12": "^2.0.1",
"c12": "^2.0.2",
"consola": "^3.4.0",
"defu": "^6.1.4",
"destr": "^2.0.3",
@ -45,12 +45,12 @@
"std-env": "^3.8.0",
"ufo": "^1.5.4",
"unctx": "^2.4.1",
"unimport": "^4.1.0",
"unimport": "^4.1.1",
"untyped": "^1.5.2"
},
"devDependencies": {
"@nuxt/schema": "workspace:*",
"@rspack/core": "1.2.2",
"@rspack/core": "1.2.3",
"@types/semver": "7.5.8",
"nitro": "npm:nitro-nightly@3.0.0-beta-28969273.f7aa9de6",
"unbuild": "3.3.1",
@ -59,6 +59,6 @@
"webpack": "5.97.1"
},
"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'
/** 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')
/** 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
/**
@ -16,7 +29,7 @@ export const nuxtCtx = getContext<Nuxt>('nuxt')
* ```
*/
export function useNuxt (): Nuxt {
const instance = nuxtCtx.tryUse()
const instance = asyncNuxtStorage.tryUse() || nuxtCtx.tryUse()
if (!instance) {
throw new Error('Nuxt instance is unavailable!')
}
@ -36,5 +49,9 @@ export function useNuxt (): Nuxt {
* ```
*/
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 { addComponent, addComponentsDir } 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 { addLayout } from './layout'
export { addRouteMiddleware, extendPages, extendRouteRules } from './pages'

View File

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

View File

@ -3,6 +3,7 @@ import { readPackageJSON, resolvePackageJSON } from 'pkg-types'
import type { Nuxt, NuxtConfig } from '@nuxt/schema'
import { resolve } from 'pathe'
import { importModule, tryImportModule } from '../internal/esm'
import { runWithNuxtContext } from '../context'
import type { LoadNuxtConfigOptions } from './config'
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 { 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', () => {
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'))
.catch(() => isV4)
const useDecorators = Boolean(nuxt.options.experimental?.decorators)
// https://www.totaltypescript.com/tsconfig-cheat-sheet
const tsConfig: TSConfig = defu(nuxt.options.typescript?.tsConfig, {
compilerOptions: {
@ -197,12 +199,20 @@ export async function _generateTypes (nuxt: Nuxt) {
noUncheckedIndexedAccess: isV4,
forceConsistentCasingInFileNames: true,
noImplicitOverride: true,
/* Decorator support */
...useDecorators
? {
useDefineForClassFields: false,
experimentalDecorators: false,
}
: {},
/* If NOT transpiling with TypeScript: */
module: hasTypescriptVersionWithModulePreserve ? 'preserve' : 'ESNext',
noEmit: true,
/* If your code runs in the DOM: */
lib: [
'ESNext',
...useDecorators ? ['esnext.decorators'] : [],
'dom',
'dom.iterable',
'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/kit": "workspace:*",
"@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.6.4",
"@nuxt/telemetry": "^2.6.5",
"@nuxt/vite-builder": "workspace:*",
"@unhead/vue": "^2.0.0-alpha.8",
"@vue/shared": "^3.5.13",
"acorn": "8.14.0",
"c12": "^2.0.1",
"c12": "^2.0.2",
"chokidar": "^4.0.3",
"compatx": "^0.1.8",
"consola": "^3.4.0",
@ -116,8 +116,8 @@
"uncrypto": "^0.1.3",
"unctx": "^2.4.1",
"unenv": "^1.10.0",
"unimport": "^4.1.0",
"unplugin": "^2.1.2",
"unimport": "^4.1.1",
"unplugin": "^2.2.0",
"unplugin-vue-router": "^0.11.2",
"unstorage": "^1.14.4",
"untyped": "^1.5.2",
@ -149,6 +149,6 @@
}
},
"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
}
export function _replaceAppConfig (newConfig: AppConfig) {
const appConfig = useAppConfig()
deepAssign(appConfig, newConfig)
deepDelete(appConfig, newConfig)
}
/**
* Deep assign the current appConfig with the new one.
*

View File

@ -1,11 +1,12 @@
import { existsSync } from 'node:fs'
import { rm } from 'node:fs/promises'
import { randomUUID } from 'node:crypto'
import { AsyncLocalStorage } from 'node:async_hooks'
import { join, normalize, relative, resolve } from 'pathe'
import { createDebugger, createHooks } from 'hookable'
import ignore from 'ignore'
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 { PackageJson } from 'pkg-types'
import { readPackageJSON } from 'pkg-types'
@ -53,17 +54,24 @@ import { VirtualFSPlugin } from './plugins/virtual'
export function createNuxt (options: NuxtOptions): Nuxt {
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 = {
__name: randomUUID(),
_version: version,
_asyncLocalStorageModule: options.experimental.debugModuleMutation ? new AsyncLocalStorage() : undefined,
hooks,
callHook: hooks.callHook,
addHooks: hooks.addHooks,
hook: hooks.hook,
ready: () => initNuxt(nuxt),
ready: () => runWithNuxtContext(nuxt, () => initNuxt(nuxt)),
close: () => hooks.callHook('close', nuxt),
vfs: {},
apps: {},
runWithContext: fn => runWithNuxtContext(nuxt, fn),
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() })
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 || []
// 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),
)
// 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(/\/?$/, '/'))
nuxt.options.modulesDir.push(...nuxt.options._layers
.filter(l => l.cwd !== nuxt.options.rootDir && locallyScannedLayersDirs.every(dir => !l.cwd.startsWith(dir)))
@ -861,27 +872,29 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
const nuxt = createNuxt(options)
if (nuxt.options.dev && !nuxt.options.test) {
nuxt.hooks.hookOnce('build:done', () => {
for (const dep of keyDependencies) {
checkDependencyVersion(dep, nuxt._version)
.catch(e => logger.warn(`Problem checking \`${dep}\` version.`, e))
}
})
}
nuxt.runWithContext(() => {
if (nuxt.options.dev && !nuxt.options.test) {
nuxt.hooks.hookOnce('build:done', () => {
for (const dep of keyDependencies) {
checkDependencyVersion(dep, nuxt._version)
.catch(e => logger.warn(`Problem checking \`${dep}\` version.`, e))
}
})
}
// We register hooks layer-by-layer so any overrides need to be registered separately
if (opts.overrides?.hooks) {
nuxt.hooks.addHooks(opts.overrides.hooks)
}
// We register hooks layer-by-layer so any overrides need to be registered separately
if (opts.overrides?.hooks) {
nuxt.hooks.addHooks(opts.overrides.hooks)
}
if (
nuxt.options.debug
&& nuxt.options.debug.hooks
&& (nuxt.options.debug.hooks === true || nuxt.options.debug.hooks.server)
) {
createDebugger(nuxt.hooks, { tag: 'nuxt' })
}
if (
nuxt.options.debug
&& nuxt.options.debug.hooks
&& (nuxt.options.debug.hooks === true || nuxt.options.debug.hooks.server)
) {
createDebugger(nuxt.hooks, { tag: 'nuxt' })
}
})
if (opts.ready !== false) {
await nuxt.ready()

View File

@ -1,5 +1,4 @@
import type { Literal, Property, SpreadElement } from 'estree'
import { transform } from 'esbuild'
import { defu } from 'defu'
import { findExports } from 'mlly'
import type { Nuxt } from '@nuxt/schema'
@ -8,7 +7,7 @@ import MagicString from 'magic-string'
import { normalize } from 'pathe'
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'
const internalOrderMap = {

View File

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

View File

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

View File

@ -1,22 +1,17 @@
import { walk as _walk } from 'estree-walker'
import type { Node, SyncHandler } from 'estree-walker'
import type {
ArrowFunctionExpression,
CatchClause,
Program as ESTreeProgram,
FunctionDeclaration,
FunctionExpression,
Identifier,
ImportDefaultSpecifier,
ImportNamespaceSpecifier,
ImportSpecifier,
VariableDeclaration,
} from 'estree'
import type { ArrowFunctionExpression, CatchClause, Program as ESTreeProgram, FunctionDeclaration, FunctionExpression, Identifier, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, VariableDeclaration } from 'estree'
import { parse } 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 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 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 { isDirectory, logger } from '../utils'
import { TransformPlugin } from './transform'
import { defaultPresets } from './presets'
import { appCompatPresets, defaultPresets } from './presets'
export default defineNuxtModule<Partial<ImportsOptions>>({
meta: {
@ -30,11 +30,16 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
exclude: undefined,
},
virtualImports: ['#imports'],
polyfills: true,
}),
async setup (options, nuxt) {
// TODO: fix sharing of defaults between invocations of modules
const presets = JSON.parse(JSON.stringify(options.presets)) as ImportPresetWithDeprecation[]
if (nuxt.options.imports.polyfills) {
presets.push(...appCompatPresets)
}
// Allow modules extending sources
await nuxt.callHook('imports:sources', presets)

View File

@ -21,14 +21,6 @@ const granularAppPresets: InlinePreset[] = [
imports: ['useNuxtApp', 'tryUseNuxtApp', 'defineNuxtPlugin', 'definePayloadPlugin', 'useRuntimeConfig', 'defineAppConfig'],
from: '#app/nuxt',
},
{
imports: ['requestIdleCallback', 'cancelIdleCallback'],
from: '#app/compat/idle-callback',
},
{
imports: ['setInterval'],
from: '#app/compat/interval',
},
{
imports: ['useAppConfig', 'updateAppConfig'],
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[] = [
...commonPresets,
...granularAppPresets,

View File

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

View File

@ -7,12 +7,11 @@ import { genArrayFromRaw, genDynamicImport, genImport, genSafeVariableName } fro
import escapeRE from 'escape-string-regexp'
import { filename } from 'pathe/utils'
import { hash } from 'ohash'
import { transform } from 'esbuild'
import type { Property } from 'estree'
import type { NuxtPage } from 'nuxt/schema'
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 { logger, toArray } from '../utils'

View File

@ -1,7 +1,8 @@
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 { withoutTrailingSlash } from 'ufo'
import { logger, tryUseNuxt, useNuxt } from '@nuxt/kit'
import { loadNuxt } from '../src'
const repoRoot = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../', import.meta.url))))
@ -12,6 +13,7 @@ vi.stubGlobal('console', {
warn: vi.fn(console.warn),
})
const loggerWarn = vi.spyOn(logger, 'warn')
vi.mock('pkg-types', async (og) => {
const originalPkgTypes = (await og<typeof import('pkg-types')>())
return {
@ -20,6 +22,9 @@ vi.mock('pkg-types', async (og) => {
}
})
beforeEach(() => {
loggerWarn.mockClear()
})
afterEach(() => {
vi.clearAllMocks()
})
@ -41,4 +46,41 @@ describe('loadNuxt', () => {
await nuxt.close()
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": {
"@nuxt/friendly-errors-webpack-plugin": "^2.6.0",
"@nuxt/kit": "workspace:*",
"@rspack/core": "^1.2.2",
"@rspack/core": "^1.2.3",
"autoprefixer": "^10.4.20",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
"cssnano": "^7.0.6",
"defu": "^6.1.4",
"esbuild-loader": "^4.2.2",
"esbuild-loader": "^4.3.0",
"escape-string-regexp": "^5.0.0",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.2",
@ -51,7 +51,7 @@
"ohash": "^1.1.4",
"pathe": "^2.0.2",
"pify": "^6.1.0",
"postcss": "^8.5.1",
"postcss": "^8.5.2",
"postcss-import": "^16.1.0",
"postcss-import-resolver": "^2.0.0",
"postcss-loader": "^8.1.1",
@ -61,7 +61,7 @@
"time-fix-plugin": "^2.0.7",
"ufo": "^1.5.4",
"unenv": "^1.10.0",
"unplugin": "^2.1.2",
"unplugin": "^2.2.0",
"url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.1",
"vue-loader": "^17.4.2",
@ -83,6 +83,6 @@
"vue": "^3.3.4"
},
"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',
'css-minimizer-webpack-plugin',
'cssnano',
'esbuild',
'esbuild-loader',
'file-loader',
'h3',

View File

@ -37,28 +37,35 @@
},
"devDependencies": {
"@types/pug": "2.0.10",
"@types/rollup-plugin-visualizer": "4.2.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9",
"@unhead/schema": "2.0.0-alpha.9",
"@vitejs/plugin-vue": "5.2.1",
"@vitejs/plugin-vue-jsx": "4.1.1",
"@vue/compiler-core": "3.5.13",
"@vue/compiler-sfc": "3.5.13",
"@vue/language-core": "2.2.0",
"c12": "2.0.1",
"c12": "2.0.2",
"chokidar": "4.0.3",
"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",
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"hookable": "5.5.3",
"ignore": "7.0.3",
"mini-css-extract-plugin": "2.9.2",
"nitro": "npm:nitro-nightly@3.0.0-beta-28969273.f7aa9de6",
"ofetch": "1.4.1",
"pkg-types": "1.3.1",
"postcss": "8.5.2",
"sass-loader": "16.0.4",
"scule": "1.3.0",
"unbuild": "3.3.1",
"unctx": "2.4.1",
"unimport": "4.1.0",
"unimport": "4.1.1",
"untyped": "1.5.2",
"vite": "6.1.0",
"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.
*
@ -14,10 +14,13 @@ export default defineUntypedSchema({
if (Array.isArray(val)) {
return { dirs: val }
}
if (val === undefined || val === true) {
return { dirs: [{ path: '~/components/global', global: true }, '~/components'] }
if (val === false) {
return { dirs: [] }
}
return {
dirs: [{ path: '~/components/global', global: true }, '~/components'],
...typeof val === 'object' ? val : {},
}
return val
},
},

View File

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

View File

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

View File

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

View File

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

View File

@ -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
* (possibly major) version of the framework.
@ -30,10 +30,10 @@ export default defineUntypedSchema({
*/
typescriptBundlerResolution: {
async $resolve (val, get) {
// TODO: remove in v3.10
val = val ?? await (get('experimental') as Promise<Record<string, any>>).then(e => e?.typescriptBundlerResolution)
// @ts-expect-error TODO: remove in v3.10
val = typeof val === 'boolean' ? val : await (get('experimental')).then(e => e?.typescriptBundlerResolution as string | undefined)
if (typeof val === 'boolean') { return val }
const setting = await get('typescript.tsConfig.compilerOptions.moduleResolution') as string | undefined
const setting = await get('typescript.tsConfig').then(r => r?.compilerOptions?.moduleResolution)
if (setting) {
return setting.toLowerCase() === 'bundler'
}
@ -53,14 +53,22 @@ export default defineUntypedSchema({
* @type {boolean | ((id?: string) => boolean)}
*/
inlineStyles: {
async $resolve (val, get) {
// TODO: remove in v3.10
val = val ?? await (get('experimental') as Promise<Record<string, any>>).then((e: Record<string, any>) => e?.inlineSSRStyles)
if (val === false || (await get('dev')) || (await get('ssr')) === false || (await get('builder')) === '@nuxt/webpack-builder') {
async $resolve (_val, get) {
const val = typeof _val === 'boolean' || typeof _val === 'function'
? _val
// @ts-expect-error TODO: legacy property - remove in v3.10
: await (get('experimental')).then(e => e?.inlineSSRStyles) as undefined | boolean
if (
val === false ||
(await get('dev')) ||
(await get('ssr')) === false ||
// @ts-expect-error TODO: handled normalised types
(await get('builder')) === '@nuxt/webpack-builder'
) {
return false
}
// Enabled by default for vite prod with ssr (for vue components)
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? (id: string) => id && id.includes('.vue') : true)
return val ?? ((await get('future')).compatibilityVersion === 4 ? (id?: string) => !!id && id.includes('.vue') : true)
},
},
@ -73,7 +81,9 @@ export default defineUntypedSchema({
*/
devLogs: {
async $resolve (val, get) {
if (val !== undefined) { return val }
if (typeof val === 'boolean' || val === 'silent') {
return val
}
const [isDev, isTest] = await Promise.all([get('dev'), get('test')])
return isDev && !isTest
},
@ -85,17 +95,25 @@ export default defineUntypedSchema({
*/
noScripts: {
async $resolve (val, get) {
// TODO: remove in v3.10
return val ?? await (get('experimental') as Promise<Record<string, any>>).then((e: Record<string, any>) => e?.noScripts) ?? false
return typeof val === 'boolean'
? val
// @ts-expect-error TODO: legacy property - remove in v3.10
: (await (get('experimental')).then(e => e?.noScripts as boolean | undefined) ?? false)
},
},
},
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).
*/
asyncEntry: {
$resolve: val => val ?? false,
$resolve: val => typeof val === 'boolean' ? val : false,
},
// TODO: Remove when nitro has support for mocking traced dependencies
@ -135,7 +153,17 @@ export default defineUntypedSchema({
if (val === 'reload') {
return 'automatic'
}
return val ?? 'automatic'
if (val === false) {
return false
}
const validOptions = ['manual', 'automatic', 'automatic-immediate'] as const
type EmitRouteChunkError = typeof validOptions[number]
if (typeof val === 'string' && validOptions.includes(val as EmitRouteChunkError)) {
return val as EmitRouteChunkError
}
return 'automatic'
},
},
@ -255,14 +283,16 @@ export default defineUntypedSchema({
*/
watcher: {
$resolve: async (val, get) => {
if (val) {
return val
const validOptions = ['chokidar', 'parcel', 'chokidar-granular'] as const
type WatcherOption = typeof validOptions[number]
if (typeof val === 'string' && validOptions.includes(val as WatcherOption)) {
return val as WatcherOption
}
const [srcDir, rootDir] = await Promise.all([get('srcDir'), get('rootDir')]) as [string, string]
const [srcDir, rootDir] = await Promise.all([get('srcDir'), get('rootDir')])
if (srcDir === rootDir) {
return 'chokidar-granular'
return 'chokidar-granular' as const
}
return 'chokidar'
return 'chokidar' as const
},
},
@ -304,7 +334,7 @@ export default defineUntypedSchema({
*/
scanPageMeta: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? 'after-resolve' : true)
return typeof val === 'boolean' || val === 'after-resolve' ? val : ((await get('future')).compatibilityVersion === 4 ? 'after-resolve' : true)
},
},
@ -343,7 +373,7 @@ export default defineUntypedSchema({
*/
sharedPrerenderData: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
return typeof val === 'boolean' ? val : ((await get('future')).compatibilityVersion === 4)
},
},
@ -414,7 +444,7 @@ export default defineUntypedSchema({
*/
normalizeComponentNames: {
$resolve: async (val, get) => {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
return typeof val === 'boolean' ? val : ((await get('future')).compatibilityVersion === 4)
},
},
@ -425,7 +455,9 @@ export default defineUntypedSchema({
*/
spaLoadingTemplateLocation: {
$resolve: async (val, get) => {
return val ?? (((await get('future') as Record<string, unknown>).compatibilityVersion === 4) ? 'body' : 'within')
const validOptions = ['body', 'within'] as const
type SpaLoadingTemplateLocation = typeof validOptions[number]
return typeof val === 'string' && validOptions.includes(val as SpaLoadingTemplateLocation) ? val as SpaLoadingTemplateLocation : (((await get('future')).compatibilityVersion === 4) ? 'body' : 'within')
},
},
@ -435,7 +467,7 @@ export default defineUntypedSchema({
* @see [the Chrome DevTools extensibility API](https://developer.chrome.com/docs/devtools/performance/extension#tracks)
*/
browserDevtoolsTiming: {
$resolve: async (val, get) => val ?? await get('dev'),
$resolve: async (val, get) => typeof val === 'boolean' ? val : await get('dev'),
},
/**
@ -443,7 +475,7 @@ export default defineUntypedSchema({
*/
debugModuleMutation: {
$resolve: async (val, get) => {
return val ?? Boolean(await get('debug'))
return typeof val === 'boolean' ? val : Boolean(await get('debug'))
},
},
},

View File

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

View File

@ -1,8 +1,11 @@
import type { ResolvableConfigSchema } from '../utils/definition'
import adhoc from './adhoc'
import app from './app'
import build from './build'
import common from './common'
import dev from './dev'
import esbuild from './esbuild'
import experimental from './experimental'
import generate from './generate'
import internal from './internal'
@ -26,6 +29,7 @@ export default {
...postcss,
...router,
...typescript,
...esbuild,
...vite,
...webpack,
}
} satisfies ResolvableConfigSchema

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,4 +32,10 @@ export interface ImportsOptions extends UnimportOptions {
exclude?: 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
/**
* Fully resolved path used internally by Nuxt. Do not depend on this value.
* @internal
*/
rawPath?: string
[key: string]: unknown
}

View File

@ -83,6 +83,7 @@ export interface NuxtApp {
export interface Nuxt {
// Private fields.
__name: string
_version: string
_ignore?: Ignore
_dependencies?: Set<string>
@ -96,6 +97,7 @@ export interface Nuxt {
hook: Nuxt['hooks']['hook']
callHook: Nuxt['hooks']['callHook']
addHooks: Nuxt['hooks']['addHooks']
runWithContext: <T extends (...args: any[]) => any>(fn: T) => ReturnType<T>
ready: () => 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",
"knitwork": "1.2.0",
"pathe": "2.0.2",
"prettier": "3.4.2",
"prettier": "3.5.0",
"scule": "1.3.0",
"svgo": "3.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
calc(var(--un-ring-width) + var(--un-ring-offset-width))
var(--un-ring-color);
box-shadow: var(--un-ring-offset-shadow), var(--un-ring-shadow),
var(--un-shadow);
box-shadow:
var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);
}
.transition-all {
transition-duration: 0.15s;

View File

@ -41,7 +41,6 @@
"defu": "^6.1.4",
"esbuild": "^0.25.0",
"escape-string-regexp": "^5.0.0",
"externality": "^1.0.2",
"get-port-please": "^3.1.2",
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"jiti": "^2.4.2",
@ -50,12 +49,12 @@
"mlly": "^1.7.4",
"pathe": "^2.0.2",
"pkg-types": "^1.3.1",
"postcss": "^8.5.1",
"postcss": "^8.5.2",
"rollup-plugin-visualizer": "^5.14.0",
"std-env": "^3.8.0",
"ufo": "^1.5.4",
"unenv": "^1.10.0",
"unplugin": "^2.1.2",
"unplugin": "^2.2.0",
"vite": "^6.1.0",
"vite-node": "^3.0.5",
"vite-plugin-checker": "^0.8.0",
@ -65,6 +64,6 @@
"vue": "^3.3.4"
},
"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({
root: viteNodeOptions.root, // Equals to Nuxt `srcDir`
base: viteNodeOptions.base,
async resolveId (id, importer) {
return await viteNodeFetch('/resolve/' + encodeURIComponent(id) + (importer ? '?importer=' + encodeURIComponent(importer) : '')) ?? undefined
},
async fetchModule (id) {
id = id.replace(/\/\//g, '/') // TODO: fix in vite-node
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 { 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 { isAbsolute, join, normalize, resolve } from 'pathe'
// import { addDevServerHandler } from '@nuxt/kit'
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 { normalizeViteManifest } from 'vue-bundle-renderer'
import { resolve as resolveModule } from 'mlly'
import { distDir } from './dirs'
import type { ViteBuildContext } from './vite'
import { isCSS } from './utils'
import { createIsExternal } from './utils/external'
// TODO: Remove this in favor of registerViteNodeMiddleware
// 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()) {
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(() => {
const manifest = getManifest(ctx)
return manifest
@ -112,54 +123,38 @@ function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = ne
return ids
}))
app.use('/module', defineLazyEventHandler(() => {
const viteServer = ctx.ssrServer!
const node = new ViteNodeServer(viteServer, {
deps: {
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
const RESOLVE_RE = /^\/(?<id>[^?]+)(?:\?importer=(?<importer>.*))?$/
app.use('/resolve', defineEventHandler(async (event) => {
const { id, importer } = event.path.match(RESOLVE_RE)?.groups || {}
if (!id || !ctx.ssrServer) {
throw createError({ statusCode: 400 })
}
return await getNode(ctx.ssrServer).resolveId(decodeURIComponent(id), importer ? decodeURIComponent(importer) : undefined).catch(() => null)
}))
return eventHandler(async (event) => {
const moduleId = decodeURI(event.path).substring(1)
if (moduleId === '/') {
throw createError({ statusCode: 400 })
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 */ })
}
const node = getNode(ctx.ssrServer)
const module = await node.fetchModule(moduleId).catch(async (err) => {
const errorData = {
code: 'VITE_ERROR',
id: moduleId,
stack: '',
...err,
}
if (isAbsolute(moduleId) && !isFileServingAllowed(moduleId, viteServer)) {
throw createError({ statusCode: 403 /* Restricted */ })
}
const module = await node.fetchModule(moduleId).catch(async (err) => {
const errorData = {
code: 'VITE_ERROR',
id: moduleId,
stack: '',
...err,
}
if (!errorData.frame && errorData.code === 'PARSE_ERROR') {
errorData.frame = await node.transformModule(moduleId, 'web').then(({ code }) => `${err.message || ''}\n${code}`).catch(() => undefined)
}
throw createError({ data: errorData })
})
return module
if (!errorData.frame && errorData.code === 'PARSE_ERROR') {
errorData.frame = await node.transformModule(moduleId, 'web').then(({ code }) => `${err.message || ''}\n${code}`).catch(() => undefined)
}
throw createError({ data: errorData })
})
return module
}))
return app

View File

@ -37,7 +37,7 @@
"css-minimizer-webpack-plugin": "^7.0.0",
"cssnano": "^7.0.6",
"defu": "^6.1.4",
"esbuild-loader": "^4.2.2",
"esbuild-loader": "^4.3.0",
"escape-string-regexp": "^5.0.0",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.2",
@ -51,7 +51,7 @@
"ohash": "^1.1.4",
"pathe": "^2.0.2",
"pify": "^6.1.0",
"postcss": "^8.5.1",
"postcss": "^8.5.2",
"postcss-import": "^16.1.0",
"postcss-import-resolver": "^2.0.0",
"postcss-loader": "^8.1.1",
@ -61,7 +61,7 @@
"time-fix-plugin": "^2.0.7",
"ufo": "^1.5.4",
"unenv": "^1.10.0",
"unplugin": "^2.1.2",
"unplugin": "^2.2.0",
"url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.1",
"vue-loader": "^17.4.2",
@ -73,7 +73,7 @@
},
"devDependencies": {
"@nuxt/schema": "workspace:*",
"@rspack/core": "1.2.2",
"@rspack/core": "1.2.3",
"@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9",
@ -85,6 +85,6 @@
"vue": "^3.3.4"
},
"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:*"
},
"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', () => {
it('should work', async () => {
describe('experimental', () => {
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')
await page.locator('body').getByText('Nuxt is Awesome!').waitFor()
expect(await page.innerHTML('body')).toContain('CWD: [available]')

View File

@ -16,6 +16,6 @@
"vue-router": "latest"
},
"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'),
},
experimental: {
decorators: true,
serverAppConfig: true,
typedPages: true,
clientFallback: true,

View File

@ -17,6 +17,6 @@
"vue": "latest"
},
"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:*"
},
"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:*"
},
"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:*"
},
"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:*"
},
"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:*"
},
"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:*"
},
"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"
},
"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: {
appManifest: process.env.TEST_MANIFEST !== 'manifest-off',
},
imports: {
polyfills: false,
},
},
},
},