diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index f1e0fd2b..905eded2 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -272,8 +272,10 @@ export function resolveComponentType( } const tagFromConst = checkType(BindingTypes.CONST) if (tagFromConst) { - // constant setup bindings (e.g. imports) can be used as-is - return tagFromConst + return context.inline + ? // in inline mode, const setup bindings (e.g. imports) can be used as-is + tagFromConst + : `$setup[${JSON.stringify(tagFromConst)}]` } } diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index 57039c05..276c8cb1 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -102,19 +102,18 @@ export function processExpression( const { inline, bindingMetadata } = context // const bindings exposed from setup - we know they never change - if (inline && bindingMetadata[node.content] === BindingTypes.CONST) { + if (bindingMetadata[node.content] === BindingTypes.CONST) { node.isRuntimeConstant = true return node } const prefix = (raw: string) => { const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw] - if (type === BindingTypes.CONST) { - return raw - } if (inline) { // setup inline mode - if (type === BindingTypes.SETUP) { + if (type === BindingTypes.CONST) { + return raw + } else if (type === BindingTypes.SETUP) { return `${context.helperString(UNREF)}(${raw})` } else if (type === BindingTypes.PROPS) { // use __props which is generated by compileScript so in ts mode @@ -122,8 +121,16 @@ export function processExpression( return `__props.${raw}` } } - // fallback to normal - return `${type ? `$${type}` : `_ctx`}.${raw}` + + if (type === BindingTypes.CONST) { + // setup const binding in non-inline mode + return `$setup.${raw}` + } else if (type) { + return `$${type}.${raw}` + } else { + // fallback to ctx + return `_ctx.${raw}` + } } // fast path if expression is a simple identifier. diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap index a91e3e65..6b95ef42 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap @@ -1,106 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`SFC compile `).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 &&