Merge branch 'main' into feat/nuxt_async_context

This commit is contained in:
Julien Huang 2025-01-11 17:44:32 +01:00
commit 97af928232
24 changed files with 2906 additions and 1808 deletions

View File

@ -1,4 +1,4 @@
FROM node:lts@sha256:0e910f435308c36ea60b4cfd7b80208044d77a074d16b768a81901ce938a62dc FROM node:lts@sha256:99981c3d1aac0d98cd9f03f74b92dddf30f30ffb0b34e6df8bd96283f62f12c6
RUN apt-get update && \ 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 && \ 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 && \

View File

@ -60,7 +60,7 @@ jobs:
run: pnpm test:attw run: pnpm test:attw
- name: Cache dist - name: Cache dist
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
retention-days: 3 retention-days: 3
name: dist name: dist

View File

@ -59,7 +59,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.
- name: "Upload artifact" - 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() if: github.repository == 'nuxt/nuxt' && success()
with: with:
name: SARIF file name: SARIF file

View File

@ -147,7 +147,7 @@ If you have not fetched data on the server (for example, with `server: false`),
```ts [Signature] ```ts [Signature]
function useFetch<DataT, ErrorT>( function useFetch<DataT, ErrorT>(
url: string | Request | Ref<string | Request> | (() => string) | Request, url: string | Request | Ref<string | Request> | (() => string | Request),
options?: UseFetchOptions<DataT> options?: UseFetchOptions<DataT>
): Promise<AsyncData<DataT, ErrorT>> ): Promise<AsyncData<DataT, ErrorT>>

View File

@ -63,7 +63,7 @@ Each active version has its own nightly releases which are generated automatical
Release | | Initial release | End Of Life | Docs Release | | Initial release | End Of Life | Docs
----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|------- ----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|-------
**4.x** (scheduled) | | 2024 Q3 | | &nbsp; **4.x** (scheduled) | | approximately 1 month after release of nitro v3 | | &nbsp;
**3.x** (stable) | <a href="https://npmjs.com/package/nuxt"><img alt="Nuxt latest 3.x version" src="https://flat.badgen.net/npm/v/nuxt?label=" class="not-prose"></a> | 2022-11-16 | TBA | [nuxt.com](/docs) **3.x** (stable) | <a href="https://npmjs.com/package/nuxt"><img alt="Nuxt latest 3.x version" src="https://flat.badgen.net/npm/v/nuxt?label=" class="not-prose"></a> | 2022-11-16 | TBA | [nuxt.com](/docs)
**2.x** (unsupported) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 2.x version" src="https://flat.badgen.net/npm/v/nuxt/2x?label=" class="not-prose"></a> | 2018-09-21 | 2024-06-30 | [v2.nuxt.com](https://v2.nuxt.com/docs) **2.x** (unsupported) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 2.x version" src="https://flat.badgen.net/npm/v/nuxt/2x?label=" class="not-prose"></a> | 2018-09-21 | 2024-06-30 | [v2.nuxt.com](https://v2.nuxt.com/docs)
**1.x** (unsupported) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 1.x version" src="https://flat.badgen.net/npm/v/nuxt/1x?label=" class="not-prose"></a> | 2018-01-08 | 2019-09-21 | &nbsp; **1.x** (unsupported) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 1.x version" src="https://flat.badgen.net/npm/v/nuxt/1x?label=" class="not-prose"></a> | 2018-01-08 | 2019-09-21 | &nbsp;

View File

