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 default {
const __default__ = {}
export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) {
expose();
@ -14,13 +16,15 @@ export default {
return { n, x }
}
}"
})"
`;
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 }) {
expose();
@ -29,29 +33,48 @@ export default {
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`] = `
"import { defineComponent as _defineComponent } from 'vue'
const __default__ = {
name: \\"test\\"
}
import { x } from './x'
function setup(__props, { expose }) {
export default /*#__PURE__*/_defineComponent({
...__default__,
setup(__props, { expose }) {
expose();
x()
return { x }
}
})"
`;
const __default__ = {
name: \\"test\\"
exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first, named default export 1`] = `
"export const n = 1
const def = {}
const __default__ = def
import { x } from './x'
export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) {
expose();
x()
return { n, def, x }
}
export default /*#__PURE__*/_defineComponent({
...__default__,
setup})"
})"
`;
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'
}
function setup(__props, { expose }) {
export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) {
expose();
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'
}
function setup(__props, { expose }) {
export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) {
expose();
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`] = `
"import { x } from './x'
"import { xx } from './x'
let aa = 1
const bb = 2
function cc() {}
class dd {}
import { x } from './x'
export default {
setup(__props, { expose }) {
@ -994,12 +1020,7 @@ export default {
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`] = `

View File

@ -169,6 +169,68 @@ defineExpose({ foo: 123 })
})
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', () => {
// #4371
test('with many spaces and newline', () => {
@ -205,50 +267,6 @@ defineExpose({ foo: 123 })
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', () => {

View File

@ -59,6 +59,9 @@ const DEFINE_EMITS = 'defineEmits'
const DEFINE_EXPOSE = 'defineExpose'
const WITH_DEFAULTS = 'withDefaults'
// constants
const DEFAULT_VAR = `__default__`
const isBuiltInDir = makeMap(
`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) {
content = rewriteDefault(content, `__default__`, plugins)
content = rewriteDefault(content, DEFAULT_VAR, plugins)
content += genNormalScriptCssVarsCode(
cssVars,
bindings,
scopeId,
isProd
)
content += `\nexport default __default__`
content += `\nexport default ${DEFAULT_VAR}`
}
return {
...script,
@ -251,7 +254,6 @@ export function compileScript(
// metadata that needs to be returned
const bindingMetadata: BindingMetadata = {}
const defaultTempVar = `__default__`
const helperImports: Set<string> = new Set()
const userImports: Record<string, ImportBinding> = 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
let scriptAst: Program | undefined
if (script) {
// import dedupe between <script> and <script setup>
scriptAst = parse(
script.content,
{
@ -809,9 +810,10 @@ export function compileScript(
} else if (node.type === 'ExportDefaultDeclaration') {
// export default
defaultExport = node
// export default { ... } --> const __default__ = { ... }
const start = node.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') {
const defaultSpecifier = node.specifiers.find(
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
// add to top
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 {
// export { x as default }
// rewrite to `const __default__ = x` and move to end
s.append(
`\nconst ${defaultTempVar} = ${defaultSpecifier.local.name}\n`
s.appendLeft(
scriptEndOffset!,
`\nconst ${DEFAULT_VAR} = ${defaultSpecifier.local.name}\n`
)
}
}
@ -871,6 +874,13 @@ export function compileScript(
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
@ -1384,26 +1394,14 @@ export function compileScript(
// explicitly call `defineExpose`, call expose() with no args.
const exposeCall =
hasDefineExposeCall || options.inlineTemplate ? `` : ` expose();\n`
// wrap setup code with function.
if (isTS) {
// for TS, make sure the exported type is still valid type with
// correct props information
// we have to use object spread for types to be merged properly
// user's TS setting should compile it down to proper targets
const def = defaultExport ? `\n ...${defaultTempVar},` : ``
// wrap setup code with function.
// export the content of <script setup> as a named export, `setup`.
// this allows `import { setup } from '*.vue'` for testing purposes.
if (defaultExport) {
s.prependLeft(
startOffset,
`\n${hasAwait ? `async ` : ``}function setup(${args}) {\n`
)
s.append(
`\nexport default /*#__PURE__*/${helper(
`defineComponent`
)}({${def}${runtimeOptions}\n setup})`
)
} else {
// export default defineComponent({ ...__default__, ... })
const def = defaultExport ? `\n ...${DEFAULT_VAR},` : ``
s.prependLeft(
startOffset,
`\nexport default /*#__PURE__*/${helper(
@ -1413,17 +1411,16 @@ export function compileScript(
}setup(${args}) {\n${exposeCall}`
)
s.appendRight(endOffset, `})`)
}
} else {
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(
startOffset,
`\n${hasAwait ? `async ` : ``}function setup(${args}) {\n`
)
s.append(
`\nexport default /*#__PURE__*/ Object.assign(${defaultTempVar}, {${runtimeOptions}\n setup\n})\n`
`\nexport default /*#__PURE__*/Object.assign(${DEFAULT_VAR}, {${runtimeOptions}\n ` +
`${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`
)
s.appendRight(endOffset, `})`)
} else {
s.prependLeft(
startOffset,