feat(compiler-sfc): new script setup implementation

- now exposes all top level bindings to template
- support `ref:` syntax sugar
This commit is contained in:
Evan You
2020-10-29 15:03:39 -04:00
parent 0935c0501b
commit 556560fae3
5 changed files with 810 additions and 609 deletions

View File

@@ -1,8 +1,7 @@
// 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
@@ -16,15 +15,14 @@ export function setup(_: {}, { emit: myEmit }: {
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[]
@@ -59,7 +57,7 @@ export function setup(myProps: {
return { }
}
export default __define__({
export default __defineComponent__({
props: {
string: { type: String, required: true },
number: { type: Number, required: true },
@@ -88,19 +86,17 @@ export default __define__({
`;
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
})"
`;
@@ -149,7 +145,7 @@ export default __default__"
`;
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'
@@ -166,26 +162,6 @@ exports[`SFC compile <script setup> errors should allow export default referenci
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 }
}
@@ -205,7 +181,7 @@ exports[`SFC compile <script setup> errors should allow export default referenci
const bar = 1
return { }
return { bar }
}
const __default__ = {
@@ -228,175 +204,220 @@ 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'
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() {
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()
exports[`SFC compile <script setup> imports import dedupe between <script> and <script setup> 1`] = `
"import { x } from './x'
return { a, b, c, d, e, f }
}
export function setup() {
export default { setup }"
`;
exports[`SFC compile <script setup> exports export const x = ... 1`] = `
"export function setup() {
const x = 1
x()
return { x }
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export default from './x' 1`] = `
"import __default__ 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 { a, b }
}
export default { setup }"
`;
exports[`SFC compile <script setup> imports should hoist and expose imports 1`] = `
"import { ref } from 'vue'
export function setup() {
return { ref }
}
export default { setup }"
`;
exports[`SFC compile <script setup> ref: syntax sugar accessing ref binding 1`] = `
"import { ref } from 'vue'
export function setup() {
const a = ref(1)
console.log(a.value)
function get() {
return a.value + 1
}
return { a, get }
}
export default { setup }"
`;
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> ref: syntax sugar convert ref declarations 1`] = `
"import { ref } from 'vue'
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> 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> ref: syntax sugar mutating ref binding 1`] = `
"import { ref } from 'vue'
export function setup() {
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 }
}
export default { setup }"
`;
exports[`SFC compile <script setup> ref: syntax sugar nested destructure 1`] = `
"import { ref } from 'vue'
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 }
}
export default { setup }"
`;
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> ref: syntax sugar should not convert non ref labels 1`] = `
"export function setup() {
foo: a = 1, b = 2, c = {
count: 0
}
return { }
}
__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 using ref binding in property shorthand 1`] = `
"import { ref } from 'vue'
const y = 1
return { y }
}
export function setup() {
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 }
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> import dedupe between <script> and <script setup> 1`] = `
exports[`SFC compile <script setup> should expose top level declarations 1`] = `
"import { x } from './x'
export function setup() {
x()
let a = 1
const b = 2
function c() {}
class d {}
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'
export function setup() {
return { }
}
export default { setup }"
`;
exports[`SFC compile <script setup> should hoist imports 1`] = `
"import { ref } from 'vue'
export function setup() {
return { }
return { x, a, b, c, d }
}
export default { setup }"

View File

@@ -22,199 +22,76 @@ function assertCode(code: string) {
}
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>`
describe('imports', () => {
test('should hoist and expose imports', () => {
assertCode(
compile(`<script setup>import { ref } from 'vue'</script>`).content
)
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('should extract comment for import or type declarations', () => {
assertCode(
compile(`<script setup>
import a from 'a' // comment
import b from 'b'
</script>`).content
)
})
test('export function x() {}', () => {
const { content, bindings } = compile(
`<script setup>export function 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({
x: 'setup'
})
expect(content).toMatch(`import { ref } from 'vue'`)
})
test('export class X() {}', () => {
const { content, bindings } = compile(
`<script setup>export class X {}</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({
X: 'setup'
})
})
test('export { x }', () => {
const { content, bindings } = compile(
`<script setup>
const x = 1
const y = 2
export { x, y }
</script>`
expect(content.indexOf(`import { x }`)).toEqual(
content.lastIndexOf(`import { x }`)
)
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']
}
)
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>`
)
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>`
)
assertCode(content)
expect(bindings).toStrictEqual({
y: 'setup'
})
})
test(`export * from './x'`, () => {
const { content, bindings } = compile(
`<script setup>
export * from './x'
export const y = 1
</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>`
)
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', () => {
@@ -333,7 +210,7 @@ import b from 'b'
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
)
@@ -356,8 +233,8 @@ import b from 'b'
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', () => {
@@ -366,7 +243,7 @@ import b from 'b'
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
@@ -379,6 +256,197 @@ import b from 'b'
})
})
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(() =>
@@ -386,13 +454,27 @@ import b from 'b'
).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', () => {
@@ -410,10 +492,10 @@ import b from 'b'
).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
}
@@ -440,22 +522,6 @@ import b from 'b'
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: {
@@ -479,29 +545,6 @@ import b from 'b'
`)
).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>