mirror of https://github.com/vuejs/core.git
fix(compiler-sfc): rewrite default export with AST analysis instead of regex (#7068)
closes #7038 closes #7041 closes #7078
This commit is contained in:
parent
7def8b15b8
commit
701b95ff3d
|
@ -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"
|
||||
`)
|
||||
})
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue