From 556560fae31d9e406cfae656089657b6332686c1 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 29 Oct 2020 15:03:39 -0400 Subject: [PATCH 01/27] feat(compiler-sfc): new script setup implementation - now exposes all top level bindings to template - support `ref:` syntax sugar --- .../src/transforms/transformExpression.ts | 97 ++-- .../__snapshots__/compileScript.spec.ts.snap | 361 +++++++------ .../__tests__/compileScript.spec.ts | 455 +++++++++------- packages/compiler-sfc/src/compileScript.ts | 504 +++++++++++------- packages/compiler-sfc/src/genCssVars.ts | 2 +- 5 files changed, 810 insertions(+), 609 deletions(-) diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index a671c563..c2f00a4c 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 a6bb93e7..69937bc9 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( - `` + describe('imports', () => { + test('should hoist and expose imports', () => { + assertCode( + compile(``).content ) - 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('should extract comment for import or type declarations', () => { + assertCode( + compile(``).content + ) }) - test('export function x() {}', () => { - const { content, bindings } = compile( - `` - ) + test('dedupe between user & helper', () => { + const { content } = compile(``) assertCode(content) - expect(bindings).toStrictEqual({ - x: 'setup' - }) + expect(content).toMatch(`import { ref } from 'vue'`) }) - test('export class X() {}', () => { - const { content, bindings } = compile( - `` - ) + test('import dedupe between + + `) assertCode(content) - expect(bindings).toStrictEqual({ - X: 'setup' - }) - }) - - test('export { x }', () => { - const { content, bindings } = compile( - `` + 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( - `` - ) - assertCode(content) - expect(bindings).toStrictEqual({ - x: 'setup', - y: 'setup' - }) - }) - - test(`export default from './x'`, () => { - const { content, bindings } = compile( - ``, - { - babelParserPlugins: ['exportDefaultFrom'] - } - ) - assertCode(content) - expect(bindings).toStrictEqual({}) - }) - - test(`export { x as default }`, () => { - const { content, bindings } = compile( - `` - ) - assertCode(content) - expect(bindings).toStrictEqual({ - y: 'setup' - }) - }) - - test(`export { x as default } from './x'`, () => { - const { content, bindings } = compile( - `` - ) - assertCode(content) - expect(bindings).toStrictEqual({ - y: 'setup' - }) - }) - - test(`export * from './x'`, () => { - const { content, bindings } = compile( - `` - ) - 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 ` - ) - 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(` `) expect(content).toMatch(`import { ref } from 'vue'`) + expect(content).not.toMatch(`ref: foo`) expect(content).not.toMatch(`ref: a`) + expect(content).not.toMatch(`ref: b`) + expect(content).toMatch(`const foo = ref()`) expect(content).toMatch(`const a = ref(1)`) expect(content).toMatch(` const b = ref({ @@ -279,6 +283,7 @@ describe('SFC compile `) - ).toThrow(`cannot contain non-type named exports`) + ).toThrow(`cannot contain non-type named or * exports`) + + expect(() => + compile(``) + ).toThrow(`cannot contain non-type named or * exports`) expect(() => compile(``) - ).toThrow(`cannot contain non-type named exports`) + ).toThrow(`cannot contain non-type named or * exports`) }) test('ref: non-assignment expressions', () => { diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 45cdea72..94eb0e56 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -27,13 +27,28 @@ import { import { walk } from 'estree-walker' import { RawSourceMap } from 'source-map' import { genCssVarsCode, injectCssVarsCalls } from './genCssVars' +import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate' export interface SFCScriptCompileOptions { /** * https://babeljs.io/docs/en/babel-parser#plugins */ babelParserPlugins?: ParserPlugin[] + /** + * Enable ref: label sugar + * https://github.com/vuejs/rfcs/pull/228 + * @default true + */ refSugar?: boolean + /** + * Compile the template and inline the resulting render function + * directly inside setup(). + * - Only affects `).content - ) - }) - test('should expose top level declarations', () => { const { content } = compile(` `) assertCode(content) - expect(content).toMatch('return { x, a, b, c, d }') + expect(content).toMatch('return { a, b, c, d, x }') + }) + + test('defineContext()', () => { + const { content, bindings } = compile(` + + `) + // should generate working code + assertCode(content) + // should anayze bindings + expect(bindings).toStrictEqual({ + foo: 'props', + bar: 'const', + props: 'const', + emit: 'const' + }) + + // should remove defineContext import and call + expect(content).not.toMatch('defineContext') + // should generate correct setup signature + expect(content).toMatch(`setup(__props, { props, emit }) {`) + // should include context options in default export + expect(content).toMatch(`export default { + props: { + foo: String + }, + emit: ['a', 'b'],`) }) describe('imports', () => { @@ -51,18 +81,22 @@ describe('SFC compile `).content + compile(` + + `).content ) }) test('dedupe between user & helper', () => { - const { content } = compile(``) + const { content } = compile(` + + `) assertCode(content) expect(content).toMatch(`import { ref } from 'vue'`) }) @@ -84,7 +118,62 @@ describe('SFC compile + + `, + { inlineTemplate: true } + ) + // check snapshot and make sure helper imports and + // hoists are placed correctly. + assertCode(content) + }) + + test('avoid unref() when necessary', () => { + // function, const, component import + const { content } = compile( + ` + + + `, + { inlineTemplate: true } + ) + assertCode(content) + // no need to unref vue component import + expect(content).toMatch(`createVNode(Foo)`) + // should unref other imports + expect(content).toMatch(`unref(other)`) + // no need to unref constant literals + expect(content).not.toMatch(`unref(constant)`) + // should unref const w/ call init (e.g. ref()) + expect(content).toMatch(`unref(count)`) + // no need to unref function declarations + expect(content).toMatch(`{ onClick: fn }`) + // no need to mark constant fns in patch flag + expect(content).not.toMatch(`PROPS`) + }) + }) + + describe('with TypeScript', () => { test('hoist type declarations', () => { const { content } = compile(` + `) + assertCode(content) + expect(content).toMatch(`export default defineComponent({ + props: { foo: String }, + emits: ['a', 'b'], + setup(__props, { props, emit }) {`) + }) + + test('defineContext w/ type / extract props', () => { + const { content, bindings } = compile(` + `) assertCode(content) expect(content).toMatch(`string: { type: String, required: true }`) @@ -154,21 +263,57 @@ describe('SFC compile + `) + assertCode(content) + expect(content).toMatch(`props: {},\n emit: (e: 'foo' | 'bar') => void,`) + expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`) + }) + + test('defineContext w/ type / extract emits (union)', () => { + 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` + `props: {},\n emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void),` ) expect(content).toMatch( `emits: ["foo", "bar", "baz"] as unknown as undefined` @@ -220,9 +365,7 @@ describe('SFC compile `) - expect(content).toMatch( - `export ${shouldAsync ? `async ` : ``}function setup` - ) + expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup()`) } test('expression statement', () => { @@ -459,25 +602,27 @@ describe('SFC compile `) - ).toThrow(`cannot contain non-type named or * exports`) + ).toThrow(moduleErrorMsg) expect(() => compile(``) - ).toThrow(`cannot contain non-type named or * exports`) + ).toThrow(moduleErrorMsg) expect(() => compile(``) - ).toThrow(`cannot contain non-type named or * exports`) + ).toThrow(moduleErrorMsg) }) test('ref: non-assignment expressions', () => { @@ -488,97 +633,74 @@ describe('SFC compile `) + }).toThrow(`cannot accept both type and non-type arguments`) + }) + + test('defineContext() referencing local var', () => { expect(() => compile(``) ).toThrow(`cannot reference locally declared variables`) }) - test('export default referencing ref declarations', () => { + test('defineContext() referencing ref declarations', () => { expect(() => compile(``) ).toThrow(`cannot reference locally declared variables`) }) - test('should allow export default referencing scope var', () => { + test('should allow defineContext() referencing scope var', () => { assertCode( compile(``).content ) }) - test('should allow export default referencing imported binding', () => { + test('should allow defineContext() 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`) - }) }) }) @@ -779,11 +901,12 @@ describe('SFC analyze `) expect(bindings).toStrictEqual({ diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index ea5bf95f..b90b677f 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -15,12 +15,12 @@ import { TSType, TSTypeLiteral, TSFunctionType, - TSDeclareFunction, ObjectProperty, ArrayExpression, Statement, Expression, - LabeledStatement + LabeledStatement, + TSUnionType } from '@babel/types' import { walk } from 'estree-walker' import { RawSourceMap } from 'source-map' @@ -143,6 +143,7 @@ export function compileScript( const refIdentifiers: Set = new Set() const enableRefSugar = options.refSugar !== false let defaultExport: Node | undefined + let hasContextCall = false let setupContextExp: string | undefined let setupContextArg: ObjectExpression | undefined let setupContextType: TSTypeLiteral | undefined @@ -182,6 +183,48 @@ export function compileScript( ) } + function processContextCall(node: Node): boolean { + if ( + node.type === 'CallExpression' && + node.callee.type === 'Identifier' && + node.callee.name === CTX_FN_NAME + ) { + if (hasContextCall) { + error('duplicate defineContext() call', node) + } + hasContextCall = true + const optsArg = node.arguments[0] + if (optsArg) { + if (optsArg.type === 'ObjectExpression') { + setupContextArg = optsArg + } else { + error(`${CTX_FN_NAME}() argument must be an object literal.`, optsArg) + } + } + // context call has type parameters - infer runtime types from it + if (node.typeParameters) { + if (setupContextArg) { + error( + `${CTX_FN_NAME}() cannot accept both type and non-type arguments ` + + `at the same time. Use one or the other.`, + node + ) + } + const typeArg = node.typeParameters.params[0] + if (typeArg.type === 'TSTypeLiteral') { + setupContextType = typeArg + } else { + error( + `type argument passed to ${CTX_FN_NAME}() must be a literal type.`, + typeArg + ) + } + } + return true + } + return false + } + function processRefExpression(exp: Expression, statement: LabeledStatement) { if (exp.type === 'AssignmentExpression') { helperImports.add('ref') @@ -500,51 +543,24 @@ export function compileScript( } } + if ( + node.type === 'ExpressionStatement' && + processContextCall(node.expression) + ) { + s.remove(node.start! + startOffset, node.end! + startOffset) + } + if (node.type === 'VariableDeclaration' && !node.declare) { for (const decl of node.declarations) { - if ( - decl.init && - decl.init.type === 'CallExpression' && - decl.init.callee.type === 'Identifier' && - decl.init.callee.name === CTX_FN_NAME - ) { - if (node.declarations.length === 1) { - s.remove(node.start! + startOffset, node.end! + startOffset) - } else { - s.remove(decl.start! + startOffset, decl.end! + startOffset) - } + if (decl.init && processContextCall(decl.init)) { setupContextExp = scriptSetup.content.slice( decl.id.start!, decl.id.end! ) - const optsArg = decl.init.arguments[0] - if (optsArg.type === 'ObjectExpression') { - setupContextArg = optsArg + if (node.declarations.length === 1) { + s.remove(node.start! + startOffset, node.end! + startOffset) } else { - error( - `${CTX_FN_NAME}() argument must be an object literal.`, - optsArg - ) - } - - // useSetupContext() has type parameters - infer runtime types from it - if (decl.init.typeParameters) { - if (setupContextArg) { - error( - `${CTX_FN_NAME}() cannot accept both type and non-type arguments ` + - `at the same time. Use one or the other.`, - decl.init - ) - } - const typeArg = decl.init.typeParameters.params[0] - if (typeArg.type === 'TSTypeLiteral') { - setupContextType = typeArg - } else { - error( - `type argument passed to ${CTX_FN_NAME}() must be a literal type.`, - typeArg - ) - } + s.remove(decl.start! + startOffset, decl.end! + startOffset) } } } @@ -641,7 +657,8 @@ export function compileScript( typeNode.start!, typeNode.end! ) - if (m.key.name === 'props') { + const key = m.key.name + if (key === 'props') { propsType = typeString if (typeNode.type === 'TSTypeLiteral') { extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes) @@ -649,18 +666,23 @@ export function compileScript( // TODO be able to trace references error(`props type must be an object literal type`, typeNode) } - } else if (m.key.name === 'emit') { + } else if (key === 'emit') { emitType = typeString - if (typeNode.type === 'TSFunctionType') { + if ( + typeNode.type === 'TSFunctionType' || + typeNode.type === 'TSUnionType' + ) { extractRuntimeEmits(typeNode, typeDeclaredEmits) } else { // TODO be able to trace references error(`emit type must be a function type`, typeNode) } - } else if (m.key.name === 'attrs') { + } else if (key === 'attrs') { attrsType = typeString - } else if (m.key.name === 'slots') { + } else if (key === 'slots') { slotsType = typeString + } else { + error(`invalid setup context property: "${key}"`, m.key) } } } @@ -747,19 +769,13 @@ export function compileScript( if (setupContextArg) { Object.assign(bindingMetadata, analyzeBindingsFromOptions(setupContextArg)) } - if (options.inlineTemplate) { - for (const [key, { source }] of Object.entries(userImports)) { - bindingMetadata[key] = source.endsWith('.vue') - ? BindingTypes.CONST - : BindingTypes.SETUP - } - for (const key in setupBindings) { - bindingMetadata[key] = setupBindings[key] - } - } else { - for (const key in allBindings) { - bindingMetadata[key] = BindingTypes.SETUP - } + for (const [key, { source }] of Object.entries(userImports)) { + bindingMetadata[key] = source.endsWith('.vue') + ? BindingTypes.CONST + : BindingTypes.SETUP + } + for (const key in setupBindings) { + bindingMetadata[key] = setupBindings[key] } // 11. generate return statement @@ -1135,11 +1151,20 @@ function toRuntimeTypeString(types: string[]) { } function extractRuntimeEmits( - node: TSFunctionType | TSDeclareFunction, + node: TSFunctionType | TSUnionType, emits: Set ) { - const eventName = - node.type === 'TSDeclareFunction' ? node.params[0] : node.parameters[0] + if (node.type === 'TSUnionType') { + for (let t of node.types) { + if (t.type === 'TSParenthesizedType') t = t.typeAnnotation + if (t.type === 'TSFunctionType') { + extractRuntimeEmits(t, emits) + } + } + return + } + + const eventName = node.parameters[0] if ( eventName.type === 'Identifier' && eventName.typeAnnotation && From 292a657861e5799ec14dab1b6e4d5def003f49d0 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 12 Nov 2020 22:44:18 -0500 Subject: [PATCH 18/27] wip: fix runtimeConstant marking --- .../src/transforms/transformExpression.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index 276c8cb1..f72bd16b 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -100,13 +100,6 @@ export function processExpression( } const { inline, bindingMetadata } = context - - // const bindings exposed from setup - we know they never change - if (bindingMetadata[node.content] === BindingTypes.CONST) { - node.isRuntimeConstant = true - return node - } - const prefix = (raw: string) => { const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw] if (inline) { @@ -138,6 +131,10 @@ export function processExpression( // bail on parens to prevent any possible function invocations. const bailConstant = rawExp.indexOf(`(`) > -1 if (isSimpleIdentifier(rawExp)) { + // const bindings exposed from setup - we know they never change + if (bindingMetadata[node.content] === BindingTypes.CONST) { + node.isRuntimeConstant = true + } if ( !asParams && !context.identifiers[rawExp] && From 001f8ce99386054c2bea0e3d248f50e1fb4c38bc Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 12 Nov 2020 22:51:40 -0500 Subject: [PATCH 19/27] wip: defineContext -> useOptions --- .../__snapshots__/compileScript.spec.ts.snap | 78 +++++++-------- .../__tests__/compileScript.spec.ts | 70 ++++++------- packages/compiler-sfc/src/compileScript.ts | 97 +++++++++---------- .../useOptions.ts} | 12 ++- packages/runtime-core/src/index.ts | 2 +- 5 files changed, 131 insertions(+), 128 deletions(-) rename packages/runtime-core/src/{apiDefineContext.ts => helpers/useOptions.ts} (58%) diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap index 6b95ef42..c48e1ff4 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap @@ -56,25 +56,7 @@ return { color } }" `; -exports[`SFC compile @@ -302,11 +302,11 @@ const { props, emit } = defineContext({ expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`) }) - test('defineContext w/ type / extract emits (union)', () => { + test('useOptions w/ type / extract emits (union)', () => { const { content } = compile(` @@ -633,21 +633,21 @@ const { props, emit } = defineContext({ ).toThrow(`ref: statements can only contain assignment expressions`) }) - test('defineContext() w/ both type and non-type args', () => { + test('useOptions() w/ both type and non-type args', () => { expect(() => { compile(``) }).toThrow(`cannot accept both type and non-type arguments`) }) - test('defineContext() referencing local var', () => { + test('useOptions() referencing local var', () => { expect(() => compile(``) ).toThrow(`cannot reference locally declared variables`) }) - test('should allow defineContext() referencing scope var', () => { + test('should allow useOptions() referencing scope var', () => { assertCode( compile(` @@ -302,11 +302,11 @@ const { props, emit } = useOptions({ expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`) }) - test('useOptions w/ type / extract emits (union)', () => { + test('defineOptions w/ type / extract emits (union)', () => { const { content } = compile(` @@ -633,21 +633,21 @@ const { props, emit } = useOptions({ ).toThrow(`ref: statements can only contain assignment expressions`) }) - test('useOptions() w/ both type and non-type args', () => { + test('defineOptions() w/ both type and non-type args', () => { expect(() => { compile(``) }).toThrow(`cannot accept both type and non-type arguments`) }) - test('useOptions() referencing local var', () => { + test('defineOptions() referencing local var', () => { expect(() => compile(``) ).toThrow(`cannot reference locally declared variables`) }) - test('should allow useOptions() referencing scope var', () => { + test('should allow defineOptions() referencing scope var', () => { assertCode( compile(` `) assertCode(content) - expect(content).toMatch(`export default defineComponent({ + expect(content).toMatch(`export default _defineComponent({ + expose: [], props: { foo: String }, emits: ['a', 'b'], setup(__props, { props, emit }) {`) @@ -410,14 +412,14 @@ const { props, emit } = defineOptions({ let c = () => {} let d `) - expect(content).toMatch(`import { ref } from 'vue'`) + expect(content).toMatch(`import { ref as _ref } from 'vue'`) expect(content).not.toMatch(`ref: foo`) expect(content).not.toMatch(`ref: a`) expect(content).not.toMatch(`ref: b`) - expect(content).toMatch(`const foo = ref()`) - expect(content).toMatch(`const a = ref(1)`) + expect(content).toMatch(`const foo = _ref()`) + expect(content).toMatch(`const a = _ref(1)`) expect(content).toMatch(` - const b = ref({ + const b = _ref({ count: 0 }) `) @@ -441,7 +443,7 @@ const { props, emit } = defineOptions({ } `) expect(content).toMatch(` - const a = ref(1), b = ref(2), c = ref({ + const a = _ref(1), b = _ref(2), c = _ref({ count: 0 }) `) @@ -526,15 +528,15 @@ const { props, emit } = defineOptions({ console.log(n, a, c, d, f, g) `) expect(content).toMatch( - `const n = ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = useFoo()` + `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(`\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)` ) @@ -556,11 +558,11 @@ const { props, emit } = defineOptions({ console.log(n, a, b, c) `) expect(content).toMatch( - `const n = ref(1), [__a, __b = 1, ...__c] = useFoo()` + `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(`\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({ @@ -580,11 +582,11 @@ const { props, emit } = defineOptions({ `) 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).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', diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 47420ca3..a049d205 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -24,7 +24,11 @@ import { } from '@babel/types' import { walk } from 'estree-walker' import { RawSourceMap } from 'source-map' -import { genCssVarsCode, injectCssVarsCalls } from './genCssVars' +import { + CSS_VARS_HELPER, + genCssVarsCode, + injectCssVarsCalls +} from './genCssVars' import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate' const DEFINE_OPTIONS = 'defineOptions' @@ -165,6 +169,11 @@ export function compileScript( const scriptStartOffset = script && script.loc.start.offset const scriptEndOffset = script && script.loc.end.offset + function helper(key: string): string { + helperImports.add(key) + return `_${key}` + } + function parse( input: string, options: ParserOptions, @@ -240,11 +249,10 @@ export function compileScript( function processRefExpression(exp: Expression, statement: LabeledStatement) { if (exp.type === 'AssignmentExpression') { - helperImports.add('ref') const { left, right } = exp if (left.type === 'Identifier') { registerRefBinding(left) - s.prependRight(right.start! + startOffset, `ref(`) + s.prependRight(right.start! + startOffset, `${helper('ref')}(`) s.appendLeft(right.end! + startOffset, ')') } else if (left.type === 'ObjectPattern') { // remove wrapping parens @@ -272,7 +280,7 @@ export function compileScript( exp.expressions.forEach(e => processRefExpression(e, statement)) } else if (exp.type === 'Identifier') { registerRefBinding(exp) - s.appendLeft(exp.end! + startOffset, ` = ref()`) + s.appendLeft(exp.end! + startOffset, ` = ${helper('ref')}()`) } else { error(`ref: statements can only contain assignment expressions.`, exp) } @@ -326,7 +334,7 @@ export function compileScript( // append binding declarations after the parent statement s.appendLeft( statement.end! + startOffset, - `\nconst ${nameId.name} = ref(__${nameId.name});` + `\nconst ${nameId.name} = ${helper('ref')}(__${nameId.name});` ) } } @@ -360,7 +368,7 @@ export function compileScript( // append binding declarations after the parent statement s.appendLeft( statement.end! + startOffset, - `\nconst ${nameId.name} = ref(__${nameId.name});` + `\nconst ${nameId.name} = ${helper('ref')}(__${nameId.name});` ) } } @@ -744,7 +752,7 @@ export function compileScript( // 8. inject `useCssVars` calls if (hasCssVars) { - helperImports.add(`useCssVars`) + helperImports.add(CSS_VARS_HELPER) for (const style of styles) { const vars = style.attrs.vars if (typeof vars === 'string') { @@ -829,7 +837,6 @@ export function compileScript( if (isTS) { // for TS, make sure the exported type is still valid type with // correct props information - 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},` : `` @@ -838,7 +845,9 @@ export function compileScript( // this allows `import { setup } from '*.vue'` for testing purposes. s.prependLeft( startOffset, - `\nexport default defineComponent({${def}${runtimeOptions}\n ${ + `\nexport default ${helper( + `defineComponent` + )}({${def}${runtimeOptions}\n ${ hasAwait ? `async ` : `` }setup(${args}) {\n` ) @@ -865,11 +874,12 @@ export function compileScript( } // 12. finalize Vue helper imports - // TODO account for cases where user imports a helper with the same name - // from a non-vue source - const helpers = [...helperImports].filter(i => !userImports[i]) - if (helpers.length) { - s.prepend(`import { ${helpers.join(', ')} } from 'vue'\n`) + if (helperImports.size > 0) { + s.prepend( + `import { ${[...helperImports] + .map(h => `${h} as _${h}`) + .join(', ')} } from 'vue'\n` + ) } s.trim() diff --git a/packages/compiler-sfc/src/genCssVars.ts b/packages/compiler-sfc/src/genCssVars.ts index 3d926cf5..69d1457f 100644 --- a/packages/compiler-sfc/src/genCssVars.ts +++ b/packages/compiler-sfc/src/genCssVars.ts @@ -10,6 +10,8 @@ import { SFCDescriptor } from './parse' import { rewriteDefault } from './rewriteDefault' import { ParserPlugin } from '@babel/parser' +export const CSS_VARS_HELPER = `useCssVars` + export function genCssVarsCode( varsExp: string, scoped: boolean, @@ -38,7 +40,7 @@ export function genCssVarsCode( }) .join('') - return `__useCssVars__(_ctx => (${transformedString})${ + return `_${CSS_VARS_HELPER}(_ctx => (${transformedString})${ scoped ? `, true` : `` })` } @@ -65,7 +67,7 @@ export function injectCssVarsCalls( return ( script + - `\nimport { useCssVars as __useCssVars__ } from 'vue'\n` + + `\nimport { ${CSS_VARS_HELPER} as _${CSS_VARS_HELPER} } from 'vue'\n` + `const __injectCSSVars__ = () => {\n${calls}}\n` + `const __setup__ = __default__.setup\n` + `__default__.setup = __setup__\n` +