import { parse, SFCScriptCompileOptions, compileScript } from '../src' import { parse as babelParse } from '@babel/parser' import { babelParserDefaultPlugins } from '@vue/shared' function compile(src: string, options?: SFCScriptCompileOptions) { const { descriptor } = parse(src) return compileScript(descriptor, options) } function assertCode(code: string) { // parse the generated code to make sure it is valid try { babelParse(code, { sourceType: 'module', plugins: [...babelParserDefaultPlugins, 'typescript'] }) } catch (e) { console.log(code) throw e } expect(code).toMatchSnapshot() } describe('SFC compile `).content ) }) test('should expose top level declarations', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch('return { x, a, b, c, d }') }) describe('imports', () => { test('should hoist and expose imports', () => { assertCode( compile(``).content ) }) test('should extract comment for import or type declarations', () => { assertCode( compile(``).content ) }) test('dedupe between user & helper', () => { const { content } = compile(``) assertCode(content) expect(content).toMatch(`import { ref } from 'vue'`) }) test('import dedupe between `) assertCode(content) expect(content.indexOf(`import { x }`)).toEqual( content.lastIndexOf(`import { x }`) ) }) }) describe('`) assertCode(content) }) test('extract props', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch(`string: { type: String, required: true }`) expect(content).toMatch(`number: { type: Number, required: true }`) expect(content).toMatch(`boolean: { type: Boolean, required: true }`) expect(content).toMatch(`object: { type: Object, required: true }`) expect(content).toMatch(`objectLiteral: { type: Object, required: true }`) expect(content).toMatch(`fn: { type: Function, required: true }`) expect(content).toMatch(`functionRef: { type: Function, required: true }`) expect(content).toMatch(`objectRef: { type: Object, required: true }`) expect(content).toMatch(`array: { type: Array, required: true }`) expect(content).toMatch(`arrayRef: { type: Array, required: true }`) expect(content).toMatch(`tuple: { type: Array, required: true }`) expect(content).toMatch(`set: { type: Set, required: true }`) expect(content).toMatch(`literal: { type: String, required: true }`) expect(content).toMatch(`optional: { type: null, required: false }`) expect(content).toMatch(`recordRef: { type: Object, required: true }`) expect(content).toMatch(`interface: { type: Object, required: true }`) expect(content).toMatch(`alias: { type: Array, required: true }`) expect(content).toMatch( `union: { type: [String, Number], required: true }` ) expect(content).toMatch( `literalUnion: { type: [String, String], required: true }` ) expect(content).toMatch( `literalUnionMixed: { type: [String, Number, Boolean], required: true }` ) expect(content).toMatch(`intersection: { type: Object, required: true }`) }) test('extract emits', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch( `declare function __emit__(e: 'foo' | 'bar'): void` ) expect(content).toMatch( `declare function __emit__(e: 'baz', id: number): void` ) expect(content).toMatch( `emits: ["foo", "bar", "baz"] as unknown as undefined` ) }) }) describe('CSS vars injection', () => { test('\n` + `` ).content ) }) test('\n` + `` ).content ) }) test('\n` + `` ).content ) }) test('w/ \n` + `` ).content ) }) }) describe('async/await detection', () => { function assertAwaitDetection(code: string, shouldAsync = true) { const { content } = compile(``) expect(content).toMatch( `export ${shouldAsync ? `async ` : ``}function setup` ) } test('expression statement', () => { assertAwaitDetection(`await foo`) }) test('variable', () => { assertAwaitDetection(`const a = 1 + (await foo)`) }) test('ref', () => { assertAwaitDetection(`ref: a = 1 + (await foo)`) }) test('nested statements', () => { assertAwaitDetection(`if (ok) { await foo } else { await bar }`) }) test('should ignore await inside functions', () => { // function declaration assertAwaitDetection(`async function foo() { await bar }`, false) // function expression assertAwaitDetection(`const foo = async () => { await bar }`, false) // object method assertAwaitDetection(`const obj = { async method() { await bar }}`, false) // class method assertAwaitDetection( `const cls = class Foo { async method() { await bar }}`, false ) }) }) describe('ref: syntax sugar', () => { test('convert ref declarations', () => { const { content, bindings } = compile(``) 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(``) 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(``) expect(content).toMatch(`foo: a = 1, b = 2`) assertCode(content) }) test('accessing ref binding', () => { const { content } = compile(``) 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(``) expect(content).not.toMatch(`a.value`) }) test('mutating ref binding', () => { const { content } = compile(``) 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(``) 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(``) 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(``) 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(``) 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('`) ).toThrow(``) ).toThrow(`cannot contain non-type named exports`) expect(() => compile(``) ).toThrow(`cannot contain non-type named exports`) }) test('ref: non-assignment expressions', () => { expect(() => compile(``) ).toThrow(`ref: statements can only contain assignment expressions`) }) test('export default referencing local var', () => { expect(() => compile(``) ).toThrow(`cannot reference locally declared variables`) }) test('export default referencing ref declarations', () => { expect(() => compile(``) ).toThrow(`cannot reference locally declared variables`) }) test('should allow export default referencing scope var', () => { assertCode( compile(``).content ) }) test('should allow export default referencing imported binding', () => { assertCode( compile(``).content ) }) test('error on duplicated default export', () => { expect(() => compile(` `) ).toThrow(`Default export is already declared`) expect(() => compile(` `) ).toThrow(`Default export is already declared`) expect(() => compile(` `) ).toThrow(`Default export is already declared`) }) }) }) describe('SFC analyze `) expect(scriptAst).toBeDefined() }) it('recognizes props array declaration', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: 'props', bar: 'props' }) }) it('recognizes props object declaration', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: 'props', bar: 'props', baz: 'props', qux: 'props' }) }) it('recognizes setup return', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: 'setup', bar: 'setup' }) }) it('recognizes async setup return', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: 'setup', bar: 'setup' }) }) it('recognizes data return', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: 'data', bar: 'data' }) }) it('recognizes methods', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: 'options' }) }) it('recognizes computeds', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: 'options', bar: 'options' }) }) it('recognizes injections array declaration', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: 'options', bar: 'options' }) }) it('recognizes injections object declaration', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: 'options', bar: 'options' }) }) it('works for mixed bindings', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: 'options', bar: 'props', baz: 'setup', qux: 'data', quux: 'options', quuz: 'options' }) }) it('works for script setup', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: 'props' }) }) })