@ -40,11 +40,11 @@
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@types/node": "22.10.5", "@types/node": "22.10.5",
"@unhead/dom": "1.11.14", "@unhead/dom": "1.11.15",
"@unhead/schema": "1.11.14", "@unhead/schema": "1.11.15",
"@unhead/shared": "1.11.14", "@unhead/shared": "1.11.15",
"@unhead/ssr": "1.11.14", "@unhead/ssr": "1.11.15",
"@unhead/vue": "1.11.14", "@unhead/vue": "1.11.15",
"@vue/compiler-core": "3.5.13", "@vue/compiler-core": "3.5.13",
"@vue/compiler-dom": "3.5.13", "@vue/compiler-dom": "3.5.13",
"@vue/shared": "3.5.13", "@vue/shared": "3.5.13",
@ -56,19 +56,19 @@
"nuxt": "workspace:*", "nuxt": "workspace:*",
"ohash": "1.1.4", "ohash": "1.1.4",
"postcss": "8.4.49", "postcss": "8.4.49",
"rollup": "4.30.0", "rollup": "4.30.1",
"send": ">=1.1.0", "send": ">=1.1.0",
"typescript": "5.7.2", "typescript": "5.7.3",
"ufo": "1.5.4", "ufo": "1.5.4",
"unbuild": "3.2.0", "unbuild": "3.2.0",
"unhead": "1.11.14", "unhead": "1.11.15",
"unimport": "3.14.5", "unimport": "3.14.5",
"vite": "6.0.7", "vite": "6.0.7",
"vue": "3.5.13" "vue": "3.5.13"
}, },
"devDependencies": { "devDependencies": {
"@arethetypeswrong/cli": "0.17.2", "@arethetypeswrong/cli": "0.17.2",
"@nuxt/eslint-config": "0.7.4", "@nuxt/eslint-config": "0.7.5",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/rspack-builder": "workspace:*", "@nuxt/rspack-builder": "workspace:*",
"@nuxt/test-utils": "3.15.1", "@nuxt/test-utils": "3.15.1",
@ -76,8 +76,8 @@
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/node": "22.10.5", "@types/node": "22.10.5",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"@unhead/schema": "1.11.14", "@unhead/schema": "1.11.15",
"@unhead/vue": "1.11.14", "@unhead/vue": "1.11.15",
"@vitest/coverage-v8": "2.1.8", "@vitest/coverage-v8": "2.1.8",
"@vue/test-utils": "2.4.6", "@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
@ -90,27 +90,27 @@
"eslint": "9.17.0", "eslint": "9.17.0",
"eslint-plugin-no-only-tests": "3.3.0", "eslint-plugin-no-only-tests": "3.3.0",
"eslint-plugin-perfectionist": "4.6.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", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"happy-dom": "16.3.0", "happy-dom": "16.5.3",
"installed-check": "9.3.0", "installed-check": "9.3.0",
"jiti": "2.4.2", "jiti": "2.4.2",
"knip": "5.41.1", "knip": "5.42.0",
"markdownlint-cli": "0.43.0", "markdownlint-cli": "0.43.0",
"memfs": "4.15.3", "memfs": "4.17.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "3.18.2", "nuxi": "3.18.2",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"nuxt-content-twoslash": "0.1.2", "nuxt-content-twoslash": "0.1.2",
"ofetch": "1.4.1", "ofetch": "1.4.1",
"pathe": "2.0.0", "pathe": "2.0.1",
"playwright-core": "1.49.1", "playwright-core": "1.49.1",
"semver": "7.6.3", "semver": "7.6.3",
"sherif": "1.1.1", "sherif": "1.1.1",
"std-env": "3.8.0", "std-env": "3.8.0",
"tinyexec": "0.3.2", "tinyexec": "0.3.2",
"tinyglobby": "0.2.10", "tinyglobby": "0.2.10",
"typescript": "5.7.2", "typescript": "5.7.3",
"ufo": "1.5.4", "ufo": "1.5.4",
"vitest": "2.1.8", "vitest": "2.1.8",
"vitest-environment-nuxt": "1.0.1", "vitest-environment-nuxt": "1.0.1",

View File

@ -39,7 +39,7 @@
"klona": "^2.0.6", "klona": "^2.0.6",
"mlly": "^1.7.3", "mlly": "^1.7.3",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^2.0.0", "pathe": "^2.0.1",
"pkg-types": "^1.3.0", "pkg-types": "^1.3.0",
"scule": "^1.3.0", "scule": "^1.3.0",
"semver": "^7.6.3", "semver": "^7.6.3",

View File

@ -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.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))] 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() await writeFile()
} }
function sortTsPaths (paths: Record<string, string[]>) {
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<string, string>) { function renderAttrs (obj: Record<string, string>) {
const attrs: string[] = [] const attrs: string[] = []
for (const key in obj) { for (const key in obj) {

View File

@ -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/*',
],
})
})
}) })

View File

@ -1,2 +1,2 @@
#!/usr/bin/env node #!/usr/bin/env node
import 'nuxi/cli' import '@nuxt/cli/cli'

View File

@ -22,7 +22,7 @@ export default defineBuildConfig({
}, },
}, },
dependencies: [ dependencies: [
'nuxi', '@nuxt/cli',
'vue-router', 'vue-router',
'ofetch', 'ofetch',
], ],

View File

@ -64,16 +64,17 @@
"test:attw": "attw --pack" "test:attw": "attw --pack"
}, },
"dependencies": { "dependencies": {
"@nuxt/cli": "^3.20.0",
"@nuxt/devalue": "^2.0.2", "@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.7.0", "@nuxt/devtools": "^1.7.0",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.6.4", "@nuxt/telemetry": "^2.6.4",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.11.14", "@unhead/dom": "^1.11.15",
"@unhead/shared": "^1.11.14", "@unhead/shared": "^1.11.15",
"@unhead/ssr": "^1.11.14", "@unhead/ssr": "^1.11.15",
"@unhead/vue": "^1.11.14", "@unhead/vue": "^1.11.15",
"@vue/shared": "^3.5.13", "@vue/shared": "^3.5.13",
"acorn": "8.14.0", "acorn": "8.14.0",
"c12": "^2.0.1", "c12": "^2.0.1",
@ -100,11 +101,10 @@
"mlly": "^1.7.3", "mlly": "^1.7.3",
"nanotar": "^0.1.1", "nanotar": "^0.1.1",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "^3.18.2",
"nypm": "^0.4.1", "nypm": "^0.4.1",
"ofetch": "^1.4.1", "ofetch": "^1.4.1",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^2.0.0", "pathe": "^2.0.1",
"perfect-debounce": "^1.0.0", "perfect-debounce": "^1.0.0",
"pkg-types": "^1.3.0", "pkg-types": "^1.3.0",
"radix3": "^1.1.2", "radix3": "^1.1.2",
@ -118,7 +118,7 @@
"uncrypto": "^0.1.3", "uncrypto": "^0.1.3",
"unctx": "^2.4.1", "unctx": "^2.4.1",
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unhead": "^1.11.14", "unhead": "^1.11.15",
"unimport": "^3.14.5", "unimport": "^3.14.5",
"unplugin": "^2.1.2", "unplugin": "^2.1.2",
"unplugin-vue-router": "^0.10.9", "unplugin-vue-router": "^0.10.9",

View File

@ -53,7 +53,7 @@ export function installNuxtModule (name: string, options?: EnsurePackageInstalle
installPrompts.add(name) installPrompts.add(name)
const nuxt = useNuxt() const nuxt = useNuxt()
return promptToInstall(name, async () => { return promptToInstall(name, async () => {
const { runCommand } = await import('nuxi') const { runCommand } = await import('@nuxt/cli')
await runCommand('module', ['add', name, '--cwd', nuxt.options.rootDir]) await runCommand('module', ['add', name, '--cwd', nuxt.options.rootDir])
}, { rootDir: nuxt.options.rootDir, searchPaths: nuxt.options.modulesDir, ...options }) }, { rootDir: nuxt.options.rootDir, searchPaths: nuxt.options.modulesDir, ...options })
} }

View File

@ -53,12 +53,30 @@ export function withLocations<T> (node: T): WithLocations<T> {
return node as WithLocations<T> return node as WithLocations<T>
} }
/**
* 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<T extends Node = Node> { abstract class BaseNode<T extends Node = Node> {
abstract type: string abstract type: string
readonly scope: string
node: WithLocations<T> node: WithLocations<T>
constructor (node: WithLocations<T>) { constructor (node: WithLocations<T>, scope: string) {
this.node = node this.node = node
this.scope = scope
} }
/** /**
@ -72,6 +90,14 @@ abstract class BaseNode<T extends Node = Node> {
* For instance, for a function parameter, this would be the end of the function declaration. * For instance, for a function parameter, this would be the end of the function declaration.
*/ */
abstract get end (): number 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<Identifier> { class IdentifierNode extends BaseNode<Identifier> {
@ -90,8 +116,8 @@ class FunctionParamNode extends BaseNode {
type = 'FunctionParam' as const type = 'FunctionParam' as const
fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression> fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>
constructor (node: WithLocations<Node>, fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>) { constructor (node: WithLocations<Node>, scope: string, fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>) {
super(node) super(node, scope)
this.fnNode = fnNode this.fnNode = fnNode
} }
@ -120,8 +146,8 @@ class VariableNode extends BaseNode<Identifier> {
type = 'Variable' as const type = 'Variable' as const
variableNode: WithLocations<VariableDeclaration> variableNode: WithLocations<VariableDeclaration>
constructor (node: WithLocations<Identifier>, variableNode: WithLocations<VariableDeclaration>) { constructor (node: WithLocations<Identifier>, scope: string, variableNode: WithLocations<VariableDeclaration>) {
super(node) super(node, scope)
this.variableNode = variableNode this.variableNode = variableNode
} }
@ -138,8 +164,8 @@ class ImportNode extends BaseNode<ImportSpecifier | ImportDefaultSpecifier | Imp
type = 'Import' as const type = 'Import' as const
importNode: WithLocations<Node> importNode: WithLocations<Node>
constructor (node: WithLocations<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>, importNode: WithLocations<Node>) { constructor (node: WithLocations<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>, scope: string, importNode: WithLocations<Node>) {
super(node) super(node, scope)
this.importNode = importNode this.importNode = importNode
} }
@ -156,8 +182,8 @@ class CatchParamNode extends BaseNode {
type = 'CatchParam' as const type = 'CatchParam' as const
catchNode: WithLocations<CatchClause> catchNode: WithLocations<CatchClause>
constructor (node: WithLocations<Node>, catchNode: WithLocations<CatchClause>) { constructor (node: WithLocations<Node>, scope: string, catchNode: WithLocations<CatchClause>) {
super(node) super(node, scope)
this.catchNode = catchNode this.catchNode = catchNode
} }
@ -264,7 +290,7 @@ export class ScopeTracker {
const identifiers = getPatternIdentifiers(param) const identifiers = getPatternIdentifiers(param)
for (const identifier of identifiers) { 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( this.declareIdentifier(
identifier.name, identifier.name,
parent.type === 'VariableDeclaration' parent.type === 'VariableDeclaration'
? new VariableNode(identifier, parent) ? new VariableNode(identifier, this.scopeIndexKey, parent)
: parent.type === 'CatchClause' : parent.type === 'CatchClause'
? new CatchParamNode(identifier, parent) ? new CatchParamNode(identifier, this.scopeIndexKey, parent)
: new FunctionParamNode(identifier, parent), : new FunctionParamNode(identifier, this.scopeIndexKey, parent),
) )
} }
} }
@ -295,7 +321,7 @@ export class ScopeTracker {
case 'FunctionDeclaration': case 'FunctionDeclaration':
// declare function name for named functions, skip for `export default` // declare function name for named functions, skip for `export default`
if (node.id?.name) { if (node.id?.name) {
this.declareIdentifier(node.id.name, new FunctionNode(node)) this.declareIdentifier(node.id.name, new FunctionNode(node, this.scopeIndexKey))
} }
this.pushScope() this.pushScope()
for (const param of node.params) { for (const param of node.params) {
@ -309,7 +335,7 @@ export class ScopeTracker {
this.pushScope() this.pushScope()
// can be undefined, for example in class method definitions // can be undefined, for example in class method definitions
if (node.id?.name) { if (node.id?.name) {
this.declareIdentifier(node.id.name, new FunctionNode(node)) this.declareIdentifier(node.id.name, new FunctionNode(node, this.scopeIndexKey))
} }
this.pushScope() this.pushScope()
@ -333,7 +359,7 @@ export class ScopeTracker {
case 'ClassDeclaration': case 'ClassDeclaration':
// declare class name for named classes, skip for `export default` // declare class name for named classes, skip for `export default`
if (node.id?.name) { 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 break
@ -342,13 +368,13 @@ export class ScopeTracker {
// e.g. const MyClass = class InternalClassName { // InternalClassName is only available within the class body // e.g. const MyClass = class InternalClassName { // InternalClassName is only available within the class body
this.pushScope() this.pushScope()
if (node.id?.name) { 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 break
case 'ImportDeclaration': case 'ImportDeclaration':
for (const specifier of node.specifiers) { 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 break
@ -429,6 +455,26 @@ export class ScopeTracker {
return null 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. * 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. * It also resets the scope index stack to its initial state, so that the scope tracker can be reused.

View File

@ -228,6 +228,8 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
if (!meta) { return } if (!meta) { return }
const definePageMetaScope = scopeTracker.getCurrentScope()
walk(meta, { walk(meta, {
scopeTracker, scopeTracker,
enter (node, parent) { 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 || node.type !== 'Identifier' // checking for `node.type` to narrow down the type
) { return } ) { 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)) { if (isStaticIdentifier(node.name)) {
addImport(node.name) addImport(node.name)
} else { } else if (declaration) {
processDeclaration(scopeTracker.getDeclaration(node.name)) processDeclaration(declaration)
} }
}, },
}) })

View File

@ -393,6 +393,188 @@ definePageMeta({
`) `)
}) })
it('should not import static identifiers when shadowed in the same scope', () => {
const sfc = `
<script setup lang="ts">
import { useState } from '#app/composables/state'
definePageMeta({
middleware: () => {
const useState = (key) => ({ value: { isLoggedIn: false } })
const auth = useState('auth')
if (!auth.value.isLoggedIn) {
return navigateTo('/login')
}
},
})
</script>
`
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 = `
<script setup lang="ts">
import { useState } from '#app/composables/state'
definePageMeta({
middleware: () => {
function isLoggedIn() {
const auth = useState('auth')
return auth.value.isLoggedIn
}
const useState = (key) => ({ value: { isLoggedIn: false } })
if (!isLoggedIn()) {
return navigateTo('/login')
}
},
})
</script>
`
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 = `
<script setup lang="ts">
import { useState } from '#app/composables/state'
definePageMeta({
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')
}
}
]
})
</script>
`
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 = `
<script setup lang="ts">
import { useState } from '#app/composables/state'
definePageMeta({
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')
}
},
})
</script>
`
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 () => { it('should work with esbuild.keepNames = true', async () => {
const sfc = ` const sfc = `
<script setup lang="ts"> <script setup lang="ts">
@ -516,7 +698,12 @@ definePageMeta({
test () {} test () {}
} }
console.log(hoisted.value) const someFunction = () => {
const someValue = 'someValue'
console.log(someValue)
}
console.log(hoisted.value, val)
}, },
], ],
validate: (route) => { validate: (route) => {
@ -564,7 +751,12 @@ const hoisted = ref('hoisted')
test () {} test () {}
} }
console.log(hoisted.value) const someFunction = () => {
const someValue = 'someValue'
console.log(someValue)
}
console.log(hoisted.value, val)
}, },
], ],
validate: (route) => { validate: (route) => {

View File

@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest' import { assert, describe, expect, it } from 'vitest'
import { getUndeclaredIdentifiersInFunction, parseAndWalk } from '../src/core/utils/parse' import { getUndeclaredIdentifiersInFunction, parseAndWalk } from '../src/core/utils/parse'
import { TestScopeTracker } from './fixture/scope-tracker' import { TestScopeTracker } from './fixture/scope-tracker'
@ -667,4 +667,87 @@ describe('parsing', () => {
expect(processedFunctions).toBe(5) expect(processedFunctions).toBe(5)
}) })
it ('should correctly compare identifiers defined in different scopes', () => {
const code = `
// ""
const a = 1
// ""
const func = () => {
// "0-0"
const b = 2
// "0-0"
function foo() {
// "0-0-0-0"
const c = 3
}
}
// ""
const func2 = () => {
// "1-0"
const d = 2
// "1-0"
function bar() {
// "1-0-0-0"
const e = 3
}
}
// ""
const f = 4
`
const scopeTracker = new TestScopeTracker({
keepExitedScopes: true,
})
parseAndWalk(code, filename, {
scopeTracker,
})
const a = scopeTracker.getDeclarationFromScope('a', '')
const func = scopeTracker.getDeclarationFromScope('func', '')
const foo = scopeTracker.getDeclarationFromScope('foo', '0-0')
const b = scopeTracker.getDeclarationFromScope('b', '0-0')
const c = scopeTracker.getDeclarationFromScope('c', '0-0-0-0')
const func2 = scopeTracker.getDeclarationFromScope('func2', '')
const bar = scopeTracker.getDeclarationFromScope('bar', '1-0')
const d = scopeTracker.getDeclarationFromScope('d', '1-0')
const e = scopeTracker.getDeclarationFromScope('e', '1-0-0-0')
const f = scopeTracker.getDeclarationFromScope('f', '')
assert(a && func && foo && b && c && func2 && bar && d && e && f, 'All declarations should be found')
// identifiers in the same scope should be equal
expect(f.isUnderScope(a.scope)).toBe(false)
expect(func.isUnderScope(a.scope)).toBe(false)
expect(d.isUnderScope(bar.scope)).toBe(false)
// identifiers in deeper scopes should be under the scope of the parent scope
expect(b.isUnderScope(a.scope)).toBe(true)
expect(b.isUnderScope(func.scope)).toBe(true)
expect(c.isUnderScope(a.scope)).toBe(true)
expect(c.isUnderScope(b.scope)).toBe(true)
expect(d.isUnderScope(a.scope)).toBe(true)
expect(d.isUnderScope(func2.scope)).toBe(true)
expect(e.isUnderScope(a.scope)).toBe(true)
expect(e.isUnderScope(d.scope)).toBe(true)
// identifiers in parent scope should not be under the scope of the children
expect(a.isUnderScope(b.scope)).toBe(false)
expect(a.isUnderScope(c.scope)).toBe(false)
expect(a.isUnderScope(d.scope)).toBe(false)
expect(a.isUnderScope(e.scope)).toBe(false)
expect(b.isUnderScope(c.scope)).toBe(false)
// identifiers in parallel scopes should not influence each other
expect(d.isUnderScope(b.scope)).toBe(false)
expect(e.isUnderScope(b.scope)).toBe(false)
expect(b.isUnderScope(d.scope)).toBe(false)
expect(c.isUnderScope(e.scope)).toBe(false)
})
}) })

View File

@ -47,9 +47,9 @@
"jiti": "^2.4.2", "jiti": "^2.4.2",
"knitwork": "^1.2.0", "knitwork": "^1.2.0",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"memfs": "^4.15.3", "memfs": "^4.17.0",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^2.0.0", "pathe": "^2.0.1",
"pify": "^6.1.0", "pify": "^6.1.0",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
@ -75,7 +75,7 @@
"@types/pify": "5.0.4", "@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0", "@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9", "@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.30.0", "rollup": "4.30.1",
"unbuild": "3.2.0", "unbuild": "3.2.0",
"vue": "3.5.13" "vue": "3.5.13"
}, },

View File

@ -37,7 +37,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/pug": "2.0.10", "@types/pug": "2.0.10",
"@unhead/schema": "1.11.14", "@unhead/schema": "1.11.15",
"@vitejs/plugin-vue": "5.2.1", "@vitejs/plugin-vue": "5.2.1",
"@vitejs/plugin-vue-jsx": "4.1.1", "@vitejs/plugin-vue-jsx": "4.1.1",
"@vue/compiler-core": "3.5.13", "@vue/compiler-core": "3.5.13",
@ -70,7 +70,7 @@
"dependencies": { "dependencies": {
"consola": "^3.3.3", "consola": "^3.3.3",
"defu": "^6.1.4", "defu": "^6.1.4",
"pathe": "^2.0.0", "pathe": "^2.0.1",
"std-env": "^3.8.0" "std-env": "^3.8.0"
}, },
"engines": { "engines": {

View File

@ -17,19 +17,19 @@
"prerender": "pnpm build && jiti ./lib/prerender" "prerender": "pnpm build && jiti ./lib/prerender"
}, },
"devDependencies": { "devDependencies": {
"@unocss/reset": "0.65.3", "@unocss/reset": "65.4.0",
"beasties": "0.2.0", "beasties": "0.2.0",
"html-validate": "9.1.1", "html-validate": "9.1.3",
"htmlnano": "2.1.1", "htmlnano": "2.1.1",
"jiti": "2.4.2", "jiti": "2.4.2",
"knitwork": "1.2.0", "knitwork": "1.2.0",
"pathe": "2.0.0", "pathe": "2.0.1",
"prettier": "3.4.2", "prettier": "3.4.2",
"scule": "1.3.0", "scule": "1.3.0",
"svgo": "3.3.2", "svgo": "3.3.2",
"tinyexec": "0.3.2", "tinyexec": "0.3.2",
"tinyglobby": "0.2.10", "tinyglobby": "0.2.10",
"unocss": "0.65.3", "unocss": "65.4.0",
"vite": "6.0.7" "vite": "6.0.7"
}, },
"engines": { "engines": {

View File

@ -26,7 +26,7 @@
}, },
"devDependencies": { "devDependencies": {
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"rollup": "4.30.0", "rollup": "4.30.1",
"unbuild": "3.2.0", "unbuild": "3.2.0",
"vue": "3.5.13" "vue": "3.5.13"
}, },
@ -48,7 +48,7 @@
"knitwork": "^1.2.0", "knitwork": "^1.2.0",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"mlly": "^1.7.3", "mlly": "^1.7.3",
"pathe": "^2.0.0", "pathe": "^2.0.1",
"pkg-types": "^1.3.0", "pkg-types": "^1.3.0",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"rollup-plugin-visualizer": "^5.13.1", "rollup-plugin-visualizer": "^5.13.1",

View File

@ -46,10 +46,10 @@
"jiti": "^2.4.2", "jiti": "^2.4.2",
"knitwork": "^1.2.0", "knitwork": "^1.2.0",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"memfs": "^4.15.3", "memfs": "^4.17.0",
"mini-css-extract-plugin": "^2.9.2", "mini-css-extract-plugin": "^2.9.2",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^2.0.0", "pathe": "^2.0.1",
"pify": "^6.1.0", "pify": "^6.1.0",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
@ -77,7 +77,7 @@
"@types/pify": "5.0.4", "@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0", "@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9", "@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.30.0", "rollup": "4.30.1",
"unbuild": "3.2.0", "unbuild": "3.2.0",
"vue": "3.5.13" "vue": "3.5.13"
}, },

