wip: tests for compileScriptSetup

This commit is contained in:
Evan You 2020-07-08 21:11:57 -04:00
parent e4df2d7749
commit 3e1cdba9db
6 changed files with 668 additions and 49 deletions

View File

@ -30,7 +30,6 @@ import {
import { createCompilerError, ErrorCodes } from '../errors'
import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
import { validateBrowserExpression } from '../validateExpression'
import { ParserPlugin } from '@babel/parser'
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
@ -130,10 +129,7 @@ export function processExpression(
: `(${rawExp})${asParams ? `=>{}` : ``}`
try {
ast = parseJS(source, {
plugins: [
...context.expressionPlugins,
...(babelParserDefautPlugins as ParserPlugin[])
]
plugins: [...context.expressionPlugins, ...babelParserDefautPlugins]
}).program
} catch (e) {
context.onError(

View File

@ -0,0 +1,248 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SFC compile <script setup> <script setup lang="ts"> hoist type declarations 1`] = `
"import { defineComponent as __define__ } from 'vue'
import { Slots as __Slots__ } from 'vue'
export interface Foo {}
type Bar = {}
export function setup() {
const a = 1
return { a }
}
export default __define__({
setup
})"
`;
exports[`SFC compile <script setup> errors should allow export default referencing imported binding 1`] = `
"import { bar } from './bar'
export function setup() {
return { bar }
}
const __default__ = {
props: {
foo: {
default: () => bar
}
}
}
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> errors should allow export default referencing re-exported binding 1`] = `
"import { bar } from './bar'
export function setup() {
return { bar }
}
const __default__ = {
props: {
foo: {
default: () => bar
}
}
}
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> errors should allow export default referencing scope var 1`] = `
"export function setup() {
const bar = 1
return { }
}
const __default__ = {
props: {
foo: {
default: bar => bar + 1
}
}
}
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> explicit setup signature 1`] = `
"export function setup(props, { emit }) {
emit('foo')
return { }
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export * from './x' 1`] = `
"import { toRefs as __toRefs__ } from 'vue'
import * as __export_all_0__ from './x'
export function setup() {
const y = 1
return Object.assign(
{ y },
__toRefs__(__export_all_0__)
)
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export { x } 1`] = `
"export function setup() {
const x = 1
const y = 2
return { x, y }
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export { x } from './x' 1`] = `
"import { x, y } from './x'
export function setup() {
return { x, y }
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export { x as default } 1`] = `
"import x from './x'
export function setup() {
const y = 1
return { y }
}
const __default__ = x
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> exports export { x as default } from './x' 1`] = `
"import { x as __default__ } from './x'
import { y } from './x'
export function setup() {
return { y }
}
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> exports export class X() {} 1`] = `
"export function setup() {
class X {}
return { X }
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export const { x } = ... (destructuring) 1`] = `
"export function setup() {
const [a = 1, { b } = { b: 123 }, ...c] = useFoo()
const { d = 2, _: [e], ...f } = useBar()
return { a, b, c, d, e, f }
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export const x = ... 1`] = `
"export function setup() {
const x = 1
return { x }
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export default from './x' 1`] = `
"import __default__ from './x'
export function setup() {
return { }
}
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> exports export default in <script setup> 1`] = `
"export function setup() {
const y = 1
return { y }
}
const __default__ = {
props: ['foo']
}
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> exports export function x() {} 1`] = `
"export function setup() {
function x(){}
return { x }
}
export default { setup }"
`;
exports[`SFC compile <script setup> import dedupe between <script> and <script setup> 1`] = `
"import { x } from './x'
export function setup() {
x()
return { }
}
export default { setup }"
`;
exports[`SFC compile <script setup> should hoist imports 1`] = `
"import { ref } from 'vue'
export function setup() {
return { }
}
export default { setup }"
`;

View File

@ -0,0 +1,366 @@
import { parse, compileScriptSetup, SFCScriptCompileOptions } from '../src'
import { parse as babelParse } from '@babel/parser'
import { babelParserDefautPlugins } from '@vue/shared'
function compile(src: string, options?: SFCScriptCompileOptions) {
const { descriptor } = parse(src)
return compileScriptSetup(descriptor, options)
}
function assertCode(code: string) {
// parse the generated code to make sure it is valid
try {
babelParse(code, {
sourceType: 'module',
plugins: [...babelParserDefautPlugins, 'typescript']
})
} catch (e) {
console.log(code)
throw e
}
expect(code).toMatchSnapshot()
}
describe('SFC compile <script setup>', () => {
test('should hoist imports', () => {
assertCode(compile(`<script setup>import { ref } from 'vue'</script>`).code)
})
test('explicit setup signature', () => {
assertCode(
compile(`<script setup="props, { emit }">emit('foo')</script>`).code
)
})
test('import dedupe between <script> and <script setup>', () => {
const code = compile(`
<script>
import { x } from './x'
</script>
<script setup>
import { x } from './x'
x()
</script>
`).code
assertCode(code)
expect(code.indexOf(`import { x }`)).toEqual(
code.lastIndexOf(`import { x }`)
)
})
describe('exports', () => {
test('export const x = ...', () => {
const { code, bindings } = compile(
`<script setup>export const x = 1</script>`
)
assertCode(code)
expect(bindings).toStrictEqual({
x: 'setup'
})
})
test('export const { x } = ... (destructuring)', () => {
const { code, bindings } = compile(`<script setup>
export const [a = 1, { b } = { b: 123 }, ...c] = useFoo()
export const { d = 2, _: [e], ...f } = useBar()
</script>`)
assertCode(code)
expect(bindings).toStrictEqual({
a: 'setup',
b: 'setup',
c: 'setup',
d: 'setup',
e: 'setup',
f: 'setup'
})
})
test('export function x() {}', () => {
const { code, bindings } = compile(
`<script setup>export function x(){}</script>`
)
assertCode(code)
expect(bindings).toStrictEqual({
x: 'setup'
})
})
test('export class X() {}', () => {
const { code, bindings } = compile(
`<script setup>export class X {}</script>`
)
assertCode(code)
expect(bindings).toStrictEqual({
X: 'setup'
})
})
test('export { x }', () => {
const { code, bindings } = compile(
`<script setup>
const x = 1
const y = 2
export { x, y }
</script>`
)
assertCode(code)
expect(bindings).toStrictEqual({
x: 'setup',
y: 'setup'
})
})
test(`export { x } from './x'`, () => {
const { code, bindings } = compile(
`<script setup>
export { x, y } from './x'
</script>`
)
assertCode(code)
expect(bindings).toStrictEqual({
x: 'setup',
y: 'setup'
})
})
test(`export default from './x'`, () => {
const { code, bindings } = compile(
`<script setup>
export default from './x'
</script>`,
{
parserPlugins: ['exportDefaultFrom']
}
)
assertCode(code)
expect(bindings).toStrictEqual({})
})
test(`export { x as default }`, () => {
const { code, bindings } = compile(
`<script setup>
import x from './x'
const y = 1
export { x as default, y }
</script>`
)
assertCode(code)
expect(bindings).toStrictEqual({
y: 'setup'
})
})
test(`export { x as default } from './x'`, () => {
const { code, bindings } = compile(
`<script setup>
export { x as default, y } from './x'
</script>`
)
assertCode(code)
expect(bindings).toStrictEqual({
y: 'setup'
})
})
test(`export * from './x'`, () => {
const { code, bindings } = compile(
`<script setup>
export * from './x'
export const y = 1
</script>`
)
assertCode(code)
expect(bindings).toStrictEqual({
y: 'setup'
// in this case we cannot extract bindings from ./x so it falls back
// to runtime proxy dispatching
})
})
test('export default in <script setup>', () => {
const { code, bindings } = compile(
`<script setup>
export default {
props: ['foo']
}
export const y = 1
</script>`
)
assertCode(code)
expect(bindings).toStrictEqual({
y: 'setup'
})
})
})
describe('<script setup lang="ts">', () => {
test('hoist type declarations', () => {
const { code, bindings } = compile(`
<script setup lang="ts">
export interface Foo {}
type Bar = {}
export const a = 1
</script>`)
assertCode(code)
expect(bindings).toStrictEqual({ a: 'setup' })
})
test('extract props', () => {})
test('extract emits', () => {})
})
describe('errors', () => {
test('must have <script setup>', () => {
expect(() => compile(`<script>foo()</script>`)).toThrow(
`SFC has no <script setup>`
)
})
test('<script> and <script setup> must have same lang', () => {
expect(() =>
compile(`<script>foo()</script><script setup lang="ts">bar()</script>`)
).toThrow(`<script> and <script setup> must have the same language type`)
})
test('export local as default', () => {
expect(() =>
compile(`<script setup>
const bar = 1
export { bar as default }
</script>`)
).toThrow(`Cannot export locally defined variable as default`)
})
test('export default referencing local var', () => {
expect(() =>
compile(`<script setup>
const bar = 1
export default {
props: {
foo: {
default: () => bar
}
}
}
</script>`)
).toThrow(`cannot reference locally declared variables`)
})
test('export default referencing exports', () => {
expect(() =>
compile(`<script setup>
export const bar = 1
export default {
props: bar
}
</script>`)
).toThrow(`cannot reference locally declared variables`)
})
test('should allow export default referencing scope var', () => {
assertCode(
compile(`<script setup>
const bar = 1
export default {
props: {
foo: {
default: bar => bar + 1
}
}
}
</script>`).code
)
})
test('should allow export default referencing imported binding', () => {
assertCode(
compile(`<script setup>
import { bar } from './bar'
export { bar }
export default {
props: {
foo: {
default: () => bar
}
}
}
</script>`).code
)
})
test('should allow export default referencing re-exported binding', () => {
assertCode(
compile(`<script setup>
export { bar } from './bar'
export default {
props: {
foo: {
default: () => bar
}
}
}
</script>`).code
)
})
test('error on duplicated defalut export', () => {
expect(() =>
compile(`
<script>
export default {}
</script>
<script setup>
export default {}
</script>
`)
).toThrow(`Default export is already declared`)
expect(() =>
compile(`
<script>
export default {}
</script>
<script setup>
const x = {}
export { x as default }
</script>
`)
).toThrow(`Default export is already declared`)
expect(() =>
compile(`
<script>
export default {}
</script>
<script setup>
export { x as default } from './y'
</script>
`)
).toThrow(`Default export is already declared`)
expect(() =>
compile(`
<script>
export { x as default } from './y'
</script>
<script setup>
export default {}
</script>
`)
).toThrow(`Default export is already declared`)
expect(() =>
compile(`
<script>
const x = {}
export { x as default }
</script>
<script setup>
export default {}
</script>
`)
).toThrow(`Default export is already declared`)
})
})
})

View File

@ -51,7 +51,21 @@ export function compileScriptSetup(
const setupExports: Record<string, boolean> = {}
let exportAllIndex = 0
let defaultExport: Node | undefined
let needDefaultExportCheck: boolean = false
let needDefaultExportRefCheck: boolean = false
const checkDuplicateDefaultExport = (node: Node) => {
if (defaultExport) {
// <script> already has export default
throw new Error(
`Default export is already declared in normal <script>.\n\n` +
generateCodeFrame(
source,
node.start! + startOffset,
node.start! + startOffset + `export default`.length
)
)
}
}
const s = new MagicString(source)
const startOffset = scriptSetup.loc.start.offset
@ -62,7 +76,7 @@ export function compileScriptSetup(
const isTS = scriptSetup.lang === 'ts'
const plugins: ParserPlugin[] = [
...(options.parserPlugins || []),
...(babelParserDefautPlugins as ParserPlugin[]),
...babelParserDefautPlugins,
...(isTS ? (['typescript'] as const) : [])
]
@ -130,10 +144,11 @@ export function compileScriptSetup(
// 2. check <script setup="xxx"> function signature
const hasExplicitSignature = typeof scriptSetup.setup === 'string'
let propsVar = `$props`
let emitVar = `$emit`
let slotsVar = `$slots`
let attrsVar = `$attrs`
let propsVar: string | undefined
let emitVar: string | undefined
let slotsVar: string | undefined
let attrsVar: string | undefined
let propsType = `{}`
let emitType = `(e: string, ...args: any[]) => void`
@ -239,24 +254,32 @@ export function compileScriptSetup(
s.remove(start, end)
}
for (const specifier of node.specifiers) {
if (specifier.type == 'ExportDefaultSpecifier') {
if (specifier.type === 'ExportDefaultSpecifier') {
// export default from './x'
// rewrite to `import __default__ from './x'`
checkDuplicateDefaultExport(node)
defaultExport = node
s.overwrite(
specifier.exported.start! + startOffset,
specifier.exported.start! + startOffset + 7,
'__default__'
)
} else if (specifier.type == 'ExportSpecifier') {
} else if (specifier.type === 'ExportSpecifier') {
if (specifier.exported.name === 'default') {
checkDuplicateDefaultExport(node)
defaultExport = node
// 1. remove specifier
if (node.specifiers.length > 1) {
s.remove(
specifier.start! + startOffset,
specifier.end! + startOffset
)
// removing the default specifier from a list of specifiers.
// look ahead until we reach the first non , or whitespace char.
let end = specifier.end! + startOffset
while (end < source.length) {
if (/[^,\s]/.test(source.charAt(end))) {
break
}
end++
}
s.remove(specifier.start! + startOffset, end)
} else {
s.remove(node.start! + startOffset!, node.end! + startOffset!)
}
@ -288,6 +311,9 @@ export function compileScriptSetup(
}
} else {
setupExports[specifier.exported.name] = true
if (node.source) {
imports[specifier.exported.name] = node.source.value
}
}
}
}
@ -305,30 +331,15 @@ export function compileScriptSetup(
}
if (node.type === 'ExportDefaultDeclaration') {
if (defaultExport) {
// <script> already has export default
throw new Error(
`Default export is already declared in normal <script>.\n\n` +
generateCodeFrame(
source,
node.start! + startOffset,
node.start! + startOffset + `export default`.length
)
)
} else {
checkDuplicateDefaultExport(node)
// export default {} inside <script setup>
// this should be kept in module scope - move it to the end
s.move(start, end, source.length)
s.overwrite(
start,
start + `export default`.length,
`const __default__ =`
)
s.overwrite(start, start + `export default`.length, `const __default__ =`)
// save it for analysis when all imports and variable declarations have
// been recorded
defaultExport = node
needDefaultExportCheck = true
}
needDefaultExportRefCheck = true
}
if (
@ -397,7 +408,7 @@ export function compileScriptSetup(
// check default export to make sure it doesn't reference setup scope
// variables
if (needDefaultExportCheck) {
if (needDefaultExportRefCheck) {
checkDefaultExport(
defaultExport!,
setupScopeVars,
@ -428,7 +439,7 @@ export function compileScriptSetup(
// wrap setup code with function
// finalize the argument signature.
let args
let args = ``
if (isTS) {
if (slotsType === '__Slots__') {
s.prepend(`import { Slots as __Slots__ } from 'vue'\n`)
@ -450,13 +461,9 @@ export function compileScriptSetup(
ss.appendRight(setupCtxASTNode.end! - 1!, `: ${ctxType}`)
}
args = ss.toString()
} else {
args = `$props: ${propsType}, { emit: $emit, slots: $slots, attrs: $attrs }: ${ctxType}`
}
} else {
args = hasExplicitSignature
? scriptSetup.setup
: `$props, { emit: $emit, slots: $slots, attrs: $attrs }`
args = hasExplicitSignature ? (scriptSetup.setup as string) : ``
}
// export the content of <script setup> as a named export, `setup`.
@ -602,6 +609,7 @@ function walkPattern(node: Node, bindings: Record<string, boolean>) {
}
function extractProps(node: TSTypeLiteral, props: Set<string>) {
// TODO generate type/required checks in dev
for (const m of node.members) {
if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') {
props.add(m.key.name)

View File

@ -23,6 +23,7 @@ export {
SFCAsyncStyleCompileOptions,
SFCStyleCompileResults
} from './compileStyle'
export { SFCScriptCompileOptions } from './compileScript'
export {
CompilerOptions,
CompilerError,

View File

@ -23,7 +23,7 @@ export const babelParserDefautPlugins = [
'bigInt',
'optionalChaining',
'nullishCoalescingOperator'
]
] as const
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
? Object.freeze({})