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')
: `(${rawExp})${asParams ? `=>{}` : ``}`
try {
ast = parseJS(source, {
- plugins: [
- ...context.expressionPlugins,
- ...(babelParserDefautPlugins as ParserPlugin[])
- ]
+ plugins: [...context.expressionPlugins, ...babelParserDefautPlugins]
}).program
} catch (e) {
context.onError(
--- /dev/null
+// 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 }"
+`;
--- /dev/null
+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`)
+ })
+ })
+})
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
const isTS = scriptSetup.lang === 'ts'
const plugins: ParserPlugin[] = [
...(options.parserPlugins || []),
- ...(babelParserDefautPlugins as ParserPlugin[]),
+ ...babelParserDefautPlugins,
...(isTS ? (['typescript'] as const) : [])
]
// 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`
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!)
}
}
} else {
setupExports[specifier.exported.name] = true
+ if (node.source) {
+ imports[specifier.exported.name] = node.source.value
+ }
}
}
}
}
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 {
- // 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__ =`
- )
- // save it for analysis when all imports and variable declarations have
- // been recorded
- defaultExport = node
- needDefaultExportCheck = true
- }
+ 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__ =`)
+ // save it for analysis when all imports and variable declarations have
+ // been recorded
+ defaultExport = node
+ needDefaultExportRefCheck = true
}
if (
// check default export to make sure it doesn't reference setup scope
// variables
- if (needDefaultExportCheck) {
+ if (needDefaultExportRefCheck) {
checkDefaultExport(
defaultExport!,
setupScopeVars,
// 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`)
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`.
}
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)
SFCAsyncStyleCompileOptions,
SFCStyleCompileResults
} from './compileStyle'
+export { SFCScriptCompileOptions } from './compileScript'
export {
CompilerOptions,
CompilerError,
'bigInt',
'optionalChaining',
'nullishCoalescingOperator'
-]
+] as const
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
? Object.freeze({})