diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 8a3d0b82ac..3c387296d5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM node:lts@sha256:0e910f435308c36ea60b4cfd7b80208044d77a074d16b768a81901ce938a62dc +FROM node:lts@sha256:99981c3d1aac0d98cd9f03f74b92dddf30f30ffb0b34e6df8bd96283f62f12c6 RUN apt-get update && \ apt-get install -fy libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdbus-1-3 libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2 && \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fc0803c60..a0da1fbf71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ jobs: run: pnpm test:attw - name: Cache dist - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: retention-days: 3 name: dist diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index cd4b3fc9af..f6baea9476 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -59,7 +59,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 if: github.repository == 'nuxt/nuxt' && success() with: name: SARIF file diff --git a/docs/3.api/2.composables/use-fetch.md b/docs/3.api/2.composables/use-fetch.md index 038082ab65..3d5834dc02 100644 --- a/docs/3.api/2.composables/use-fetch.md +++ b/docs/3.api/2.composables/use-fetch.md @@ -147,7 +147,7 @@ If you have not fetched data on the server (for example, with `server: false`), ```ts [Signature] function useFetch( - url: string | Request | Ref | (() => string) | Request, + url: string | Request | Ref | (() => string | Request), options?: UseFetchOptions ): Promise> diff --git a/docs/5.community/6.roadmap.md b/docs/5.community/6.roadmap.md index c659fd0e55..95362bd084 100644 --- a/docs/5.community/6.roadmap.md +++ b/docs/5.community/6.roadmap.md @@ -63,7 +63,7 @@ Each active version has its own nightly releases which are generated automatical Release | | Initial release | End Of Life | Docs ----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|------- -**4.x** (scheduled) | | 2024 Q3 | |   +**4.x** (scheduled) | | approximately 1 month after release of nitro v3 | |   **3.x** (stable) | Nuxt latest 3.x version | 2022-11-16 | TBA | [nuxt.com](/docs) **2.x** (unsupported) | Nuxt 2.x version | 2018-09-21 | 2024-06-30 | [v2.nuxt.com](https://v2.nuxt.com/docs) **1.x** (unsupported) | Nuxt 1.x version | 2018-01-08 | 2019-09-21 |   diff --git a/package.json b/package.json index 61284d5faf..6cb3fe15d2 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "@nuxt/vite-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*", "@types/node": "22.10.5", - "@unhead/dom": "1.11.14", - "@unhead/schema": "1.11.14", - "@unhead/shared": "1.11.14", - "@unhead/ssr": "1.11.14", - "@unhead/vue": "1.11.14", + "@unhead/dom": "1.11.15", + "@unhead/schema": "1.11.15", + "@unhead/shared": "1.11.15", + "@unhead/ssr": "1.11.15", + "@unhead/vue": "1.11.15", "@vue/compiler-core": "3.5.13", "@vue/compiler-dom": "3.5.13", "@vue/shared": "3.5.13", @@ -56,19 +56,19 @@ "nuxt": "workspace:*", "ohash": "1.1.4", "postcss": "8.4.49", - "rollup": "4.30.0", + "rollup": "4.30.1", "send": ">=1.1.0", - "typescript": "5.7.2", + "typescript": "5.7.3", "ufo": "1.5.4", "unbuild": "3.2.0", - "unhead": "1.11.14", + "unhead": "1.11.15", "unimport": "3.14.5", "vite": "6.0.7", "vue": "3.5.13" }, "devDependencies": { "@arethetypeswrong/cli": "0.17.2", - "@nuxt/eslint-config": "0.7.4", + "@nuxt/eslint-config": "0.7.5", "@nuxt/kit": "workspace:*", "@nuxt/rspack-builder": "workspace:*", "@nuxt/test-utils": "3.15.1", @@ -76,8 +76,8 @@ "@testing-library/vue": "8.1.0", "@types/node": "22.10.5", "@types/semver": "7.5.8", - "@unhead/schema": "1.11.14", - "@unhead/vue": "1.11.14", + "@unhead/schema": "1.11.15", + "@unhead/vue": "1.11.15", "@vitest/coverage-v8": "2.1.8", "@vue/test-utils": "2.4.6", "autoprefixer": "10.4.20", @@ -90,27 +90,27 @@ "eslint": "9.17.0", "eslint-plugin-no-only-tests": "3.3.0", "eslint-plugin-perfectionist": "4.6.0", - "eslint-typegen": "0.3.2", + "eslint-typegen": "1.0.0", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", - "happy-dom": "16.3.0", + "happy-dom": "16.5.3", "installed-check": "9.3.0", "jiti": "2.4.2", - "knip": "5.41.1", + "knip": "5.42.0", "markdownlint-cli": "0.43.0", - "memfs": "4.15.3", + "memfs": "4.17.0", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", "nuxi": "3.18.2", "nuxt": "workspace:*", "nuxt-content-twoslash": "0.1.2", "ofetch": "1.4.1", - "pathe": "2.0.0", + "pathe": "2.0.1", "playwright-core": "1.49.1", "semver": "7.6.3", "sherif": "1.1.1", "std-env": "3.8.0", "tinyexec": "0.3.2", "tinyglobby": "0.2.10", - "typescript": "5.7.2", + "typescript": "5.7.3", "ufo": "1.5.4", "vitest": "2.1.8", "vitest-environment-nuxt": "1.0.1", diff --git a/packages/kit/package.json b/packages/kit/package.json index 78bb5d437e..3a318286f2 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -39,7 +39,7 @@ "klona": "^2.0.6", "mlly": "^1.7.3", "ohash": "^1.1.4", - "pathe": "^2.0.0", + "pathe": "^2.0.1", "pkg-types": "^1.3.0", "scule": "^1.3.0", "semver": "^7.6.3", diff --git a/packages/kit/src/template.ts b/packages/kit/src/template.ts index d471a7c1eb..8fa065fad7 100644 --- a/packages/kit/src/template.ts +++ b/packages/kit/src/template.ts @@ -291,6 +291,10 @@ export async function _generateTypes (nuxt: Nuxt) { })) } + // Ensure `#build` is placed at the end of the paths object. + // https://github.com/nuxt/nuxt/issues/30325 + sortTsPaths(tsConfig.compilerOptions.paths) + tsConfig.include = [...new Set(tsConfig.include.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))] tsConfig.exclude = [...new Set(tsConfig.exclude!.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))] @@ -330,6 +334,17 @@ export async function writeTypes (nuxt: Nuxt) { await writeFile() } +function sortTsPaths (paths: Record) { + for (const pathKey in paths) { + if (pathKey.startsWith('#build')) { + const pathValue = paths[pathKey]! + // Delete & Reassign to ensure key is inserted at the end of object. + delete paths[pathKey] + paths[pathKey] = pathValue + } + } +} + function renderAttrs (obj: Record) { const attrs: string[] = [] for (const key in obj) { diff --git a/packages/kit/test/generate-types.spec.ts b/packages/kit/test/generate-types.spec.ts index d65d6809bd..b01811e131 100644 --- a/packages/kit/test/generate-types.spec.ts +++ b/packages/kit/test/generate-types.spec.ts @@ -59,4 +59,42 @@ describe('tsConfig generation', () => { ] `) }) + + it('should add #build after #components to paths', async () => { + const { tsConfig } = await _generateTypes(mockNuxtWithOptions({ + alias: { + '~': '/my-app', + '@': '/my-app', + 'some-custom-alias': '/my-app/some-alias', + '#build': './build-dir', + '#build/*': './build-dir/*', + '#imports': './imports', + '#components': './components', + }, + })) + + expect(tsConfig.compilerOptions?.paths).toMatchObject({ + '~': [ + '..', + ], + 'some-custom-alias': [ + '../some-alias', + ], + '@': [ + '..', + ], + '#imports': [ + './imports', + ], + '#components': [ + './components', + ], + '#build': [ + './build-dir', + ], + '#build/*': [ + './build-dir/*', + ], + }) + }) }) diff --git a/packages/nuxt/bin/nuxt.mjs b/packages/nuxt/bin/nuxt.mjs index 7faab34846..8c0237f4ee 100755 --- a/packages/nuxt/bin/nuxt.mjs +++ b/packages/nuxt/bin/nuxt.mjs @@ -1,2 +1,2 @@ #!/usr/bin/env node -import 'nuxi/cli' +import '@nuxt/cli/cli' diff --git a/packages/nuxt/build.config.ts b/packages/nuxt/build.config.ts index 1f8a82cf20..4301b165fb 100644 --- a/packages/nuxt/build.config.ts +++ b/packages/nuxt/build.config.ts @@ -22,7 +22,7 @@ export default defineBuildConfig({ }, }, dependencies: [ - 'nuxi', + '@nuxt/cli', 'vue-router', 'ofetch', ], diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 7ed7b6cf80..5d8d666f56 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -64,16 +64,17 @@ "test:attw": "attw --pack" }, "dependencies": { + "@nuxt/cli": "^3.20.0", "@nuxt/devalue": "^2.0.2", "@nuxt/devtools": "^1.7.0", "@nuxt/kit": "workspace:*", "@nuxt/schema": "workspace:*", "@nuxt/telemetry": "^2.6.4", "@nuxt/vite-builder": "workspace:*", - "@unhead/dom": "^1.11.14", - "@unhead/shared": "^1.11.14", - "@unhead/ssr": "^1.11.14", - "@unhead/vue": "^1.11.14", + "@unhead/dom": "^1.11.15", + "@unhead/shared": "^1.11.15", + "@unhead/ssr": "^1.11.15", + "@unhead/vue": "^1.11.15", "@vue/shared": "^3.5.13", "acorn": "8.14.0", "c12": "^2.0.1", @@ -100,11 +101,10 @@ "mlly": "^1.7.3", "nanotar": "^0.1.1", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", - "nuxi": "^3.18.2", "nypm": "^0.4.1", "ofetch": "^1.4.1", "ohash": "^1.1.4", - "pathe": "^2.0.0", + "pathe": "^2.0.1", "perfect-debounce": "^1.0.0", "pkg-types": "^1.3.0", "radix3": "^1.1.2", @@ -118,7 +118,7 @@ "uncrypto": "^0.1.3", "unctx": "^2.4.1", "unenv": "^1.10.0", - "unhead": "^1.11.14", + "unhead": "^1.11.15", "unimport": "^3.14.5", "unplugin": "^2.1.2", "unplugin-vue-router": "^0.10.9", diff --git a/packages/nuxt/src/core/features.ts b/packages/nuxt/src/core/features.ts index 6e22d04bff..d19b4021c0 100644 --- a/packages/nuxt/src/core/features.ts +++ b/packages/nuxt/src/core/features.ts @@ -53,7 +53,7 @@ export function installNuxtModule (name: string, options?: EnsurePackageInstalle installPrompts.add(name) const nuxt = useNuxt() return promptToInstall(name, async () => { - const { runCommand } = await import('nuxi') + const { runCommand } = await import('@nuxt/cli') await runCommand('module', ['add', name, '--cwd', nuxt.options.rootDir]) }, { rootDir: nuxt.options.rootDir, searchPaths: nuxt.options.modulesDir, ...options }) } diff --git a/packages/nuxt/src/core/utils/parse.ts b/packages/nuxt/src/core/utils/parse.ts index c063dbcdfa..0f6f6d8b5e 100644 --- a/packages/nuxt/src/core/utils/parse.ts +++ b/packages/nuxt/src/core/utils/parse.ts @@ -53,12 +53,30 @@ export function withLocations (node: T): WithLocations { return node as WithLocations } +/** + * A function to check whether scope A is a child of scope B. + * @example + * ```ts + * isChildScope('0-1-2', '0-1') // true + * isChildScope('0-1', '0-1') // false + * ``` + * + * @param a the child scope + * @param b the parent scope + * @returns true if scope A is a child of scope B, false otherwise (also when they are the same) + */ +function isChildScope (a: string, b: string) { + return a.startsWith(b) && a.length > b.length +} + abstract class BaseNode { abstract type: string + readonly scope: string node: WithLocations - constructor (node: WithLocations) { + constructor (node: WithLocations, scope: string) { this.node = node + this.scope = scope } /** @@ -72,6 +90,14 @@ abstract class BaseNode { * For instance, for a function parameter, this would be the end of the function declaration. */ abstract get end (): number + + /** + * Check if the node is defined under a specific scope. + * @param scope + */ + isUnderScope (scope: string) { + return isChildScope(this.scope, scope) + } } class IdentifierNode extends BaseNode { @@ -90,8 +116,8 @@ class FunctionParamNode extends BaseNode { type = 'FunctionParam' as const fnNode: WithLocations - constructor (node: WithLocations, fnNode: WithLocations) { - super(node) + constructor (node: WithLocations, scope: string, fnNode: WithLocations) { + super(node, scope) this.fnNode = fnNode } @@ -120,8 +146,8 @@ class VariableNode extends BaseNode { type = 'Variable' as const variableNode: WithLocations - constructor (node: WithLocations, variableNode: WithLocations) { - super(node) + constructor (node: WithLocations, scope: string, variableNode: WithLocations) { + super(node, scope) this.variableNode = variableNode } @@ -138,8 +164,8 @@ class ImportNode extends BaseNode - constructor (node: WithLocations, importNode: WithLocations) { - super(node) + constructor (node: WithLocations, scope: string, importNode: WithLocations) { + super(node, scope) this.importNode = importNode } @@ -156,8 +182,8 @@ class CatchParamNode extends BaseNode { type = 'CatchParam' as const catchNode: WithLocations - constructor (node: WithLocations, catchNode: WithLocations) { - super(node) + constructor (node: WithLocations, scope: string, catchNode: WithLocations) { + super(node, scope) this.catchNode = catchNode } @@ -264,7 +290,7 @@ export class ScopeTracker { const identifiers = getPatternIdentifiers(param) for (const identifier of identifiers) { - this.declareIdentifier(identifier.name, new FunctionParamNode(identifier, fn)) + this.declareIdentifier(identifier.name, new FunctionParamNode(identifier, this.scopeIndexKey, fn)) } } @@ -276,10 +302,10 @@ export class ScopeTracker { this.declareIdentifier( identifier.name, parent.type === 'VariableDeclaration' - ? new VariableNode(identifier, parent) + ? new VariableNode(identifier, this.scopeIndexKey, parent) : parent.type === 'CatchClause' - ? new CatchParamNode(identifier, parent) - : new FunctionParamNode(identifier, parent), + ? new CatchParamNode(identifier, this.scopeIndexKey, parent) + : new FunctionParamNode(identifier, this.scopeIndexKey, parent), ) } } @@ -295,7 +321,7 @@ export class ScopeTracker { case 'FunctionDeclaration': // declare function name for named functions, skip for `export default` if (node.id?.name) { - this.declareIdentifier(node.id.name, new FunctionNode(node)) + this.declareIdentifier(node.id.name, new FunctionNode(node, this.scopeIndexKey)) } this.pushScope() for (const param of node.params) { @@ -309,7 +335,7 @@ export class ScopeTracker { this.pushScope() // can be undefined, for example in class method definitions if (node.id?.name) { - this.declareIdentifier(node.id.name, new FunctionNode(node)) + this.declareIdentifier(node.id.name, new FunctionNode(node, this.scopeIndexKey)) } this.pushScope() @@ -333,7 +359,7 @@ export class ScopeTracker { case 'ClassDeclaration': // declare class name for named classes, skip for `export default` if (node.id?.name) { - this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id))) + this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id), this.scopeIndexKey)) } break @@ -342,13 +368,13 @@ export class ScopeTracker { // e.g. const MyClass = class InternalClassName { // InternalClassName is only available within the class body this.pushScope() if (node.id?.name) { - this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id))) + this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id), this.scopeIndexKey)) } break case 'ImportDeclaration': for (const specifier of node.specifiers) { - this.declareIdentifier(specifier.local.name, new ImportNode(withLocations(specifier), node)) + this.declareIdentifier(specifier.local.name, new ImportNode(withLocations(specifier), this.scopeIndexKey, node)) } break @@ -429,6 +455,26 @@ export class ScopeTracker { return null } + getCurrentScope () { + return this.scopeIndexKey + } + + /** + * Check if the current scope is a child of a specific scope. + * @example + * ```ts + * // current scope is 0-1 + * isCurrentScopeUnder('0') // true + * isCurrentScopeUnder('0-1') // false + * ``` + * + * @param scope the parent scope + * @returns `true` if the current scope is a child of the specified scope, `false` otherwise (also when they are the same) + */ + isCurrentScopeUnder (scope: string) { + return isChildScope(this.scopeIndexKey, scope) + } + /** * Freezes the scope tracker, preventing further declarations. * It also resets the scope index stack to its initial state, so that the scope tracker can be reused. diff --git a/packages/nuxt/src/pages/plugins/page-meta.ts b/packages/nuxt/src/pages/plugins/page-meta.ts index a5972f4e44..bde07df6a3 100644 --- a/packages/nuxt/src/pages/plugins/page-meta.ts +++ b/packages/nuxt/src/pages/plugins/page-meta.ts @@ -228,6 +228,8 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp if (!meta) { return } + const definePageMetaScope = scopeTracker.getCurrentScope() + walk(meta, { scopeTracker, enter (node, parent) { @@ -236,10 +238,24 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp || node.type !== 'Identifier' // checking for `node.type` to narrow down the type ) { return } + const declaration = scopeTracker.getDeclaration(node.name) + if (declaration) { + // check if the declaration was made inside `definePageMeta` and if so, do not process it + // (ensures that we don't hoist local variables in inline middleware, for example) + if ( + declaration.isUnderScope(definePageMetaScope) + // ensures that we compare the correct declaration to the reference + // (when in the same scope, the declaration must come before the reference, otherwise it must be in a parent scope) + && (scopeTracker.isCurrentScopeUnder(declaration.scope) || declaration.start < node.start) + ) { + return + } + } + if (isStaticIdentifier(node.name)) { addImport(node.name) - } else { - processDeclaration(scopeTracker.getDeclaration(node.name)) + } else if (declaration) { + processDeclaration(declaration) } }, }) diff --git a/packages/nuxt/test/page-metadata.test.ts b/packages/nuxt/test/page-metadata.test.ts index 60247fd4e1..ab1c44b46a 100644 --- a/packages/nuxt/test/page-metadata.test.ts +++ b/packages/nuxt/test/page-metadata.test.ts @@ -393,6 +393,188 @@ definePageMeta({ `) }) + it('should not import static identifiers when shadowed in the same scope', () => { + const sfc = ` + + ` + const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' }) + expect(transformPlugin.transform.call({ + parse: (code: string, opts: any = {}) => Parser.parse(code, { + sourceType: 'module', + ecmaVersion: 'latest', + locations: true, + ...opts, + }), + }, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(` + "const __nuxt_page_meta = { + middleware: () => { + const useState = (key) => ({ value: { isLoggedIn: false } }) + const auth = useState('auth') + if (!auth.value.isLoggedIn) { + return navigateTo('/login') + } + }, + } + export default __nuxt_page_meta" + `) + }) + + it('should not import static identifiers when shadowed in parent scope', () => { + const sfc = ` + + ` + const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' }) + expect(transformPlugin.transform.call({ + parse: (code: string, opts: any = {}) => Parser.parse(code, { + sourceType: 'module', + ecmaVersion: 'latest', + locations: true, + ...opts, + }), + }, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(` + "const __nuxt_page_meta = { + middleware: () => { + function isLoggedIn() { + const auth = useState('auth') + return auth.value.isLoggedIn + } + + const useState = (key) => ({ value: { isLoggedIn: false } }) + if (!isLoggedIn()) { + return navigateTo('/login') + } + }, + } + export default __nuxt_page_meta" + `) + }) + + it('should import static identifiers when a shadowed and a non-shadowed one is used', () => { + const sfc = ` + + ` + const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' }) + expect(transformPlugin.transform.call({ + parse: (code: string, opts: any = {}) => Parser.parse(code, { + sourceType: 'module', + ecmaVersion: 'latest', + locations: true, + ...opts, + }), + }, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(` + "import { useState } from '#app/composables/state' + + const __nuxt_page_meta = { + middleware: [ + () => { + const useState = (key) => ({ value: { isLoggedIn: false } }) + const auth = useState('auth') + if (!auth.value.isLoggedIn) { + return navigateTo('/login') + } + }, + () => { + const auth = useState('auth') + if (!auth.value.isLoggedIn) { + return navigateTo('/login') + } + } + ] + } + export default __nuxt_page_meta" + `) + }) + + it('should import static identifiers when a shadowed and a non-shadowed one is used in the same scope', () => { + const sfc = ` + + ` + const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' }) + expect(transformPlugin.transform.call({ + parse: (code: string, opts: any = {}) => Parser.parse(code, { + sourceType: 'module', + ecmaVersion: 'latest', + locations: true, + ...opts, + }), + }, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(` + "import { useState } from '#app/composables/state' + + const __nuxt_page_meta = { + middleware: () => { + const auth1 = useState('auth') + const useState = (key) => ({ value: { isLoggedIn: false } }) + const auth2 = useState('auth') + if (!auth1.value.isLoggedIn || !auth2.value.isLoggedIn) { + return navigateTo('/login') + } + }, + } + export default __nuxt_page_meta" + `) + }) + it('should work with esbuild.keepNames = true', async () => { const sfc = `