fix(compiler-sfc): generate valid TS in script and script setup co-usage with TS

fix #5094
This commit is contained in:
Evan You 2021-12-12 09:53:52 +08:00
parent ea1fcfba37
commit 7e4f0a8694
3 changed files with 147 additions and 111 deletions

View File

@ -5,7 +5,9 @@ exports[`SFC compile <script setup> <script> and <script setup> co-usage script
export const n = 1 export const n = 1
export default { const __default__ = {}
export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) { setup(__props, { expose }) {
expose(); expose();
@ -14,13 +16,15 @@ export default {
return { n, x } return { n, x }
} }
}" })"
`; `;
exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first 1`] = ` exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first 1`] = `
"import { x } from './x' "export const n = 1
const __default__ = {}
import { x } from './x'
export default { export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) { setup(__props, { expose }) {
expose(); expose();
@ -29,29 +33,48 @@ export default {
return { n, x } return { n, x }
} }
} })"
export const n = 1"
`; `;
exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first, lang="ts", script block content export default 1`] = ` exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first, lang="ts", script block content export default 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { x } from './x'
function setup(__props, { expose }) { const __default__ = {
name: \\"test\\"
}
import { x } from './x'
export default /*#__PURE__*/_defineComponent({
...__default__,
setup(__props, { expose }) {
expose();
x() x()
return { x } return { x }
} }
})"
`;
const __default__ = { exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first, named default export 1`] = `
name: \\"test\\" "export const n = 1
} const def = {}
export default /*#__PURE__*/_defineComponent({
...__default__, const __default__ = def
setup})" import { x } from './x'
export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) {
expose();
x()
return { n, def, x }
}
})"
`; `;
exports[`SFC compile <script setup> <script> and <script setup> co-usage spaces in ExportDefaultDeclaration node with many spaces and newline 1`] = ` exports[`SFC compile <script setup> <script> and <script setup> co-usage spaces in ExportDefaultDeclaration node with many spaces and newline 1`] = `
@ -62,16 +85,15 @@ exports[`SFC compile <script setup> <script> and <script setup> co-usage spaces
some:'option' some:'option'
} }
function setup(__props, { expose }) { export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) {
expose();
x() x()
return { n, x } return { n, x }
} }
export default /*#__PURE__*/ Object.assign(__default__, {
setup
})" })"
`; `;
@ -83,16 +105,15 @@ exports[`SFC compile <script setup> <script> and <script setup> co-usage spaces
some:'option' some:'option'
} }
function setup(__props, { expose }) { export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) {
expose();
x() x()
return { n, x } return { n, x }
} }
export default /*#__PURE__*/ Object.assign(__default__, {
setup
})" })"
`; `;
@ -980,7 +1001,12 @@ return () => {}
`; `;
exports[`SFC compile <script setup> should expose top level declarations 1`] = ` exports[`SFC compile <script setup> should expose top level declarations 1`] = `
"import { x } from './x' "import { xx } from './x'
let aa = 1
const bb = 2
function cc() {}
class dd {}
import { x } from './x'
export default { export default {
setup(__props, { expose }) { setup(__props, { expose }) {
@ -994,12 +1020,7 @@ export default {
return { aa, bb, cc, dd, a, b, c, d, xx, x } return { aa, bb, cc, dd, a, b, c, d, xx, x }
} }
} }"
import { xx } from './x'
let aa = 1
const bb = 2
function cc() {}
class dd {}"
`; `;
exports[`SFC compile <script setup> with TypeScript const Enum 1`] = ` exports[`SFC compile <script setup> with TypeScript const Enum 1`] = `

View File

@ -169,6 +169,68 @@ defineExpose({ foo: 123 })
}) })
describe('<script> and <script setup> co-usage', () => { describe('<script> and <script setup> co-usage', () => {
test('script first', () => {
const { content } = compile(`
<script>
export const n = 1
export default {}
</script>
<script setup>
import { x } from './x'
x()
</script>
`)
assertCode(content)
})
test('script setup first', () => {
const { content } = compile(`
<script setup>
import { x } from './x'
x()
</script>
<script>
export const n = 1
export default {}
</script>
`)
assertCode(content)
})
test('script setup first, named default export', () => {
const { content } = compile(`
<script setup>
import { x } from './x'
x()
</script>
<script>
export const n = 1
const def = {}
export { def as default }
</script>
`)
assertCode(content)
})
// #4395
test('script setup first, lang="ts", script block content export default', () => {
const { content } = compile(`
<script setup lang="ts">
import { x } from './x'
x()
</script>
<script lang="ts">
export default {
name: "test"
}
</script>
`)
// ensure __default__ is declared before used
expect(content).toMatch(/const __default__[\S\s]*\.\.\.__default__/m)
assertCode(content)
})
describe('spaces in ExportDefaultDeclaration node', () => { describe('spaces in ExportDefaultDeclaration node', () => {
// #4371 // #4371
test('with many spaces and newline', () => { test('with many spaces and newline', () => {
@ -205,50 +267,6 @@ defineExpose({ foo: 123 })
assertCode(content) assertCode(content)
}) })
}) })
test('script first', () => {
const { content } = compile(`
<script>
export const n = 1
</script>
<script setup>
import { x } from './x'
x()
</script>
`)
assertCode(content)
})
test('script setup first', () => {
const { content } = compile(`
<script setup>
import { x } from './x'
x()
</script>
<script>
export const n = 1
</script>
`)
assertCode(content)
})
// #4395
test('script setup first, lang="ts", script block content export default', () => {
const { content } = compile(`
<script setup lang="ts">
import { x } from './x'
x()
</script>
<script lang="ts">
export default {
name: "test"
}
</script>
`)
// ensure __default__ is declared before used
expect(content).toMatch(/const __default__[\S\s]*\.\.\.__default__/m)
assertCode(content)
})
}) })
describe('imports', () => { describe('imports', () => {

View File

@ -59,6 +59,9 @@ const DEFINE_EMITS = 'defineEmits'
const DEFINE_EXPOSE = 'defineExpose' const DEFINE_EXPOSE = 'defineExpose'
const WITH_DEFAULTS = 'withDefaults' const WITH_DEFAULTS = 'withDefaults'
// constants
const DEFAULT_VAR = `__default__`
const isBuiltInDir = makeMap( const isBuiltInDir = makeMap(
`once,memo,if,else,else-if,slot,text,html,on,bind,model,show,cloak,is` `once,memo,if,else,else-if,slot,text,html,on,bind,model,show,cloak,is`
) )
@ -214,14 +217,14 @@ export function compileScript(
} }
} }
if (cssVars.length) { if (cssVars.length) {
content = rewriteDefault(content, `__default__`, plugins) content = rewriteDefault(content, DEFAULT_VAR, plugins)
content += genNormalScriptCssVarsCode( content += genNormalScriptCssVarsCode(
cssVars, cssVars,
bindings, bindings,
scopeId, scopeId,
isProd isProd
) )
content += `\nexport default __default__` content += `\nexport default ${DEFAULT_VAR}`
} }
return { return {
...script, ...script,
@ -251,7 +254,6 @@ export function compileScript(
// metadata that needs to be returned // metadata that needs to be returned
const bindingMetadata: BindingMetadata = {} const bindingMetadata: BindingMetadata = {}
const defaultTempVar = `__default__`
const helperImports: Set<string> = new Set() const helperImports: Set<string> = new Set()
const userImports: Record<string, ImportBinding> = Object.create(null) const userImports: Record<string, ImportBinding> = Object.create(null)
const userImportAlias: Record<string, string> = Object.create(null) const userImportAlias: Record<string, string> = Object.create(null)
@ -780,7 +782,6 @@ export function compileScript(
// 1. process normal <script> first if it exists // 1. process normal <script> first if it exists
let scriptAst: Program | undefined let scriptAst: Program | undefined
if (script) { if (script) {
// import dedupe between <script> and <script setup>
scriptAst = parse( scriptAst = parse(
script.content, script.content,
{ {
@ -809,9 +810,10 @@ export function compileScript(
} else if (node.type === 'ExportDefaultDeclaration') { } else if (node.type === 'ExportDefaultDeclaration') {
// export default // export default
defaultExport = node defaultExport = node
// export default { ... } --> const __default__ = { ... }
const start = node.start! + scriptStartOffset! const start = node.start! + scriptStartOffset!
const end = node.declaration.start! + scriptStartOffset! const end = node.declaration.start! + scriptStartOffset!
s.overwrite(start, end, `const ${defaultTempVar} = `) s.overwrite(start, end, `const ${DEFAULT_VAR} = `)
} else if (node.type === 'ExportNamedDeclaration') { } else if (node.type === 'ExportNamedDeclaration') {
const defaultSpecifier = node.specifiers.find( const defaultSpecifier = node.specifiers.find(
s => s.exported.type === 'Identifier' && s.exported.name === 'default' s => s.exported.type === 'Identifier' && s.exported.name === 'default'
@ -835,13 +837,14 @@ export function compileScript(
// rewrite to `import { x as __default__ } from './x'` and // rewrite to `import { x as __default__ } from './x'` and
// add to top // add to top
s.prepend( s.prepend(
`import { ${defaultSpecifier.local.name} as ${defaultTempVar} } from '${node.source.value}'\n` `import { ${defaultSpecifier.local.name} as ${DEFAULT_VAR} } from '${node.source.value}'\n`
) )
} else { } else {
// export { x as default } // export { x as default }
// rewrite to `const __default__ = x` and move to end // rewrite to `const __default__ = x` and move to end
s.append( s.appendLeft(
`\nconst ${defaultTempVar} = ${defaultSpecifier.local.name}\n` scriptEndOffset!,
`\nconst ${DEFAULT_VAR} = ${defaultSpecifier.local.name}\n`
) )
} }
} }
@ -871,6 +874,13 @@ export function compileScript(
helperImports.add(h) helperImports.add(h)
} }
} }
// <script> after <script setup>
// we need to move the block up so that `const __default__` is
// declared before being used in the actual component definition
if (scriptStartOffset! > startOffset) {
s.move(scriptStartOffset!, scriptEndOffset!, 0)
}
} }
// 2. parse <script setup> and walk over top level statements // 2. parse <script setup> and walk over top level statements
@ -1384,46 +1394,33 @@ export function compileScript(
// explicitly call `defineExpose`, call expose() with no args. // explicitly call `defineExpose`, call expose() with no args.
const exposeCall = const exposeCall =
hasDefineExposeCall || options.inlineTemplate ? `` : ` expose();\n` hasDefineExposeCall || options.inlineTemplate ? `` : ` expose();\n`
// wrap setup code with function.
if (isTS) { if (isTS) {
// for TS, make sure the exported type is still valid type with // for TS, make sure the exported type is still valid type with
// correct props information // correct props information
// we have to use object spread for types to be merged properly // we have to use object spread for types to be merged properly
// user's TS setting should compile it down to proper targets // user's TS setting should compile it down to proper targets
const def = defaultExport ? `\n ...${defaultTempVar},` : `` // export default defineComponent({ ...__default__, ... })
// wrap setup code with function. const def = defaultExport ? `\n ...${DEFAULT_VAR},` : ``
// export the content of <script setup> as a named export, `setup`. s.prependLeft(
// this allows `import { setup } from '*.vue'` for testing purposes. startOffset,
if (defaultExport) { `\nexport default /*#__PURE__*/${helper(
s.prependLeft( `defineComponent`
startOffset, )}({${def}${runtimeOptions}\n ${
`\n${hasAwait ? `async ` : ``}function setup(${args}) {\n` hasAwait ? `async ` : ``
) }setup(${args}) {\n${exposeCall}`
s.append( )
`\nexport default /*#__PURE__*/${helper( s.appendRight(endOffset, `})`)
`defineComponent`
)}({${def}${runtimeOptions}\n setup})`
)
} else {
s.prependLeft(
startOffset,
`\nexport default /*#__PURE__*/${helper(
`defineComponent`
)}({${def}${runtimeOptions}\n ${
hasAwait ? `async ` : ``
}setup(${args}) {\n${exposeCall}`
)
s.appendRight(endOffset, `})`)
}
} else { } else {
if (defaultExport) { if (defaultExport) {
// can't rely on spread operator in non ts mode // without TS, can't rely on rest spread, so we use Object.assign
// export default Object.assign(__default__, { ... })
s.prependLeft( s.prependLeft(
startOffset, startOffset,
`\n${hasAwait ? `async ` : ``}function setup(${args}) {\n` `\nexport default /*#__PURE__*/Object.assign(${DEFAULT_VAR}, {${runtimeOptions}\n ` +
) `${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`
s.append(
`\nexport default /*#__PURE__*/ Object.assign(${defaultTempVar}, {${runtimeOptions}\n setup\n})\n`
) )
s.appendRight(endOffset, `})`)
} else { } else {
s.prependLeft( s.prependLeft(
startOffset, startOffset,