mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 13:45:18 +00:00
fix(vite): skip generating keys for locally scoped functions (#20955)
This commit is contained in:
parent
ec72066f91
commit
67f2232014
@ -142,7 +142,7 @@ export default defineUntypedSchema({
|
|||||||
*
|
*
|
||||||
* The key will be unique based on the location of the function being invoked within the file.
|
* The key will be unique based on the location of the function being invoked within the file.
|
||||||
*
|
*
|
||||||
* @type {Array<{ name: string, argumentLength: number }>}
|
* @type {Array<{ name: string, source?: string | RegExp, argumentLength: number }>}
|
||||||
*/
|
*/
|
||||||
keyedComposables: {
|
keyedComposables: {
|
||||||
$resolve: (val) => [
|
$resolve: (val) => [
|
||||||
|
@ -5,15 +5,16 @@ import type { Node } from 'estree-walker'
|
|||||||
import { walk } from 'estree-walker'
|
import { walk } from 'estree-walker'
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import { hash } from 'ohash'
|
import { hash } from 'ohash'
|
||||||
import type { CallExpression } from 'estree'
|
import type { CallExpression, Pattern } from 'estree'
|
||||||
import { parseQuery, parseURL } from 'ufo'
|
import { parseQuery, parseURL } from 'ufo'
|
||||||
import escapeRE from 'escape-string-regexp'
|
import escapeRE from 'escape-string-regexp'
|
||||||
import { findStaticImports, parseStaticImport } from 'mlly'
|
import { findStaticImports, parseStaticImport } from 'mlly'
|
||||||
|
import { matchWithStringOrRegex } from '../utils'
|
||||||
|
|
||||||
export interface ComposableKeysOptions {
|
export interface ComposableKeysOptions {
|
||||||
sourcemap: boolean
|
sourcemap: boolean
|
||||||
rootDir: string
|
rootDir: string
|
||||||
composables: Array<{ name: string, argumentLength: number }>
|
composables: Array<{ name: string, source?: string | RegExp, argumentLength: number }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringTypes = ['Literal', 'TemplateLiteral']
|
const stringTypes = ['Literal', 'TemplateLiteral']
|
||||||
@ -42,21 +43,60 @@ export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptio
|
|||||||
let imports: Set<string> | undefined
|
let imports: Set<string> | undefined
|
||||||
let count = 0
|
let count = 0
|
||||||
const relativeID = isAbsolute(id) ? relative(options.rootDir, id) : id
|
const relativeID = isAbsolute(id) ? relative(options.rootDir, id) : id
|
||||||
walk(this.parse(script, {
|
const { pathname: relativePathname } = parseURL(relativeID)
|
||||||
|
|
||||||
|
const ast = this.parse(script, {
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
ecmaVersion: 'latest'
|
ecmaVersion: 'latest'
|
||||||
}) as Node, {
|
}) as Node
|
||||||
|
|
||||||
|
// To handle variables hoisting we need a pre-pass to collect variable and function declarations with scope info.
|
||||||
|
let scopeTracker = new ScopeTracker()
|
||||||
|
const varCollector = new ScopedVarsCollector()
|
||||||
|
walk(ast, {
|
||||||
enter (_node) {
|
enter (_node) {
|
||||||
|
if (_node.type === 'BlockStatement') {
|
||||||
|
scopeTracker.enterScope()
|
||||||
|
varCollector.refresh(scopeTracker.curScopeKey)
|
||||||
|
} else if (_node.type === 'FunctionDeclaration' && _node.id) {
|
||||||
|
varCollector.addVar(_node.id.name)
|
||||||
|
} else if (_node.type === 'VariableDeclarator') {
|
||||||
|
varCollector.collect(_node.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leave (_node) {
|
||||||
|
if (_node.type === 'BlockStatement') {
|
||||||
|
scopeTracker.leaveScope()
|
||||||
|
varCollector.refresh(scopeTracker.curScopeKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
scopeTracker = new ScopeTracker()
|
||||||
|
walk(ast, {
|
||||||
|
enter (_node) {
|
||||||
|
if (_node.type === 'BlockStatement') {
|
||||||
|
scopeTracker.enterScope()
|
||||||
|
}
|
||||||
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
||||||
const node: CallExpression = _node as CallExpression
|
const node: CallExpression = _node as CallExpression
|
||||||
const name = 'name' in node.callee && node.callee.name
|
const name = 'name' in node.callee && node.callee.name
|
||||||
if (!name || !keyedFunctions.has(name) || node.arguments.length >= maxLength) { return }
|
if (!name || !keyedFunctions.has(name) || node.arguments.length >= maxLength) { return }
|
||||||
|
|
||||||
imports = imports || detectImportNames(script)
|
imports = imports || detectImportNames(script, composableMeta)
|
||||||
if (imports.has(name)) { return }
|
if (imports.has(name)) { return }
|
||||||
|
|
||||||
const meta = composableMeta[name]
|
const meta = composableMeta[name]
|
||||||
|
|
||||||
|
if (varCollector.hasVar(scopeTracker.curScopeKey, name)) {
|
||||||
|
let skip = true
|
||||||
|
if (meta.source) {
|
||||||
|
skip = !matchWithStringOrRegex(relativePathname, meta.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skip) { return }
|
||||||
|
}
|
||||||
|
|
||||||
if (node.arguments.length >= meta.argumentLength) { return }
|
if (node.arguments.length >= meta.argumentLength) { return }
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
@ -82,6 +122,11 @@ export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptio
|
|||||||
codeIndex + (node as any).end - 1,
|
codeIndex + (node as any).end - 1,
|
||||||
(node.arguments.length && !endsWithComma ? ', ' : '') + "'$" + hash(`${relativeID}-${++count}`) + "'"
|
(node.arguments.length && !endsWithComma ? ', ' : '') + "'$" + hash(`${relativeID}-${++count}`) + "'"
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
leave (_node) {
|
||||||
|
if (_node.type === 'BlockStatement') {
|
||||||
|
scopeTracker.leaveScope()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (s.hasChanged()) {
|
if (s.hasChanged()) {
|
||||||
@ -96,22 +141,112 @@ export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptio
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
class ScopeTracker {
|
||||||
|
scopeIndexStack: number[]
|
||||||
|
curScopeKey: string
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
// top level
|
||||||
|
this.scopeIndexStack = [0]
|
||||||
|
this.curScopeKey = '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
getKey () {
|
||||||
|
return this.scopeIndexStack.slice(0, -1).join('-')
|
||||||
|
}
|
||||||
|
|
||||||
|
enterScope () {
|
||||||
|
this.scopeIndexStack.push(0)
|
||||||
|
this.curScopeKey = this.getKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveScope () {
|
||||||
|
this.scopeIndexStack.pop()
|
||||||
|
this.curScopeKey = this.getKey()
|
||||||
|
this.scopeIndexStack[this.scopeIndexStack.length - 1]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScopedVarsCollector {
|
||||||
|
curScopeKey: string
|
||||||
|
all: Map<string, Set<string>>
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this.all = new Map()
|
||||||
|
// top level
|
||||||
|
this.curScopeKey = '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh (scopeKey: string) {
|
||||||
|
this.curScopeKey = scopeKey
|
||||||
|
}
|
||||||
|
|
||||||
|
addVar (name: string) {
|
||||||
|
let vars = this.all.get(this.curScopeKey)
|
||||||
|
if (!vars) {
|
||||||
|
vars = new Set()
|
||||||
|
this.all.set(this.curScopeKey, vars)
|
||||||
|
}
|
||||||
|
vars.add(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasVar (scopeKey: string, name: string) {
|
||||||
|
const indices = scopeKey.split('-').map(Number)
|
||||||
|
for (let i = indices.length; i > 0; i--) {
|
||||||
|
if (this.all.get(indices.slice(0, i).join('-'))?.has(name)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
collect (n: Pattern) {
|
||||||
|
const t = n.type
|
||||||
|
if (t === 'Identifier') {
|
||||||
|
this.addVar(n.name)
|
||||||
|
} else if (t === 'RestElement') {
|
||||||
|
this.collect(n.argument)
|
||||||
|
} else if (t === 'AssignmentPattern') {
|
||||||
|
this.collect(n.left)
|
||||||
|
} else if (t === 'ArrayPattern') {
|
||||||
|
n.elements.forEach(e => e && this.collect(e))
|
||||||
|
} else if (t === 'ObjectPattern') {
|
||||||
|
n.properties.forEach((p) => {
|
||||||
|
if (p.type === 'RestElement') {
|
||||||
|
this.collect(p)
|
||||||
|
} else {
|
||||||
|
this.collect(p.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const NUXT_IMPORT_RE = /nuxt|#app|#imports/
|
const NUXT_IMPORT_RE = /nuxt|#app|#imports/
|
||||||
|
|
||||||
function detectImportNames (code: string) {
|
function detectImportNames (code: string, composableMeta: Record<string, { source?: string | RegExp }>) {
|
||||||
const imports = findStaticImports(code)
|
const imports = findStaticImports(code)
|
||||||
const names = new Set<string>()
|
const names = new Set<string>()
|
||||||
for (const i of imports) {
|
for (const i of imports) {
|
||||||
if (NUXT_IMPORT_RE.test(i.specifier)) { continue }
|
if (NUXT_IMPORT_RE.test(i.specifier)) { continue }
|
||||||
const { namedImports, defaultImport, namespacedImport } = parseStaticImport(i)
|
|
||||||
for (const name in namedImports || {}) {
|
function addName (name: string) {
|
||||||
|
const source = composableMeta[name]?.source
|
||||||
|
if (source && matchWithStringOrRegex(i.specifier, source)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
names.add(namedImports![name])
|
names.add(namedImports![name])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { namedImports, defaultImport, namespacedImport } = parseStaticImport(i)
|
||||||
|
for (const name in namedImports || {}) {
|
||||||
|
addName(namedImports![name])
|
||||||
|
}
|
||||||
if (defaultImport) {
|
if (defaultImport) {
|
||||||
names.add(defaultImport)
|
addName(defaultImport)
|
||||||
}
|
}
|
||||||
if (namespacedImport) {
|
if (namespacedImport) {
|
||||||
names.add(namespacedImport)
|
addName(namespacedImport)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return names
|
return names
|
||||||
|
@ -43,3 +43,13 @@ export async function isDirectory (path: string) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function matchWithStringOrRegex (value: string, matcher: string | RegExp) {
|
||||||
|
if (typeof matcher === 'string') {
|
||||||
|
return value === matcher
|
||||||
|
} else if (matcher instanceof RegExp) {
|
||||||
|
return matcher.test(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -1086,6 +1086,12 @@ describe('automatically keyed composables', () => {
|
|||||||
it('should match server-generated keys', async () => {
|
it('should match server-generated keys', async () => {
|
||||||
await expectNoClientErrors('/keyed-composables')
|
await expectNoClientErrors('/keyed-composables')
|
||||||
})
|
})
|
||||||
|
it('should not automatically generate keys', async () => {
|
||||||
|
await expectNoClientErrors('/keyed-composables/local')
|
||||||
|
const html = await $fetch('/keyed-composables/local')
|
||||||
|
expect(html).toContain('true')
|
||||||
|
expect(html).not.toContain('false')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe.skipIf(isDev() || isWebpack)('inlining component styles', () => {
|
describe.skipIf(isDev() || isWebpack)('inlining component styles', () => {
|
||||||
|
3
test/fixtures/basic/nuxt.config.ts
vendored
3
test/fixtures/basic/nuxt.config.ts
vendored
@ -62,7 +62,8 @@ export default defineNuxtConfig({
|
|||||||
optimization: {
|
optimization: {
|
||||||
keyedComposables: [
|
keyedComposables: [
|
||||||
{
|
{
|
||||||
name: 'useKeyedComposable',
|
name: 'useCustomKeyedComposable',
|
||||||
|
source: 'pages/keyed-composables/index.vue',
|
||||||
argumentLength: 1
|
argumentLength: 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -33,10 +33,10 @@ const useLocalLazyFetch = () => useLazyFetch(() => '/api/counter')
|
|||||||
const { data: useLazyFetchTest1 } = await useLocalLazyFetch()
|
const { data: useLazyFetchTest1 } = await useLocalLazyFetch()
|
||||||
const { data: useLazyFetchTest2 } = await useLocalLazyFetch()
|
const { data: useLazyFetchTest2 } = await useLocalLazyFetch()
|
||||||
|
|
||||||
const useKeyedComposable = (arg?: string) => arg
|
const useCustomKeyedComposable = (arg?: string) => arg
|
||||||
const useLocalKeyedComposable = () => useKeyedComposable()
|
const useLocalCustomKeyedComposable = () => useCustomKeyedComposable()
|
||||||
const useMyAsyncDataTest1 = useLocalKeyedComposable()
|
const useMyAsyncDataTest1 = useLocalCustomKeyedComposable()
|
||||||
const useMyAsyncDataTest2 = useLocalKeyedComposable()
|
const useMyAsyncDataTest2 = useLocalCustomKeyedComposable()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
59
test/fixtures/basic/pages/keyed-composables/local.vue
vendored
Normal file
59
test/fixtures/basic/pages/keyed-composables/local.vue
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
function localScopedComposables () {
|
||||||
|
const _assert = (key?: string) => key ?? 'was not keyed'
|
||||||
|
|
||||||
|
function basic () {
|
||||||
|
function useState (key?: string) {
|
||||||
|
return _assert(key)
|
||||||
|
}
|
||||||
|
const useAsyncData = _assert
|
||||||
|
|
||||||
|
return [useState(), useAsyncData()]
|
||||||
|
}
|
||||||
|
|
||||||
|
function hoisting () {
|
||||||
|
return [useState()]
|
||||||
|
|
||||||
|
function useState (key?: string) {
|
||||||
|
return _assert(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function complex () {
|
||||||
|
const [useState] = [_assert]
|
||||||
|
const { a: useAsyncData } = {
|
||||||
|
a: _assert
|
||||||
|
}
|
||||||
|
const [_, { b: useLazyAsyncData }] = [null, {
|
||||||
|
b: _assert
|
||||||
|
}]
|
||||||
|
|
||||||
|
return [useState(), useAsyncData(), useLazyAsyncData()]
|
||||||
|
}
|
||||||
|
|
||||||
|
function deeperScope () {
|
||||||
|
const useState = _assert
|
||||||
|
|
||||||
|
return [(function () {
|
||||||
|
return useState()
|
||||||
|
})()]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...basic(), ...hoisting(), ...complex(), ...deeperScope()]
|
||||||
|
}
|
||||||
|
|
||||||
|
const skippedLocalScopedComposables = localScopedComposables().every(res => res === 'was not keyed')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
{{ skippedLocalScopedComposables }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
body {
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in New Issue
Block a user