diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index a671c563f70..c2f00a4ce27 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -161,9 +161,9 @@ export function processExpression( 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) @@ -278,46 +278,65 @@ const isStaticProperty = (node: Node): node is ObjectProperty => (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 } diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap index a6bb93e7405..69937bc9e79 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap @@ -1,8 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SFC compile `).content - ) - }) - - test('should extract comment for import or type declarations', () => { - assertCode( - compile(``).content - ) - }) - test('explicit setup signature', () => { assertCode( compile(``).content ) }) - test('import dedupe between `) 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( - `` - ) - assertCode(content) - expect(bindings).toStrictEqual({ - x: 'setup' - }) - }) - - test('export const { x } = ... (destructuring)', () => { - const { content, bindings } = compile(``) - 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( - `` - ) - assertCode(content) - expect(bindings).toStrictEqual({ - x: 'setup' - }) - }) - - test('export class X() {}', () => { - const { content, bindings } = compile( - `` - ) - assertCode(content) - expect(bindings).toStrictEqual({ - X: 'setup' - }) - }) - - test('export { x }', () => { - const { content, bindings } = compile( - `` - ) - assertCode(content) - expect(bindings).toStrictEqual({ - x: 'setup', - y: 'setup' - }) - }) - - test(`export { x } from './x'`, () => { - const { content, bindings } = compile( - `` - ) - assertCode(content) - expect(bindings).toStrictEqual({ - x: 'setup', - y: 'setup' - }) - }) - - test(`export default from './x'`, () => { - const { content, bindings } = compile( - ``, - { - babelParserPlugins: ['exportDefaultFrom'] - } + describe('imports', () => { + test('should hoist and expose imports', () => { + assertCode( + compile(``).content ) - assertCode(content) - expect(bindings).toStrictEqual({}) }) - test(`export { x as default }`, () => { - const { content, bindings } = compile( - `` + test('should extract comment for import or type declarations', () => { + assertCode( + compile(``).content ) - assertCode(content) - expect(bindings).toStrictEqual({ - y: 'setup' - }) }) - test(`export { x as default } from './x'`, () => { - const { content, bindings } = compile( - `` - ) + test('dedupe between user & helper', () => { + const { content } = compile(``) assertCode(content) - expect(bindings).toStrictEqual({ - y: 'setup' - }) + expect(content).toMatch(`import { ref } from 'vue'`) }) - test(`export * from './x'`, () => { - const { content, bindings } = compile( - `` - ) + test('import dedupe between + + `) 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 ` + expect(content.indexOf(`import { x }`)).toEqual( + content.lastIndexOf(`import { x }`) ) - assertCode(content) - expect(bindings).toStrictEqual({ - foo: 'props', - y: 'setup' - }) }) }) describe('`) assertCode(content) - expect(bindings).toStrictEqual({ a: 'setup' }) }) test('extract props', () => { @@ -333,7 +210,7 @@ import b from 'b' test('w/ \n` + + `\n` + `` ).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(``) + 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(`cannot contain non-type named exports`) + expect(() => compile(``) - ).toThrow(`Cannot export locally defined variable as default`) + ).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', () => { @@ -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(``).content - ) - }) - - test('should allow export default referencing re-exported binding', () => { - assertCode( - compile(` - - `) - ).toThrow(`Default export is already declared`) - - expect(() => - compile(` - - - `) - ).toThrow(`Default export is already declared`) - expect(() => compile(`