if (!isDuplicate(node)) {
const needPrefix = shouldPrefix(node, parent)
if (!knownIds[node.name] && needPrefix) {
- if (isPropertyShorthand(node, parent)) {
- // property shorthand like { foo }, we need to add the key since we
- // rewrite the value
+ if (isStaticProperty(parent) && parent.shorthand) {
+ // property shorthand like { foo }, we need to add the key since
+ // we rewrite the value
node.prefix = `${node.name}: `
}
node.name = prefix(node.name)
(node.type === 'ObjectProperty' || node.type === 'ObjectMethod') &&
!node.computed
-const isPropertyShorthand = (node: Node, parent: Node) => {
- return (
- isStaticProperty(parent) &&
- parent.value === node &&
- parent.key.type === 'Identifier' &&
- parent.key.name === (node as Identifier).name &&
- parent.key.start === node.start
- )
-}
-
const isStaticPropertyKey = (node: Node, parent: Node) =>
isStaticProperty(parent) && parent.key === node
-function shouldPrefix(identifier: Identifier, parent: Node) {
+function shouldPrefix(id: Identifier, parent: Node) {
+ // declaration id
if (
- !(
- isFunction(parent) &&
- // not id of a FunctionDeclaration
- ((parent as any).id === identifier ||
- // not a params of a function
- parent.params.includes(identifier))
- ) &&
- // not a key of Property
- !isStaticPropertyKey(identifier, parent) &&
- // not a property of a MemberExpression
- !(
- (parent.type === 'MemberExpression' ||
- parent.type === 'OptionalMemberExpression') &&
- parent.property === identifier &&
- !parent.computed
- ) &&
- // not in an Array destructure pattern
- !(parent.type === 'ArrayPattern') &&
- // skip whitelisted globals
- !isGloballyWhitelisted(identifier.name) &&
- // special case for webpack compilation
- identifier.name !== `require` &&
- // is a special keyword but parsed as identifier
- identifier.name !== `arguments`
+ (parent.type === 'VariableDeclarator' ||
+ parent.type === 'ClassDeclaration') &&
+ parent.id === id
) {
- return true
+ return false
+ }
+
+ if (isFunction(parent)) {
+ // function decalration/expression id
+ if ((parent as any).id === id) {
+ return false
+ }
+ // params list
+ if (parent.params.includes(id)) {
+ return false
+ }
}
+
+ // property key
+ // this also covers object destructure pattern
+ if (isStaticPropertyKey(id, parent)) {
+ return false
+ }
+
+ // array destructure pattern
+ if (parent.type === 'ArrayPattern') {
+ return false
+ }
+
+ // member expression property
+ if (
+ (parent.type === 'MemberExpression' ||
+ parent.type === 'OptionalMemberExpression') &&
+ parent.property === id &&
+ !parent.computed
+ ) {
+ return false
+ }
+
+ // is a special keyword but parsed as identifier
+ if (id.name === 'arguments') {
+ return false
+ }
+
+ // skip whitelisted globals
+ if (isGloballyWhitelisted(id.name)) {
+ return false
+ }
+
+ // special case for webpack compilation
+ if (id.name === 'require') {
+ return false
+ }
+
+ return true
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SFC compile <script setup> <script setup lang="ts"> extract emits 1`] = `
-"import { defineComponent as __define__ } from 'vue'
-import { Slots as __Slots__ } from 'vue'
+"import { Slots, defineComponent } from 'vue'
declare function __emit__(e: 'foo' | 'bar'): void
declare function __emit__(e: 'baz', id: number): void
return { }
}
-export default __define__({
+export default __defineComponent__({
emits: [\\"foo\\", \\"bar\\", \\"baz\\"] as unknown as undefined,
setup
})"
`;
exports[`SFC compile <script setup> <script setup lang="ts"> extract props 1`] = `
-"import { defineComponent as __define__ } from 'vue'
-import { Slots as __Slots__ } from 'vue'
+"import { Slots, defineComponent } from 'vue'
interface Test {}
type Alias = number[]
return { }
}
-export default __define__({
+export default __defineComponent__({
props: {
string: { type: String, required: true },
number: { type: Number, required: true },
`;
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'
+"import { Slots, defineComponent } from 'vue'
export interface Foo {}
type Bar = {}
-
+
export function setup() {
- const a = 1
-
-return { a }
+
+return { }
}
-export default __define__({
+export default __defineComponent__({
setup
})"
`;
`;
exports[`SFC compile <script setup> CSS vars injection w/ <script setup> 1`] = `
-"import { useCssVars as __useCssVars__ } from 'vue'
+"import { useCssVars } from 'vue'
export function setup() {
const color = 'red'
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 bar = 1
-return { }
+return { bar }
}
const __default__ = {
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'
-
+exports[`SFC compile <script setup> imports dedupe between user & helper 1`] = `
+"import { ref } from 'vue'
+
export function setup() {
- const y = 1
-
-return Object.assign(
- { y },
- __toRefs__(__export_all_0__)
-)
+ const foo = ref(1)
+
+return { ref, foo }
}
export default { setup }"
`;
-exports[`SFC compile <script setup> exports export { x } 1`] = `
-"export function setup() {
+exports[`SFC compile <script setup> imports import dedupe between <script> and <script setup> 1`] = `
+"import { x } from './x'
+
+export function setup() {
- const x = 1
- const y = 2
-
-return { x, y }
+ x()
+
+return { x }
}
export default { setup }"
`;
-exports[`SFC compile <script setup> exports export { x } from './x' 1`] = `
-"import { x, y } from './x'
-
+exports[`SFC compile <script setup> imports should extract comment for import or type declarations 1`] = `
+"import a from 'a' // comment
+ import b from 'b'
+
export function setup() {
-
-return { x, y }
+
+return { a, b }
}
export default { setup }"
`;
-exports[`SFC compile <script setup> exports export { x as default } 1`] = `
-"import x from './x'
-
+exports[`SFC compile <script setup> imports should hoist and expose imports 1`] = `
+"import { ref } from 'vue'
export function setup() {
- const y = 1
-
-return { y }
+return { ref }
}
-
-const __default__ = x
-__default__.setup = setup
-export default __default__"
+export default { setup }"
`;
-exports[`SFC compile <script setup> exports export { x as default } from './x' 1`] = `
-"import { x as __default__ } from './x'
-import { y } from './x'
-
+exports[`SFC compile <script setup> ref: syntax sugar accessing ref binding 1`] = `
+"import { ref } from 'vue'
+
export function setup() {
-
-return { y }
+ const a = ref(1)
+ console.log(a.value)
+ function get() {
+ return a.value + 1
+ }
+
+return { a, get }
}
-__default__.setup = setup
-export default __default__"
+export default { setup }"
`;
-exports[`SFC compile <script setup> exports export class X() {} 1`] = `
-"export function setup() {
-class X {}
-return { X }
+exports[`SFC compile <script setup> ref: syntax sugar array destructure 1`] = `
+"import { ref } from 'vue'
+
+export function setup() {
+
+ const n = ref(1), [__a, __b = 1, ...__c] = useFoo()
+const a = ref(__a);
+const b = ref(__b);
+const c = ref(__c);
+ console.log(n.value, a.value, b.value, c.value)
+
+return { n, a, b, c }
}
export default { setup }"
`;
-exports[`SFC compile <script setup> exports export const { x } = ... (destructuring) 1`] = `
-"export function setup() {
+exports[`SFC compile <script setup> ref: syntax sugar convert ref declarations 1`] = `
+"import { ref } from 'vue'
- const [a = 1, { b } = { b: 123 }, ...c] = useFoo()
- const { d = 2, _: [e], ...f } = useBar()
-
-return { a, b, c, d, e, f }
+export function setup() {
+
+ const a = ref(1)
+ const b = ref({
+ count: 0
+ })
+ let c = () => {}
+ let d
+
+return { a, b, c, d }
}
export default { setup }"
`;
-exports[`SFC compile <script setup> exports export const x = ... 1`] = `
-"export function setup() {
-const x = 1
-return { x }
+exports[`SFC compile <script setup> ref: syntax sugar multi ref declarations 1`] = `
+"import { ref } from 'vue'
+
+export function setup() {
+
+ const a = ref(1), b = ref(2), c = ref({
+ count: 0
+ })
+
+return { a, b, c }
}
export default { setup }"
`;
-exports[`SFC compile <script setup> exports export default from './x' 1`] = `
-"import __default__ from './x'
-
+exports[`SFC compile <script setup> ref: syntax sugar mutating ref binding 1`] = `
+"import { ref } from 'vue'
+
export function setup() {
-
-return { }
+ const a = ref(1)
+ const b = ref({ count: 0 })
+ function inc() {
+ a.value++
+ a.value = a.value + 1
+ b.value.count++
+ b.value.count = b.value.count + 1
+ }
+
+return { a, b, inc }
}
-__default__.setup = setup
-export default __default__"
+export default { setup }"
`;
-exports[`SFC compile <script setup> exports export default in <script setup> 1`] = `
-"export function setup() {
+exports[`SFC compile <script setup> ref: syntax sugar nested destructure 1`] = `
+"import { ref } from 'vue'
- const y = 1
-
-return { y }
+export function setup() {
+
+ const [{ a: { b: __b }}] = useFoo()
+const b = ref(__b);
+ const { c: [__d, __e] } = useBar()
+const d = ref(__d);
+const e = ref(__e);
+ console.log(b.value, d.value, e.value)
+
+return { b, d, e }
}
-const __default__ = {
- props: ['foo']
- }
- __default__.setup = setup
-export default __default__"
+export default { setup }"
`;
-exports[`SFC compile <script setup> exports export function x() {} 1`] = `
-"export function setup() {
-function x(){}
-return { x }
+exports[`SFC compile <script setup> ref: syntax sugar object destructure 1`] = `
+"import { ref } from 'vue'
+
+export function setup() {
+
+ const n = ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = useFoo()
+const a = ref(__a);
+const c = ref(__c);
+const d = ref(__d);
+const f = ref(__f);
+const g = ref(__g);
+ console.log(n.value, a.value, c.value, d.value, f.value, g.value)
+
+return { n, a, c, d, f, g }
}
export default { setup }"
`;
-exports[`SFC compile <script setup> import dedupe between <script> and <script setup> 1`] = `
-"import { x } from './x'
-
-export function setup() {
+exports[`SFC compile <script setup> ref: syntax sugar should not convert non ref labels 1`] = `
+"export function setup() {
- x()
+ foo: a = 1, b = 2, c = {
+ count: 0
+ }
return { }
}
export default { setup }"
`;
-exports[`SFC compile <script setup> should extract comment for import or type declarations 1`] = `
-"import a from 'a' // comment
-import b from 'b'
+exports[`SFC compile <script setup> ref: syntax sugar using ref binding in property shorthand 1`] = `
+"import { ref } from 'vue'
export function setup() {
-
-return { }
+ const a = ref(1)
+ const b = { a: a.value }
+ function test() {
+ const { a } = b
+ }
+
+return { a, b, test }
}
export default { setup }"
`;
-exports[`SFC compile <script setup> should hoist imports 1`] = `
-"import { ref } from 'vue'
+exports[`SFC compile <script setup> should expose top level declarations 1`] = `
+"import { x } from './x'
+
export function setup() {
-return { }
+ let a = 1
+ const b = 2
+ function c() {}
+ class d {}
+
+return { x, a, b, c, d }
}
export default { setup }"
}
describe('SFC compile <script setup>', () => {
- test('should hoist imports', () => {
- assertCode(
- compile(`<script setup>import { ref } from 'vue'</script>`).content
- )
- })
-
- test('should extract comment for import or type declarations', () => {
- assertCode(
- compile(`<script setup>
-import a from 'a' // comment
-import b from 'b'
-</script>`).content
- )
- })
-
test('explicit setup signature', () => {
assertCode(
compile(`<script setup="props, { emit }">emit('foo')</script>`).content
)
})
- test('import dedupe between <script> and <script setup>', () => {
+ test('should expose top level declarations', () => {
const { content } = compile(`
- <script>
- import { x } from './x'
- </script>
<script setup>
import { x } from './x'
- x()
+ let a = 1
+ const b = 2
+ function c() {}
+ class d {}
</script>
`)
assertCode(content)
- expect(content.indexOf(`import { x }`)).toEqual(
- content.lastIndexOf(`import { x }`)
- )
+ expect(content).toMatch('return { x, a, b, c, d }')
})
- describe('exports', () => {
- test('export const x = ...', () => {
- const { content, bindings } = compile(
- `<script setup>export const x = 1</script>`
- )
- assertCode(content)
- expect(bindings).toStrictEqual({
- x: 'setup'
- })
- })
-
- test('export const { x } = ... (destructuring)', () => {
- const { content, bindings } = compile(`<script setup>
- export const [a = 1, { b } = { b: 123 }, ...c] = useFoo()
- export const { d = 2, _: [e], ...f } = useBar()
- </script>`)
- assertCode(content)
- expect(bindings).toStrictEqual({
- a: 'setup',
- b: 'setup',
- c: 'setup',
- d: 'setup',
- e: 'setup',
- f: 'setup'
- })
- })
-
- test('export function x() {}', () => {
- const { content, bindings } = compile(
- `<script setup>export function x(){}</script>`
- )
- assertCode(content)
- expect(bindings).toStrictEqual({
- x: 'setup'
- })
- })
-
- test('export class X() {}', () => {
- const { content, bindings } = compile(
- `<script setup>export class X {}</script>`
- )
- assertCode(content)
- expect(bindings).toStrictEqual({
- X: 'setup'
- })
- })
-
- test('export { x }', () => {
- const { content, bindings } = compile(
- `<script setup>
- const x = 1
- const y = 2
- export { x, y }
- </script>`
- )
- assertCode(content)
- expect(bindings).toStrictEqual({
- x: 'setup',
- y: 'setup'
- })
- })
-
- test(`export { x } from './x'`, () => {
- const { content, bindings } = compile(
- `<script setup>
- export { x, y } from './x'
- </script>`
- )
- assertCode(content)
- expect(bindings).toStrictEqual({
- x: 'setup',
- y: 'setup'
- })
- })
-
- test(`export default from './x'`, () => {
- const { content, bindings } = compile(
- `<script setup>
- export default from './x'
- </script>`,
- {
- babelParserPlugins: ['exportDefaultFrom']
- }
+ describe('imports', () => {
+ test('should hoist and expose imports', () => {
+ assertCode(
+ compile(`<script setup>import { ref } from 'vue'</script>`).content
)
- assertCode(content)
- expect(bindings).toStrictEqual({})
})
- test(`export { x as default }`, () => {
- const { content, bindings } = compile(
- `<script setup>
- import x from './x'
- const y = 1
- export { x as default, y }
- </script>`
+ test('should extract comment for import or type declarations', () => {
+ assertCode(
+ compile(`<script setup>
+ import a from 'a' // comment
+ import b from 'b'
+ </script>`).content
)
- assertCode(content)
- expect(bindings).toStrictEqual({
- y: 'setup'
- })
})
- test(`export { x as default } from './x'`, () => {
- const { content, bindings } = compile(
- `<script setup>
- export { x as default, y } from './x'
- </script>`
- )
+ test('dedupe between user & helper', () => {
+ const { content } = compile(`<script setup>
+ import { ref } from 'vue'
+ ref: foo = 1
+ </script>`)
assertCode(content)
- expect(bindings).toStrictEqual({
- y: 'setup'
- })
+ expect(content).toMatch(`import { ref } from 'vue'`)
})
- test(`export * from './x'`, () => {
- const { content, bindings } = compile(
- `<script setup>
- export * from './x'
- export const y = 1
- </script>`
- )
+ test('import dedupe between <script> and <script setup>', () => {
+ const { content } = compile(`
+ <script>
+ import { x } from './x'
+ </script>
+ <script setup>
+ import { x } from './x'
+ x()
+ </script>
+ `)
assertCode(content)
- 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 { content, bindings } = compile(
- `<script setup>
- export default {
- props: ['foo']
- }
- export const y = 1
- </script>`
+ expect(content.indexOf(`import { x }`)).toEqual(
+ content.lastIndexOf(`import { x }`)
)
- assertCode(content)
- expect(bindings).toStrictEqual({
- foo: 'props',
- y: 'setup'
- })
})
})
describe('<script setup lang="ts">', () => {
test('hoist type declarations', () => {
- const { content, bindings } = compile(`
+ const { content } = compile(`
<script setup lang="ts">
export interface Foo {}
type Bar = {}
- export const a = 1
</script>`)
assertCode(content)
- expect(bindings).toStrictEqual({ a: 'setup' })
})
test('extract props', () => {
test('w/ <script setup>', () => {
assertCode(
compile(
- `<script setup>export const color = 'red'</script>\n` +
+ `<script setup>const color = 'red'</script>\n` +
`<style vars="{ color }">div{ color: var(--color); }</style>`
).content
)
assertAwaitDetection(`const a = 1 + (await foo)`)
})
- test('export', () => {
- assertAwaitDetection(`export const a = 1 + (await foo)`)
+ test('ref', () => {
+ assertAwaitDetection(`ref: a = 1 + (await foo)`)
})
test('nested statements', () => {
test('should ignore await inside functions', () => {
// function declaration
- assertAwaitDetection(`export async function foo() { await bar }`, false)
+ assertAwaitDetection(`async function foo() { await bar }`, false)
// function expression
assertAwaitDetection(`const foo = async () => { await bar }`, false)
// object method
})
})
+ describe('ref: syntax sugar', () => {
+ test('convert ref declarations', () => {
+ const { content, bindings } = compile(`<script setup>
+ ref: a = 1
+ ref: b = {
+ count: 0
+ }
+ let c = () => {}
+ let d
+ </script>`)
+ expect(content).toMatch(`import { ref } from 'vue'`)
+ expect(content).not.toMatch(`ref: a`)
+ expect(content).toMatch(`const a = ref(1)`)
+ expect(content).toMatch(`
+ const b = ref({
+ count: 0
+ })
+ `)
+ // normal declarations left untouched
+ expect(content).toMatch(`let c = () => {}`)
+ expect(content).toMatch(`let d`)
+ assertCode(content)
+ expect(bindings).toStrictEqual({
+ a: 'setup',
+ b: 'setup',
+ c: 'setup',
+ d: 'setup'
+ })
+ })
+
+ test('multi ref declarations', () => {
+ const { content, bindings } = compile(`<script setup>
+ ref: a = 1, b = 2, c = {
+ count: 0
+ }
+ </script>`)
+ expect(content).toMatch(`
+ const a = ref(1), b = ref(2), c = ref({
+ count: 0
+ })
+ `)
+ expect(content).toMatch(`return { a, b, c }`)
+ assertCode(content)
+ expect(bindings).toStrictEqual({
+ a: 'setup',
+ b: 'setup',
+ c: 'setup'
+ })
+ })
+
+ test('should not convert non ref labels', () => {
+ const { content } = compile(`<script setup>
+ foo: a = 1, b = 2, c = {
+ count: 0
+ }
+ </script>`)
+ expect(content).toMatch(`foo: a = 1, b = 2`)
+ assertCode(content)
+ })
+
+ test('accessing ref binding', () => {
+ const { content } = compile(`<script setup>
+ ref: a = 1
+ console.log(a)
+ function get() {
+ return a + 1
+ }
+ </script>`)
+ expect(content).toMatch(`console.log(a.value)`)
+ expect(content).toMatch(`return a.value + 1`)
+ assertCode(content)
+ })
+
+ test('cases that should not append .value', () => {
+ const { content } = compile(`<script setup>
+ ref: a = 1
+ console.log(b.a)
+ function get(a) {
+ return a + 1
+ }
+ </script>`)
+ expect(content).not.toMatch(`a.value`)
+ })
+
+ test('mutating ref binding', () => {
+ const { content } = compile(`<script setup>
+ ref: a = 1
+ ref: b = { count: 0 }
+ function inc() {
+ a++
+ a = a + 1
+ b.count++
+ b.count = b.count + 1
+ }
+ </script>`)
+ expect(content).toMatch(`a.value++`)
+ expect(content).toMatch(`a.value = a.value + 1`)
+ expect(content).toMatch(`b.value.count++`)
+ expect(content).toMatch(`b.value.count = b.value.count + 1`)
+ assertCode(content)
+ })
+
+ test('using ref binding in property shorthand', () => {
+ const { content } = compile(`<script setup>
+ ref: a = 1
+ const b = { a }
+ function test() {
+ const { a } = b
+ }
+ </script>`)
+ expect(content).toMatch(`const b = { a: a.value }`)
+ // should not convert destructure
+ expect(content).toMatch(`const { a } = b`)
+ assertCode(content)
+ })
+
+ test('object destructure', () => {
+ const { content, bindings } = compile(`<script setup>
+ ref: n = 1, ({ a, b: c, d = 1, e: f = 2, ...g } = useFoo())
+ console.log(n, a, c, d, f, g)
+ </script>`)
+ expect(content).toMatch(
+ `const n = ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = useFoo()`
+ )
+ expect(content).toMatch(`\nconst a = ref(__a);`)
+ expect(content).not.toMatch(`\nconst b = ref(__b);`)
+ expect(content).toMatch(`\nconst c = ref(__c);`)
+ expect(content).toMatch(`\nconst d = ref(__d);`)
+ expect(content).not.toMatch(`\nconst e = ref(__e);`)
+ expect(content).toMatch(`\nconst f = ref(__f);`)
+ expect(content).toMatch(`\nconst g = ref(__g);`)
+ expect(content).toMatch(
+ `console.log(n.value, a.value, c.value, d.value, f.value, g.value)`
+ )
+ expect(content).toMatch(`return { n, a, c, d, f, g }`)
+ expect(bindings).toStrictEqual({
+ n: 'setup',
+ a: 'setup',
+ c: 'setup',
+ d: 'setup',
+ f: 'setup',
+ g: 'setup'
+ })
+ assertCode(content)
+ })
+
+ test('array destructure', () => {
+ const { content, bindings } = compile(`<script setup>
+ ref: n = 1, [a, b = 1, ...c] = useFoo()
+ console.log(n, a, b, c)
+ </script>`)
+ expect(content).toMatch(
+ `const n = ref(1), [__a, __b = 1, ...__c] = useFoo()`
+ )
+ expect(content).toMatch(`\nconst a = ref(__a);`)
+ expect(content).toMatch(`\nconst b = ref(__b);`)
+ expect(content).toMatch(`\nconst c = ref(__c);`)
+ expect(content).toMatch(`console.log(n.value, a.value, b.value, c.value)`)
+ expect(content).toMatch(`return { n, a, b, c }`)
+ expect(bindings).toStrictEqual({
+ n: 'setup',
+ a: 'setup',
+ b: 'setup',
+ c: 'setup'
+ })
+ assertCode(content)
+ })
+
+ test('nested destructure', () => {
+ const { content, bindings } = compile(`<script setup>
+ ref: [{ a: { b }}] = useFoo()
+ ref: ({ c: [d, e] } = useBar())
+ console.log(b, d, e)
+ </script>`)
+ expect(content).toMatch(`const [{ a: { b: __b }}] = useFoo()`)
+ expect(content).toMatch(`const { c: [__d, __e] } = useBar()`)
+ expect(content).not.toMatch(`\nconst a = ref(__a);`)
+ expect(content).not.toMatch(`\nconst c = ref(__c);`)
+ expect(content).toMatch(`\nconst b = ref(__b);`)
+ expect(content).toMatch(`\nconst d = ref(__d);`)
+ expect(content).toMatch(`\nconst e = ref(__e);`)
+ expect(content).toMatch(`return { b, d, e }`)
+ expect(bindings).toStrictEqual({
+ b: 'setup',
+ d: 'setup',
+ e: 'setup'
+ })
+ assertCode(content)
+ })
+ })
+
describe('errors', () => {
test('<script> and <script setup> must have same lang', () => {
expect(() =>
).toThrow(`<script> and <script setup> must have the same language type`)
})
- test('export local as default', () => {
+ test('non-type named exports', () => {
+ expect(() =>
+ compile(`<script setup>
+ export const a = 1
+ </script>`)
+ ).toThrow(`cannot contain non-type named exports`)
+
expect(() =>
compile(`<script setup>
const bar = 1
export { bar as default }
</script>`)
- ).toThrow(`Cannot export locally defined variable as default`)
+ ).toThrow(`cannot contain non-type named exports`)
+ })
+
+ test('ref: non-assignment expressions', () => {
+ expect(() =>
+ compile(`<script setup>
+ ref: a = 1, foo()
+ </script>`)
+ ).toThrow(`ref: statements can only contain assignment expressions`)
})
test('export default referencing local var', () => {
).toThrow(`cannot reference locally declared variables`)
})
- test('export default referencing exports', () => {
+ test('export default referencing ref declarations', () => {
expect(() =>
compile(`<script setup>
- export const bar = 1
+ ref: bar = 1
export default {
props: bar
}
assertCode(
compile(`<script setup>
import { bar } from './bar'
- export { bar }
- export default {
- props: {
- foo: {
- default: () => bar
- }
- }
- }
- </script>`).content
- )
- })
-
- test('should allow export default referencing re-exported binding', () => {
- assertCode(
- compile(`<script setup>
- export { bar } from './bar'
export default {
props: {
foo: {
`)
).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>
TSDeclareFunction,
ObjectProperty,
ArrayExpression,
- Statement
+ Statement,
+ Expression,
+ LabeledStatement
} from '@babel/types'
import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map'
* https://babeljs.io/docs/en/babel-parser#plugins
*/
babelParserPlugins?: ParserPlugin[]
+ refSugar?: boolean
}
let hasWarned = false
}
const defaultTempVar = `__default__`
- const bindings: BindingMetadata = {}
- const imports: Record<string, string> = {}
- const setupScopeVars: Record<string, boolean> = {}
- const setupExports: Record<string, boolean> = {}
- let exportAllIndex = 0
+ const bindingMetadata: BindingMetadata = {}
+ const helperImports: Set<string> = new Set()
+ const userImports: Record<string, string> = Object.create(null)
+ const setupBindings: Record<string, boolean> = Object.create(null)
+ const refBindings: Record<string, boolean> = Object.create(null)
+ const refIdentifiers: Set<Identifier> = new Set()
+ const enableRefSugar = options.refSugar !== false
let defaultExport: Node | undefined
let needDefaultExportRefCheck = false
let hasAwait = 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 endOffset = scriptSetup.loc.end.offset
const scriptStartOffset = script && script.loc.start.offset
const scriptEndOffset = script && script.loc.end.offset
- let scriptAst
+ function error(
+ msg: string,
+ node: Node,
+ end: number = node.end! + startOffset
+ ) {
+ throw new Error(
+ msg + `\n\n` + generateCodeFrame(source, node.start! + startOffset, end)
+ )
+ }
+
+ function processRefExpression(exp: Expression, statement: LabeledStatement) {
+ if (exp.type === 'AssignmentExpression') {
+ helperImports.add('ref')
+ const { left, right } = exp
+ if (left.type === 'Identifier') {
+ if (left.name[0] === '$') {
+ error(`ref variable identifiers cannot start with $.`, left)
+ }
+ refBindings[left.name] = setupBindings[left.name] = true
+ refIdentifiers.add(left)
+ s.prependRight(right.start! + startOffset, `ref(`)
+ s.appendLeft(right.end! + startOffset, ')')
+ } else if (left.type === 'ObjectPattern') {
+ // remove wrapping parens
+ for (let i = left.start!; i > 0; i--) {
+ const char = source[i + startOffset]
+ if (char === '(') {
+ s.remove(i + startOffset, i + startOffset + 1)
+ break
+ }
+ }
+ for (let i = left.end!; i > 0; i++) {
+ const char = source[i + startOffset]
+ if (char === ')') {
+ s.remove(i + startOffset, i + startOffset + 1)
+ break
+ }
+ }
+ processRefObjectPattern(left, statement)
+ } else if (left.type === 'ArrayPattern') {
+ processRefArrayPattern(left, statement)
+ }
+ } else if (exp.type === 'SequenceExpression') {
+ // possible multiple declarations
+ // ref: x = 1, y = 2
+ exp.expressions.forEach(e => processRefExpression(e, statement))
+ } else {
+ error(`ref: statements can only contain assignment expressions.`, exp)
+ }
+ }
+
+ function processRefObjectPattern(
+ pattern: ObjectPattern,
+ statement: LabeledStatement
+ ) {
+ for (const p of pattern.properties) {
+ let nameId: Identifier | undefined
+ if (p.type === 'ObjectProperty') {
+ if (p.key.start! === p.value.start!) {
+ // shorthand { foo } --> { foo: __foo }
+ nameId = p.key as Identifier
+ s.appendLeft(nameId.end! + startOffset, `: __${nameId.name}`)
+ if (p.value.type === 'AssignmentPattern') {
+ // { foo = 1 }
+ refIdentifiers.add(p.value.left as Identifier)
+ }
+ } else {
+ if (p.value.type === 'Identifier') {
+ // { foo: bar } --> { foo: __bar }
+ nameId = p.value
+ s.prependRight(nameId.start! + startOffset, `__`)
+ } else if (p.value.type === 'ObjectPattern') {
+ processRefObjectPattern(p.value, statement)
+ } else if (p.value.type === 'ArrayPattern') {
+ processRefArrayPattern(p.value, statement)
+ } else if (p.value.type === 'AssignmentPattern') {
+ // { foo: bar = 1 } --> { foo: __bar = 1 }
+ nameId = p.value.left as Identifier
+ s.prependRight(nameId.start! + startOffset, `__`)
+ }
+ }
+ } else {
+ // rest element { ...foo } --> { ...__foo }
+ nameId = p.argument as Identifier
+ s.prependRight(nameId.start! + startOffset, `__`)
+ }
+ if (nameId) {
+ // register binding
+ refBindings[nameId.name] = setupBindings[nameId.name] = true
+ refIdentifiers.add(nameId)
+ // append binding declarations after the parent statement
+ s.appendLeft(
+ statement.end! + startOffset,
+ `\nconst ${nameId.name} = ref(__${nameId.name});`
+ )
+ }
+ }
+ }
+
+ function processRefArrayPattern(
+ pattern: ArrayPattern,
+ statement: LabeledStatement
+ ) {
+ for (const e of pattern.elements) {
+ if (!e) continue
+ let nameId: Identifier | undefined
+ if (e.type === 'Identifier') {
+ // [a] --> [__a]
+ nameId = e
+ } else if (e.type === 'AssignmentPattern') {
+ // [a = 1] --> [__a = 1]
+ nameId = e.left as Identifier
+ } else if (e.type === 'RestElement') {
+ // [...a] --> [...__a]
+ nameId = e.argument as Identifier
+ } else if (e.type === 'ObjectPattern') {
+ processRefObjectPattern(e, statement)
+ } else if (e.type === 'ArrayPattern') {
+ processRefArrayPattern(e, statement)
+ }
+ if (nameId) {
+ s.prependRight(nameId.start! + startOffset, `__`)
+ // register binding
+ refBindings[nameId.name] = setupBindings[nameId.name] = true
+ refIdentifiers.add(nameId)
+ // append binding declarations after the parent statement
+ s.appendLeft(
+ statement.end! + startOffset,
+ `\nconst ${nameId.name} = ref(__${nameId.name});`
+ )
+ }
+ }
+ }
// 1. process normal <script> first if it exists
+ let scriptAst
if (script) {
// import dedupe between <script> and <script setup>
scriptAst = parse(script.content, {
for (const {
local: { name }
} of node.specifiers) {
- imports[name] = node.source.value
+ userImports[name] = node.source.value
}
} else if (node.type === 'ExportDefaultDeclaration') {
// export default
end++
}
+ // process `ref: x` bindings (convert to refs)
+ if (
+ enableRefSugar &&
+ node.type === 'LabeledStatement' &&
+ node.label.name === 'ref' &&
+ node.body.type === 'ExpressionStatement'
+ ) {
+ s.overwrite(
+ node.label.start! + startOffset,
+ node.body.start! + startOffset,
+ 'const '
+ )
+ processRefExpression(node.body.expression, node)
+ }
+
if (node.type === 'ImportDeclaration') {
// import declarations are moved to top
s.move(start, end, 0)
let prev
let removed = 0
for (const specifier of node.specifiers) {
- if (imports[specifier.local.name]) {
+ if (userImports[specifier.local.name]) {
// already imported in <script setup>, dedupe
removed++
s.remove(
specifier.end! + startOffset
)
} else {
- imports[specifier.local.name] = node.source.value
+ userImports[specifier.local.name] = node.source.value
}
prev = specifier
}
}
if (node.type === 'ExportNamedDeclaration' && node.exportKind !== 'type') {
- // named exports
- if (node.declaration) {
- // variable/function/class declarations.
- // remove leading `export ` keyword
- s.remove(start, start + 7)
- walkDeclaration(node.declaration, setupExports)
- }
- if (node.specifiers.length) {
- // named export with specifiers
- if (node.source) {
- // export { x } from './x'
- // change it to import and move to top
- s.overwrite(start, start + 6, 'import')
- s.move(start, end, 0)
- } else {
- // export { x }
- s.remove(start, end)
- }
- for (const specifier of node.specifiers) {
- 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,
- defaultTempVar
- )
- } else if (
- specifier.type === 'ExportSpecifier' &&
- specifier.exported.type === 'Identifier'
- ) {
- if (specifier.exported.name === 'default') {
- checkDuplicateDefaultExport(node)
- defaultExport = node
- // 1. remove specifier
- if (node.specifiers.length > 1) {
- // 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!)
- }
- if (!node.source) {
- // export { x as default, ... }
- const local = specifier.local.name
- if (setupScopeVars[local] || setupExports[local]) {
- throw new Error(
- `Cannot export locally defined variable as default in <script setup>.\n` +
- `Default export must be an object literal with no reference to local scope.\n` +
- generateCodeFrame(
- source,
- specifier.start! + startOffset,
- specifier.end! + startOffset
- )
- )
- }
- // rewrite to `const __default__ = x` and move to end
- s.append(`\nconst ${defaultTempVar} = ${local}\n`)
- } else {
- // export { x as default } from './x'
- // rewrite to `import { x as __default__ } from './x'` and
- // add to top
- s.prepend(
- `import { ${
- specifier.local.name
- } as ${defaultTempVar} } from '${node.source.value}'\n`
- )
- }
- } else {
- setupExports[specifier.exported.name] = true
- if (node.source) {
- imports[specifier.exported.name] = node.source.value
- }
- }
- }
- }
- }
+ // TODO warn
+ error(`<script setup> cannot contain non-type named exports.`, node)
}
if (node.type === 'ExportAllDeclaration') {
- // export * from './x'
- s.overwrite(
- start,
- node.source.start! + startOffset,
- `import * as __export_all_${exportAllIndex++}__ from `
- )
- s.move(start, end, 0)
+ // TODO warn
}
if (node.type === 'ExportDefaultDeclaration') {
- checkDuplicateDefaultExport(node)
+ if (defaultExport) {
+ // <script> already has export default
+ error(
+ `Default export is already declared in normal <script>.`,
+ node,
+ node.start! + startOffset + `export default`.length
+ )
+ }
// export default {} inside <script setup>
// this should be kept in module scope - move it to the end
s.move(start, end, source.length)
node.type === 'ClassDeclaration') &&
!node.declare
) {
- walkDeclaration(node, setupScopeVars)
+ walkDeclaration(node, setupBindings)
}
// Type declarations
// await
if (
node.type === 'VariableDeclaration' ||
- (node.type === 'ExportNamedDeclaration' &&
- node.declaration &&
- node.declaration.type === 'VariableDeclaration') ||
node.type.endsWith('Statement')
) {
;(walk as any)(node, {
}
}
- // 4. check default export to make sure it doesn't reference setup scope
+ // 4. Do a full walk to rewrite identifiers referencing let exports with ref
+ // value access
+ if (enableRefSugar && Object.keys(refBindings).length) {
+ for (const node of scriptSetupAst) {
+ if (node.type !== 'ImportDeclaration') {
+ walkIdentifiers(node, (id, parent) => {
+ if (refBindings[id.name] && !refIdentifiers.has(id)) {
+ if (isStaticProperty(parent) && parent.shorthand) {
+ // let binding used in a property shorthand
+ // { foo } -> { foo: foo.value }
+ // skip for destructure patterns
+ if (!(parent as any).inPattern) {
+ s.appendLeft(id.end! + startOffset, `: ${id.name}.value`)
+ }
+ } else {
+ s.appendLeft(id.end! + startOffset, '.value')
+ }
+ } else if (id.name[0] === '$' && refBindings[id.name.slice(1)]) {
+ // $xxx raw ref access variables, remove the $ prefix
+ s.remove(id.start! + startOffset, id.start! + startOffset + 1)
+ }
+ })
+ }
+ }
+ }
+
+ // 5. check default export to make sure it doesn't reference setup scope
// variables
if (needDefaultExportRefCheck) {
- checkDefaultExport(
- defaultExport!,
- setupScopeVars,
- imports,
- setupExports,
- source,
- startOffset
- )
+ walkIdentifiers(defaultExport!, id => {
+ if (setupBindings[id.name]) {
+ error(
+ `\`export default\` in <script setup> cannot reference locally ` +
+ `declared variables because it will be hoisted outside of the ` +
+ `setup() function. If your component options requires initialization ` +
+ `in the module scope, use a separate normal <script> to export ` +
+ `the options instead.`,
+ id
+ )
+ }
+ })
}
- // 5. remove non-script content
+ // 6. remove non-script content
if (script) {
if (startOffset < scriptStartOffset!) {
// <script setup> before <script>
s.remove(endOffset, source.length)
}
- // 5. finalize setup argument signature.
+ // 7. finalize setup argument signature.
let args = ``
if (isTS) {
if (slotsType === '__Slots__') {
- s.prepend(`import { Slots as __Slots__ } from 'vue'\n`)
+ helperImports.add('Slots')
}
const ctxType = `{
emit: ${emitType},
args = hasExplicitSignature ? (setupValue as string) : ``
}
- // 6. wrap setup code with function.
+ // 8. 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.
s.prependLeft(
)
// generate return statement
- let returned = `{ ${Object.keys(setupExports).join(', ')} }`
-
- // handle `export * from`. We need to call `toRefs` on the imported module
- // object before merging.
- if (exportAllIndex > 0) {
- s.prepend(`import { toRefs as __toRefs__ } from 'vue'\n`)
- for (let i = 0; i < exportAllIndex; i++) {
- returned += `,\n __toRefs__(__export_all_${i}__)`
- }
- returned = `Object.assign(\n ${returned}\n)`
- }
+ const exposedBindings = { ...userImports, ...setupBindings }
+ let returned = `{ ${Object.keys(exposedBindings).join(', ')} }`
// inject `useCssVars` calls
if (hasCssVars) {
- s.prepend(`import { useCssVars as __useCssVars__ } from 'vue'\n`)
+ helperImports.add(`useCssVars`)
for (const style of styles) {
const vars = style.attrs.vars
if (typeof vars === 'string') {
s.prependRight(
endOffset,
- `\n${genCssVarsCode(vars, !!style.scoped, setupExports)}`
+ `\n${genCssVarsCode(vars, !!style.scoped, exposedBindings)}`
)
}
}
s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
- // 7. finalize default export
+ // 9. finalize default export
if (isTS) {
// for TS, make sure the exported type is still valid type with
// correct props information
- s.prepend(`import { defineComponent as __define__ } from 'vue'\n`)
+ helperImports.add(`defineComponent`)
// 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},` : ``
const runtimeProps = genRuntimeProps(typeDeclaredProps)
const runtimeEmits = genRuntimeEmits(typeDeclaredEmits)
s.append(
- `export default __define__({${def}${runtimeProps}${runtimeEmits}\n setup\n})`
+ `export default __defineComponent__({${def}${runtimeProps}${runtimeEmits}\n setup\n})`
)
} else {
if (defaultExport) {
}
}
- // 8. expose bindings for template compiler optimization
+ // 10. finalize Vue helper imports
+ const helpers = [...helperImports].filter(i => userImports[i] !== 'vue')
+ if (helpers.length) {
+ s.prepend(`import { ${helpers.join(', ')} } from 'vue'\n`)
+ }
+
+ // 11. expose bindings for template compiler optimization
if (scriptAst) {
- Object.assign(bindings, analyzeScriptBindings(scriptAst))
+ Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
}
- Object.keys(setupExports).forEach(key => {
- bindings[key] = 'setup'
+ Object.keys(exposedBindings).forEach(key => {
+ bindingMetadata[key] = 'setup'
})
Object.keys(typeDeclaredProps).forEach(key => {
- bindings[key] = 'props'
+ bindingMetadata[key] = 'props'
})
- Object.assign(bindings, analyzeScriptBindings(scriptSetupAst))
+ Object.assign(bindingMetadata, analyzeScriptBindings(scriptSetupAst))
s.trim()
return {
...scriptSetup,
- bindings,
+ bindings: bindingMetadata,
content: s.toString(),
map: (s.generateMap({
source: filename,
}
/**
- * export default {} inside `<script setup>` cannot access variables declared
- * inside since it's hoisted. Walk and check to make sure.
+ * Walk an AST and find identifiers that are variable references.
+ * This is largely the same logic with `transformExpressions` in compiler-core
+ * but with some subtle differences as this needs to handle a wider range of
+ * possible syntax.
*/
-function checkDefaultExport(
+function walkIdentifiers(
root: Node,
- scopeVars: Record<string, boolean>,
- imports: Record<string, string>,
- exports: Record<string, boolean>,
- source: string,
- offset: number
+ onIdentifier: (node: Identifier, parent: Node) => void
) {
const knownIds: Record<string, number> = Object.create(null)
;(walk as any)(root, {
enter(node: Node & { scopeIds?: Set<string> }, parent: Node) {
if (node.type === 'Identifier') {
- if (
- !knownIds[node.name] &&
- !isStaticPropertyKey(node, parent) &&
- (scopeVars[node.name] || (!imports[node.name] && exports[node.name]))
- ) {
- throw new Error(
- `\`export default\` in <script setup> cannot reference locally ` +
- `declared variables because it will be hoisted outside of the ` +
- `setup() function. If your component options requires initialization ` +
- `in the module scope, use a separate normal <script> to export ` +
- `the options instead.\n\n` +
- generateCodeFrame(
- source,
- node.start! + offset,
- node.end! + offset
- )
- )
+ if (!knownIds[node.name] && isRefIdentifier(node, parent)) {
+ onIdentifier(node, parent)
}
} else if (isFunction(node)) {
// walk function expressions and add its arguments to known identifiers
}
})
)
+ } else if (
+ node.type === 'ObjectProperty' &&
+ parent.type === 'ObjectPattern'
+ ) {
+ // mark property in destructure pattern
+ ;(node as any).inPattern = true
}
},
leave(node: Node & { scopeIds?: Set<string> }) {
})
}
-function isStaticPropertyKey(node: Node, parent: Node): boolean {
- return (
- parent &&
- (parent.type === 'ObjectProperty' || parent.type === 'ObjectMethod') &&
- !parent.computed &&
- parent.key === node
- )
+function isRefIdentifier(id: Identifier, parent: Node) {
+ // declaration id
+ if (
+ (parent.type === 'VariableDeclarator' ||
+ parent.type === 'ClassDeclaration') &&
+ parent.id === id
+ ) {
+ return false
+ }
+
+ if (isFunction(parent)) {
+ // function decalration/expression id
+ if ((parent as any).id === id) {
+ return false
+ }
+ // params list
+ if (parent.params.includes(id)) {
+ return false
+ }
+ }
+
+ // property key
+ // this also covers object destructure pattern
+ if (isStaticPropertyKey(id, parent)) {
+ return false
+ }
+
+ // array destructure pattern
+ if (parent.type === 'ArrayPattern') {
+ return false
+ }
+
+ // member expression property
+ if (
+ (parent.type === 'MemberExpression' ||
+ parent.type === 'OptionalMemberExpression') &&
+ parent.property === id &&
+ !parent.computed
+ ) {
+ return false
+ }
+
+ // is a special keyword but parsed as identifier
+ if (id.name === 'arguments') {
+ return false
+ }
+
+ return true
}
+const isStaticProperty = (node: Node): node is ObjectProperty =>
+ node &&
+ (node.type === 'ObjectProperty' || node.type === 'ObjectMethod') &&
+ !node.computed
+
+const isStaticPropertyKey = (node: Node, parent: Node) =>
+ isStaticProperty(parent) && parent.key === node
+
function isFunction(node: Node): node is FunctionNode {
return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
}
export function genCssVarsCode(
varsExp: string,
scoped: boolean,
- knownBindings?: Record<string, boolean>
+ knownBindings?: Record<string, string | boolean>
) {
const exp = createSimpleExpression(varsExp, false)
const context = createTransformContext(createRoot([]), {