fix(compiler-sfc): rewrite default export with AST analysis instead of regex (#7068)

closes #7038
closes #7041
closes #7078
This commit is contained in:
三咲智子 Kevin Deng 2023-03-28 11:54:22 +08:00 committed by GitHub
parent 7def8b15b8
commit 701b95ff3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 151 additions and 91 deletions

View File

@ -2,8 +2,9 @@ import { rewriteDefault } from '../src'
describe('compiler sfc: rewriteDefault', () => {
test('without export default', () => {
expect(rewriteDefault(`export a = {}`, 'script')).toMatchInlineSnapshot(`
"export a = {}
expect(rewriteDefault(`export const a = {}`, 'script'))
.toMatchInlineSnapshot(`
"export const a = {}
const script = {}"
`)
})
@ -14,6 +15,14 @@ describe('compiler sfc: rewriteDefault', () => {
).toMatchInlineSnapshot(`"const script = {}"`)
})
test('rewrite variable value default', () => {
expect(rewriteDefault(`export const foo = 'default'`, 'script'))
.toMatchInlineSnapshot(`
"export const foo = 'default'
const script = {}"
`)
})
test('rewrite export named default', () => {
expect(
rewriteDefault(
@ -36,6 +45,18 @@ describe('compiler sfc: rewriteDefault', () => {
export { a as b, a as c}
const script = a"
`)
expect(
rewriteDefault(
`const a = 1 \n export { a as b } \n export { a as default, a as c }`,
'script'
)
).toMatchInlineSnapshot(`
"const a = 1
export { a as b }
export { a as c }
const script = a"
`)
})
test('w/ comments', async () => {
@ -52,7 +73,7 @@ describe('compiler sfc: rewriteDefault', () => {
).toMatchInlineSnapshot(`
"let App = {}
export {
}
const _sfc_main = App"
`)
@ -96,25 +117,25 @@ describe('compiler sfc: rewriteDefault', () => {
expect(
rewriteDefault(`export { default, foo } from './index.js'`, 'script')
).toMatchInlineSnapshot(`
"import { default as __VUE_DEFAULT__ } from './index.js'
export { foo } from './index.js'
const script = __VUE_DEFAULT__"
"import { default as __VUE_DEFAULT__ } from './index.js'
export { foo } from './index.js'
const script = __VUE_DEFAULT__"
`)
expect(
rewriteDefault(`export { default , foo } from './index.js'`, 'script')
).toMatchInlineSnapshot(`
"import { default as __VUE_DEFAULT__ } from './index.js'
export { foo } from './index.js'
const script = __VUE_DEFAULT__"
"import { default as __VUE_DEFAULT__ } from './index.js'
export { foo } from './index.js'
const script = __VUE_DEFAULT__"
`)
expect(
rewriteDefault(`export { foo, default } from './index.js'`, 'script')
).toMatchInlineSnapshot(`
"import { default as __VUE_DEFAULT__ } from './index.js'
export { foo, } from './index.js'
const script = __VUE_DEFAULT__"
"import { default as __VUE_DEFAULT__ } from './index.js'
export { foo, } from './index.js'
const script = __VUE_DEFAULT__"
`)
expect(
@ -123,9 +144,9 @@ describe('compiler sfc: rewriteDefault', () => {
'script'
)
).toMatchInlineSnapshot(`
"import { foo } from './index.js'
export { bar } from './index.js'
const script = foo"
"import { foo as __VUE_DEFAULT__ } from './index.js'
export { bar } from './index.js'
const script = __VUE_DEFAULT__"
`)
expect(
@ -134,9 +155,9 @@ describe('compiler sfc: rewriteDefault', () => {
'script'
)
).toMatchInlineSnapshot(`
"import { foo } from './index.js'
export { bar } from './index.js'
const script = foo"
"import { foo as __VUE_DEFAULT__ } from './index.js'
export { bar } from './index.js'
const script = __VUE_DEFAULT__"
`)
expect(
@ -145,18 +166,42 @@ describe('compiler sfc: rewriteDefault', () => {
'script'
)
).toMatchInlineSnapshot(`
"import { foo } from './index.js'
export { bar, } from './index.js'
const script = foo"
"import { foo as __VUE_DEFAULT__ } from './index.js'
export { bar, } from './index.js'
const script = __VUE_DEFAULT__"
`)
expect(
rewriteDefault(
`export { foo as default } from './index.js' \n const foo = 1`,
'script'
)
).toMatchInlineSnapshot(`
"import { foo as __VUE_DEFAULT__ } from './index.js'
export { } from './index.js'
const foo = 1
const script = __VUE_DEFAULT__"
`)
expect(
rewriteDefault(
`const a = 1 \nexport { a as default } from 'xxx'`,
'script'
)
).toMatchInlineSnapshot(`
"import { a as __VUE_DEFAULT__ } from 'xxx'
const a = 1
export { } from 'xxx'
const script = __VUE_DEFAULT__"
`)
})
test('export default class', async () => {
expect(rewriteDefault(`export default class Foo {}`, 'script'))
.toMatchInlineSnapshot(`
"class Foo {}
const script = Foo"
`)
" class Foo {}
const script = Foo"
`)
})
test('export default class w/ comments', async () => {
@ -164,7 +209,7 @@ describe('compiler sfc: rewriteDefault', () => {
rewriteDefault(`// export default\nexport default class Foo {}`, 'script')
).toMatchInlineSnapshot(`
"// export default
class Foo {}
class Foo {}
const script = Foo"
`)
})
@ -190,16 +235,18 @@ describe('compiler sfc: rewriteDefault', () => {
).toMatchInlineSnapshot(`
"/*
export default class Foo {}*/
class Bar {}
class Bar {}
const script = Bar"
`)
})
test('@Component\nexport default class', async () => {
expect(rewriteDefault(`@Component\nexport default class Foo {}`, 'script'))
.toMatchInlineSnapshot(`
"@Component
class Foo {}
expect(
rewriteDefault(`@Component\nexport default class Foo {}`, 'script', [
'decorators-legacy'
])
).toMatchInlineSnapshot(`
"@Component class Foo {}
const script = Foo"
`)
})
@ -208,12 +255,12 @@ describe('compiler sfc: rewriteDefault', () => {
expect(
rewriteDefault(
`// export default\n@Component\nexport default class Foo {}`,
'script'
'script',
['decorators-legacy']
)
).toMatchInlineSnapshot(`
"// export default
@Component
class Foo {}
@Component class Foo {}
const script = Foo"
`)
})
@ -242,7 +289,7 @@ describe('compiler sfc: rewriteDefault', () => {
"/*
@Component
export default class Foo {}*/
class Bar {}
class Bar {}
const script = Bar"
`)
})

View File

@ -53,7 +53,7 @@ import {
} from './cssVars'
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
import { warnOnce } from './warn'
import { rewriteDefault } from './rewriteDefault'
import { rewriteDefaultAST } from './rewriteDefault'
import { createCache } from './cache'
import { shouldTransform, transformAST } from '@vue/reactivity-transform'
@ -231,7 +231,9 @@ export function compileScript(
}
}
if (cssVars.length) {
content = rewriteDefault(content, DEFAULT_VAR, plugins)
const s = new MagicString(content)
rewriteDefaultAST(scriptAst.body, s, DEFAULT_VAR)
content = s.toString()
content += genNormalScriptCssVarsCode(
cssVars,
bindings,
@ -1759,6 +1761,7 @@ export function compileScript(
return {
...scriptSetup,
s,
bindings: bindingMetadata,
imports: userImports,
content: s.toString(),

View File

@ -3,7 +3,7 @@ export { parse } from './parse'
export { compileTemplate } from './compileTemplate'
export { compileStyle, compileStyleAsync } from './compileStyle'
export { compileScript } from './compileScript'
export { rewriteDefault } from './rewriteDefault'
export { rewriteDefault, rewriteDefaultAST } from './rewriteDefault'
export {
shouldTransform as shouldTransformRef,
transform as transformRef,

View File

@ -12,6 +12,7 @@ import { TemplateCompiler } from './compileTemplate'
import { parseCssVars } from './cssVars'
import { createCache } from './cache'
import { hmrShouldReload, ImportBinding } from './compileScript'
import MagicString from 'magic-string'
export const DEFAULT_FILENAME = 'anonymous.vue'
@ -41,6 +42,7 @@ export interface SFCTemplateBlock extends SFCBlock {
export interface SFCScriptBlock extends SFCBlock {
type: 'script'
s: MagicString
setup?: string | boolean
bindings?: BindingMetadata
imports?: Record<string, ImportBinding>

View File

@ -1,55 +1,55 @@
import { parse, ParserPlugin } from '@babel/parser'
import { parse } from '@babel/parser'
import MagicString from 'magic-string'
import type { ParserPlugin } from '@babel/parser'
import type { Identifier, Statement } from '@babel/types'
const defaultExportRE = /((?:^|\n|;)\s*)export(\s*)default/
const namedDefaultExportRE = /((?:^|\n|;)\s*)export(.+)(?:as)?(\s*)default/s
const exportDefaultClassRE =
/((?:^|\n|;)\s*)export\s+default\s+class\s+([\w$]+)/
/**
* Utility for rewriting `export default` in a script block into a variable
* declaration so that we can inject things into it
*/
export function rewriteDefault(
input: string,
as: string,
parserPlugins?: ParserPlugin[]
): string {
if (!hasDefaultExport(input)) {
return input + `\nconst ${as} = {}`
}
let replaced: string | undefined
const classMatch = input.match(exportDefaultClassRE)
if (classMatch) {
replaced =
input.replace(exportDefaultClassRE, '$1class $2') +
`\nconst ${as} = ${classMatch[2]}`
} else {
replaced = input.replace(defaultExportRE, `$1const ${as} =`)
}
if (!hasDefaultExport(replaced)) {
return replaced
}
// if the script somehow still contains `default export`, it probably has
// multi-line comments or template strings. fallback to a full parse.
const s = new MagicString(input)
const ast = parse(input, {
sourceType: 'module',
plugins: parserPlugins
}).program.body
const s = new MagicString(input)
rewriteDefaultAST(ast, s, as)
return s.toString()
}
/**
* Utility for rewriting `export default` in a script block into a variable
* declaration so that we can inject things into it
*/
export function rewriteDefaultAST(
ast: Statement[],
s: MagicString,
as: string
): void {
if (!hasDefaultExport(ast)) {
s.append(`\nconst ${as} = {}`)
return
}
// if the script somehow still contains `default export`, it probably has
// multi-line comments or template strings. fallback to a full parse.
ast.forEach(node => {
if (node.type === 'ExportDefaultDeclaration') {
if (node.declaration.type === 'ClassDeclaration') {
s.overwrite(node.start!, node.declaration.id.start!, `class `)
let start: number =
node.declaration.decorators && node.declaration.decorators.length > 0
? node.declaration.decorators[
node.declaration.decorators.length - 1
].end!
: node.start!
s.overwrite(start, node.declaration.id.start!, ` class `)
s.append(`\nconst ${as} = ${node.declaration.id.name}`)
} else {
s.overwrite(node.start!, node.declaration.start!, `const ${as} = `)
}
}
if (node.type === 'ExportNamedDeclaration') {
} else if (node.type === 'ExportNamedDeclaration') {
for (const specifier of node.specifiers) {
if (
specifier.type === 'ExportSpecifier' &&
@ -58,56 +58,64 @@ export function rewriteDefault(
) {
if (node.source) {
if (specifier.local.name === 'default') {
const end = specifierEnd(input, specifier.local.end!, node.end!)
s.prepend(
`import { default as __VUE_DEFAULT__ } from '${node.source.value}'\n`
)
s.overwrite(specifier.start!, end, ``)
const end = specifierEnd(s, specifier.local.end!, node.end!)
s.remove(specifier.start!, end)
s.append(`\nconst ${as} = __VUE_DEFAULT__`)
continue
} else {
const end = specifierEnd(
input,
specifier.exported.end!,
node.end!
)
s.prepend(
`import { ${input.slice(
`import { ${s.slice(
specifier.local.start!,
specifier.local.end!
)} } from '${node.source.value}'\n`
)} as __VUE_DEFAULT__ } from '${node.source.value}'\n`
)
s.overwrite(specifier.start!, end, ``)
s.append(`\nconst ${as} = ${specifier.local.name}`)
const end = specifierEnd(s, specifier.exported.end!, node.end!)
s.remove(specifier.start!, end)
s.append(`\nconst ${as} = __VUE_DEFAULT__`)
continue
}
}
const end = specifierEnd(input, specifier.end!, node.end!)
s.overwrite(specifier.start!, end, ``)
const end = specifierEnd(s, specifier.end!, node.end!)
s.remove(specifier.start!, end)
s.append(`\nconst ${as} = ${specifier.local.name}`)
}
}
}
})
return s.toString()
}
export function hasDefaultExport(input: string): boolean {
return defaultExportRE.test(input) || namedDefaultExportRE.test(input)
export function hasDefaultExport(ast: Statement[]): boolean {
for (const stmt of ast) {
if (stmt.type === 'ExportDefaultDeclaration') {
return true
} else if (
stmt.type === 'ExportNamedDeclaration' &&
stmt.specifiers.some(
spec => (spec.exported as Identifier).name === 'default'
)
) {
return true
}
}
return false
}
function specifierEnd(input: string, end: number, nodeEnd: number | null) {
function specifierEnd(s: MagicString, end: number, nodeEnd: number | null) {
// export { default , foo } ...
let hasCommas = false
let oldEnd = end
while (end < nodeEnd!) {
if (/\s/.test(input.charAt(end))) {
if (/\s/.test(s.slice(end, end + 1))) {
end++
} else if (input.charAt(end) === ',') {
} else if (s.slice(end, end + 1) === ',') {
end++
hasCommas = true
break
} else if (input.charAt(end) === '}') {
} else if (s.slice(end, end + 1) === '}') {
break
}
}