File diff suppressed because it is too large Load Diff

View File

@ -61,7 +61,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"210k"`) expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"210k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir) const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1396k"`) expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1398k"`)
const packages = modules.files const packages = modules.files
.filter(m => m.endsWith('package.json')) .filter(m => m.endsWith('package.json'))
@ -86,6 +86,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
"entities", "entities",
"estree-walker", "estree-walker",
"hookable", "hookable",
"packrup",
"source-map-js", "source-map-js",
"ufo", "ufo",
"unhead", "unhead",
@ -102,7 +103,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"561k"`) expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"561k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir) const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"94.4k"`) expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"96.4k"`)
const packages = modules.files const packages = modules.files
.filter(m => m.endsWith('package.json')) .filter(m => m.endsWith('package.json'))
@ -116,6 +117,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
"db0", "db0",
"devalue", "devalue",
"hookable", "hookable",
"packrup",
"unhead", "unhead",
] ]
`) `)
@ -128,7 +130,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"303k"`) expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"303k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir) const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1396k"`) expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1398k"`)
const packages = modules.files const packages = modules.files
.filter(m => m.endsWith('package.json')) .filter(m => m.endsWith('package.json'))
@ -153,6 +155,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
"entities", "entities",
"estree-walker", "estree-walker",
"hookable", "hookable",
"packrup",
"source-map-js", "source-map-js",
"ufo", "ufo",
"unhead", "unhead",