From f1b8eb198c5ab5e2aac1f0bed77a3cef6b24e9d6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 25 Sep 2021 18:21:49 -0400 Subject: [PATCH 1/4] wip: basic transforms --- packages/compiler-sfc/src/compileScript.ts | 109 ++++++++++++++----- packages/ref-transform/README.md | 6 +- packages/ref-transform/src/refTransform.ts | 74 +++++++++---- packages/runtime-core/src/apiSetupHelpers.ts | 30 +++-- 4 files changed, 158 insertions(+), 61 deletions(-) diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 0dac40b2340..24a845dfdf0 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -39,7 +39,9 @@ import { TSInterfaceBody, AwaitExpression, Program, - ObjectMethod + ObjectMethod, + LVal, + Expression } from '@babel/types' import { walk } from 'estree-walker' import { RawSourceMap } from 'source-map' @@ -248,6 +250,8 @@ export function compileScript( let hasDefineExposeCall = false let propsRuntimeDecl: Node | undefined let propsRuntimeDefaults: ObjectExpression | undefined + const propsDestructuredBindings: Record = + Object.create(null) let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined let propsTypeDeclRaw: Node | undefined let propsIdentifier: string | undefined @@ -337,7 +341,7 @@ export function compileScript( } } - function processDefineProps(node: Node): boolean { + function processDefineProps(node: Node, declId?: LVal): boolean { if (!isCallOf(node, DEFINE_PROPS)) { return false } @@ -374,14 +378,48 @@ export function compileScript( } } + if (declId) { + if (declId.type === 'ObjectPattern') { + // props destructure - handle compilation sugar + for (const prop of declId.properties) { + if (prop.type === 'ObjectProperty') { + if (prop.value.type === 'AssignmentPattern') { + // default value { foo = 123 } + const { left, right } = prop.value + if (left.type !== 'Identifier') { + error( + `${DEFINE_PROPS}() destructure does not support nested patterns.`, + left + ) + } + // store default value + propsDestructuredBindings[left.name] = right + } else if (prop.value.type === 'Identifier') { + // simple destucture + propsDestructuredBindings[prop.value.name] = true + } else { + error( + `${DEFINE_PROPS}() destructure does not support nested patterns.`, + prop.value + ) + } + } else { + // TODO rest spread + } + } + } else { + propsIdentifier = scriptSetup!.content.slice(declId.start!, declId.end!) + } + } + return true } - function processWithDefaults(node: Node): boolean { + function processWithDefaults(node: Node, declId?: LVal): boolean { if (!isCallOf(node, WITH_DEFAULTS)) { return false } - if (processDefineProps(node.arguments[0])) { + if (processDefineProps(node.arguments[0], declId)) { if (propsRuntimeDecl) { error( `${WITH_DEFAULTS} can only be used with type-based ` + @@ -389,6 +427,13 @@ export function compileScript( node ) } + if (Object.keys(propsDestructuredBindings).length) { + error( + `${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` + + `Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...).`, + node.callee + ) + } propsRuntimeDefaults = node.arguments[1] as ObjectExpression if ( !propsRuntimeDefaults || @@ -408,7 +453,7 @@ export function compileScript( return true } - function processDefineEmits(node: Node): boolean { + function processDefineEmits(node: Node, declId?: LVal): boolean { if (!isCallOf(node, DEFINE_EMITS)) { return false } @@ -440,6 +485,11 @@ export function compileScript( ) } } + + if (declId) { + emitIdentifier = scriptSetup!.content.slice(declId.start!, declId.end!) + } + return true } @@ -754,7 +804,7 @@ export function compileScript( // apply ref transform if (enableRefTransform && shouldTransformRef(script.content)) { - const { rootVars, importedHelpers } = transformRefAST( + const { rootRefs: rootVars, importedHelpers } = transformRefAST( scriptAst, s, scriptStartOffset! @@ -900,20 +950,9 @@ export function compileScript( if (decl.init) { // defineProps / defineEmits const isDefineProps = - processDefineProps(decl.init) || processWithDefaults(decl.init) - if (isDefineProps) { - propsIdentifier = scriptSetup.content.slice( - decl.id.start!, - decl.id.end! - ) - } - const isDefineEmits = processDefineEmits(decl.init) - if (isDefineEmits) { - emitIdentifier = scriptSetup.content.slice( - decl.id.start!, - decl.id.end! - ) - } + processDefineProps(decl.init, decl.id) || + processWithDefaults(decl.init, decl.id) + const isDefineEmits = processDefineEmits(decl.init, decl.id) if (isDefineProps || isDefineEmits) { if (left === 1) { s.remove(node.start! + startOffset, node.end! + startOffset) @@ -1004,14 +1043,20 @@ export function compileScript( } // 3. Apply ref sugar transform - if (enableRefTransform && shouldTransformRef(scriptSetup.content)) { - const { rootVars, importedHelpers } = transformRefAST( + const propsDestructuredKeys = Object.keys(propsDestructuredBindings) + if ( + (enableRefTransform && shouldTransformRef(scriptSetup.content)) || + propsDestructuredKeys.length + ) { + const { rootRefs, importedHelpers } = transformRefAST( scriptSetupAst, s, startOffset, - refBindings + refBindings, + propsDestructuredKeys, + !enableRefTransform ) - refBindings = refBindings ? [...refBindings, ...rootVars] : rootVars + refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs for (const h of importedHelpers) { helperImports.add(h) } @@ -1313,6 +1358,8 @@ export function compileScript( } s.trim() + + debugger return { ...scriptSetup, bindings: bindingMetadata, @@ -1376,10 +1423,16 @@ function walkDeclaration( bindingType = BindingTypes.SETUP_LET } registerBinding(bindings, id, bindingType) - } else if (id.type === 'ObjectPattern') { - walkObjectPattern(id, bindings, isConst, isDefineCall) - } else if (id.type === 'ArrayPattern') { - walkArrayPattern(id, bindings, isConst, isDefineCall) + } else { + if (isCallOf(init, DEFINE_PROPS)) { + // skip walking props destructure + return + } + if (id.type === 'ObjectPattern') { + walkObjectPattern(id, bindings, isConst, isDefineCall) + } else if (id.type === 'ArrayPattern') { + walkArrayPattern(id, bindings, isConst, isDefineCall) + } } } } else if ( diff --git a/packages/ref-transform/README.md b/packages/ref-transform/README.md index e5ba22fb2bf..7de8d6d2146 100644 --- a/packages/ref-transform/README.md +++ b/packages/ref-transform/README.md @@ -64,7 +64,9 @@ const { // @babel/parser plugins to enable. // 'typescript' and 'jsx' will be auto-inferred from filename if provided, // so in most cases explicit parserPlugins are not necessary - parserPlugins: [/* ... */] + parserPlugins: [ + /* ... */ + ] }) ``` @@ -93,7 +95,7 @@ const ast = parse(src, { sourceType: 'module' }) const s = new MagicString(src) const { - rootVars, // ['a'] + rootRefs, // ['a'] importedHelpers // ['ref'] } = transformAST(ast, s) diff --git a/packages/ref-transform/src/refTransform.ts b/packages/ref-transform/src/refTransform.ts index d577347661c..b8adfcc7b72 100644 --- a/packages/ref-transform/src/refTransform.ts +++ b/packages/ref-transform/src/refTransform.ts @@ -31,7 +31,7 @@ export function shouldTransform(src: string): boolean { return transformCheckRE.test(src) } -type Scope = Record +type Scope = Record export interface RefTransformOptions { filename?: string @@ -43,7 +43,7 @@ export interface RefTransformOptions { export interface RefTransformResults { code: string map: SourceMap | null - rootVars: string[] + rootRefs: string[] importedHelpers: string[] } @@ -99,13 +99,17 @@ export function transformAST( ast: Program, s: MagicString, offset = 0, - knownRootVars?: string[] + knownRefs?: string[], + knownProps?: string[], + rewritePropsOnly = false ): { - rootVars: string[] + rootRefs: string[] importedHelpers: string[] } { // TODO remove when out of experimental - warnExperimental() + if (!rewritePropsOnly) { + warnExperimental() + } const importedHelpers = new Set() const rootScope: Scope = {} @@ -114,13 +118,19 @@ export function transformAST( const excludedIds = new WeakSet() const parentStack: Node[] = [] - if (knownRootVars) { - for (const key of knownRootVars) { + if (knownRefs) { + for (const key of knownRefs) { rootScope[key] = true } } + if (knownProps) { + for (const key of knownProps) { + rootScope[key] = 'prop' + } + } function error(msg: string, node: Node) { + if (rewritePropsOnly) return const e = new Error(msg) ;(e as any).node = node throw e @@ -145,17 +155,19 @@ export function transformAST( const registerRefBinding = (id: Identifier) => registerBinding(id, true) - function walkScope(node: Program | BlockStatement) { + function walkScope(node: Program | BlockStatement, isRoot = false) { for (const stmt of node.body) { if (stmt.type === 'VariableDeclaration') { if (stmt.declare) continue for (const decl of stmt.declarations) { let toVarCall - if ( + const isCall = decl.init && decl.init.type === 'CallExpression' && - decl.init.callee.type === 'Identifier' && - (toVarCall = isToVarCall(decl.init.callee.name)) + decl.init.callee.type === 'Identifier' + if ( + isCall && + (toVarCall = isToVarCall((decl as any).init.callee.name)) ) { processRefDeclaration( toVarCall, @@ -164,8 +176,18 @@ export function transformAST( stmt ) } else { + const isProps = + isRoot && + isCall && + (decl as any).init.callee.name === 'defineProps' for (const id of extractIdentifiers(decl.id)) { - registerBinding(id) + if (isProps) { + // for defineProps destructure, only exclude them since they + // are already passed in as knownProps + excludedIds.add(id) + } else { + registerBinding(id) + } } } } @@ -303,26 +325,40 @@ export function transformAST( } } - function checkRefId( + function rewriteId( scope: Scope, id: Identifier, parent: Node, parentStack: Node[] ): boolean { if (hasOwn(scope, id.name)) { - if (scope[id.name]) { + const bindingType = scope[id.name] + if (bindingType) { + const isProp = bindingType === 'prop' + if (rewritePropsOnly && !isProp) { + return true + } + // ref if (isStaticProperty(parent) && parent.shorthand) { // let binding used in a property shorthand // { foo } -> { foo: foo.value } + // { prop } -> { prop: __prop.prop } // skip for destructure patterns if ( !(parent as any).inPattern || isInDestructureAssignment(parent, parentStack) ) { - s.appendLeft(id.end! + offset, `: ${id.name}.value`) + s.appendLeft( + id.end! + offset, + isProp ? `: __props.${id.name}` : `: ${id.name}.value` + ) } } else { - s.appendLeft(id.end! + offset, '.value') + if (isProp) { + s.prependRight(id.start! + offset, `__props.`) + } else { + s.appendLeft(id.end! + offset, '.value') + } } } return true @@ -331,7 +367,7 @@ export function transformAST( } // check root scope first - walkScope(ast) + walkScope(ast, true) ;(walk as any)(ast, { enter(node: Node, parent?: Node) { parent && parentStack.push(parent) @@ -371,7 +407,7 @@ export function transformAST( // walk up the scope chain to check if id should be appended .value let i = scopeStack.length while (i--) { - if (checkRefId(scopeStack[i], node, parent!, parentStack)) { + if (rewriteId(scopeStack[i], node, parent!, parentStack)) { return } } @@ -424,7 +460,7 @@ export function transformAST( }) return { - rootVars: Object.keys(rootScope).filter(key => rootScope[key]), + rootRefs: Object.keys(rootScope).filter(key => rootScope[key] === true), importedHelpers: [...importedHelpers] } } diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts index 54391fc4229..9f00c1031be 100644 --- a/packages/runtime-core/src/apiSetupHelpers.ts +++ b/packages/runtime-core/src/apiSetupHelpers.ts @@ -1,4 +1,5 @@ -import { isPromise } from '../../shared/src' +import { ComponentPropsOptions } from '@vue/runtime-core' +import { isArray, isPromise, isFunction } from '@vue/shared' import { getCurrentInstance, setCurrentInstance, @@ -7,11 +8,7 @@ import { unsetCurrentInstance } from './component' import { EmitFn, EmitsOptions } from './componentEmits' -import { - ComponentObjectPropsOptions, - PropOptions, - ExtractPropTypes -} from './componentProps' +import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps' import { warn } from './warning' // dev only @@ -195,15 +192,24 @@ function getContext(): SetupContext { * @internal */ export function mergeDefaults( - // the base props is compiler-generated and guaranteed to be in this shape. - props: Record, + raw: ComponentPropsOptions, defaults: Record ) { + const props = isArray(raw) + ? raw.reduce( + (normalized, p) => ((normalized[p] = {}), normalized), + {} as ComponentObjectPropsOptions + ) + : raw for (const key in defaults) { - const val = props[key] - if (val) { - val.default = defaults[key] - } else if (val === null) { + const opt = props[key] + if (opt) { + if (isArray(opt) || isFunction(opt)) { + props[key] = { type: opt, default: defaults[key] } + } else { + opt.default = defaults[key] + } + } else if (opt === null) { props[key] = { default: defaults[key] } } else if (__DEV__) { warn(`props default key "${key}" has no corresponding declaration.`) From 055275f3b6b1d0373c1e61028eb91959c76f3a68 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 25 Sep 2021 21:48:24 -0400 Subject: [PATCH 2/4] wip: more --- packages/compiler-sfc/src/compileScript.ts | 76 ++++++++++++++++++---- packages/ref-transform/src/refTransform.ts | 33 +++++++--- 2 files changed, 87 insertions(+), 22 deletions(-) diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 24a845dfdf0..9c36544a6d6 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -250,8 +250,7 @@ export function compileScript( let hasDefineExposeCall = false let propsRuntimeDecl: Node | undefined let propsRuntimeDefaults: ObjectExpression | undefined - const propsDestructuredBindings: Record = - Object.create(null) + let propsDestructureDecl: Node | undefined let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined let propsTypeDeclRaw: Node | undefined let propsIdentifier: string | undefined @@ -270,6 +269,14 @@ export function compileScript( const typeDeclaredEmits: Set = new Set() // record declared types for runtime props type generation const declaredTypes: Record = {} + // props destructure data + const propsDestructuredBindings: Record< + string, // public prop key + { + local: string // local identifier, may be different + default?: Expression + } + > = Object.create(null) // magic-string state const s = new MagicString(source) @@ -380,9 +387,17 @@ export function compileScript( if (declId) { if (declId.type === 'ObjectPattern') { + propsDestructureDecl = declId // props destructure - handle compilation sugar for (const prop of declId.properties) { if (prop.type === 'ObjectProperty') { + if (prop.computed) { + error( + `${DEFINE_PROPS}() destructure cannot use computed key.`, + prop.key + ) + } + const propKey = (prop.key as Identifier).name if (prop.value.type === 'AssignmentPattern') { // default value { foo = 123 } const { left, right } = prop.value @@ -393,10 +408,15 @@ export function compileScript( ) } // store default value - propsDestructuredBindings[left.name] = right + propsDestructuredBindings[propKey] = { + local: left.name, + default: right + } } else if (prop.value.type === 'Identifier') { // simple destucture - propsDestructuredBindings[prop.value.name] = true + propsDestructuredBindings[propKey] = { + local: prop.value.name + } } else { error( `${DEFINE_PROPS}() destructure does not support nested patterns.`, @@ -427,7 +447,7 @@ export function compileScript( node ) } - if (Object.keys(propsDestructuredBindings).length) { + if (propsDestructureDecl) { error( `${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` + `Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...).`, @@ -615,7 +635,7 @@ export function compileScript( * static properties, we can directly generate more optimzied default * declarations. Otherwise we will have to fallback to runtime merging. */ - function checkStaticDefaults() { + function hasStaticWithDefaults() { return ( propsRuntimeDefaults && propsRuntimeDefaults.type === 'ObjectExpression' && @@ -632,13 +652,16 @@ export function compileScript( if (!keys.length) { return `` } - const hasStaticDefaults = checkStaticDefaults() + const hasStaticDefaults = hasStaticWithDefaults() const scriptSetupSource = scriptSetup!.content let propsDecls = `{ ${keys .map(key => { let defaultString: string | undefined - if (hasStaticDefaults) { + const destructured = genDestructuredDefaultValue(key) + if (destructured) { + defaultString = `default: ${destructured}` + } else if (hasStaticDefaults) { const prop = propsRuntimeDefaults!.properties.find( (node: any) => node.key.name === key ) as ObjectProperty | ObjectMethod @@ -682,9 +705,21 @@ export function compileScript( return `\n props: ${propsDecls},` } + function genDestructuredDefaultValue(key: string): string | undefined { + const destructured = propsDestructuredBindings[key] + if (destructured && destructured.default) { + const value = scriptSetup!.content.slice( + destructured.default.start!, + destructured.default.end! + ) + const isLiteral = destructured.default.type.endsWith('Literal') + return isLiteral ? value : `() => ${value}` + } + } + function genSetupPropsType(node: TSTypeLiteral | TSInterfaceBody) { const scriptSetupSource = scriptSetup!.content - if (checkStaticDefaults()) { + if (hasStaticWithDefaults()) { // if withDefaults() is used, we need to remove the optional flags // on props that have default values let res = `{ ` @@ -1043,17 +1078,16 @@ export function compileScript( } // 3. Apply ref sugar transform - const propsDestructuredKeys = Object.keys(propsDestructuredBindings) if ( (enableRefTransform && shouldTransformRef(scriptSetup.content)) || - propsDestructuredKeys.length + propsDestructureDecl ) { const { rootRefs, importedHelpers } = transformRefAST( scriptSetupAst, s, startOffset, refBindings, - propsDestructuredKeys, + propsDestructuredBindings, !enableRefTransform ) refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs @@ -1074,6 +1108,7 @@ export function compileScript( // variables checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS) checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS) + checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS) checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_PROPS) // 6. remove non-script content @@ -1280,9 +1315,22 @@ export function compileScript( runtimeOptions += `\n __ssrInlineRender: true,` } if (propsRuntimeDecl) { - runtimeOptions += `\n props: ${scriptSetup.content + let declCode = scriptSetup.content .slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!) - .trim()},` + .trim() + if (propsDestructureDecl) { + const defaults: string[] = [] + for (const key in propsDestructuredBindings) { + const d = genDestructuredDefaultValue(key) + if (d) defaults.push(`${key}: ${d}`) + } + if (defaults.length) { + declCode = `${helper( + `mergeDefaults` + )}(${declCode}, {\n ${defaults.join(',\n ')}\n})` + } + } + runtimeOptions += `\n props: ${declCode},` } else if (propsTypeDecl) { runtimeOptions += genRuntimeProps(typeDeclaredProps) } diff --git a/packages/ref-transform/src/refTransform.ts b/packages/ref-transform/src/refTransform.ts index b8adfcc7b72..4822a4e064c 100644 --- a/packages/ref-transform/src/refTransform.ts +++ b/packages/ref-transform/src/refTransform.ts @@ -100,7 +100,13 @@ export function transformAST( s: MagicString, offset = 0, knownRefs?: string[], - knownProps?: string[], + knownProps?: Record< + string, // public prop key + { + local: string // local identifier, may be different + default?: any + } + >, rewritePropsOnly = false ): { rootRefs: string[] @@ -117,6 +123,7 @@ export function transformAST( let currentScope: Scope = rootScope const excludedIds = new WeakSet() const parentStack: Node[] = [] + const propsLocalToPublicMap = Object.create(null) if (knownRefs) { for (const key of knownRefs) { @@ -124,8 +131,10 @@ export function transformAST( } } if (knownProps) { - for (const key of knownProps) { - rootScope[key] = 'prop' + for (const key in knownProps) { + const { local } = knownProps[key] + rootScope[local] = 'prop' + propsLocalToPublicMap[local] = key } } @@ -348,14 +357,22 @@ export function transformAST( !(parent as any).inPattern || isInDestructureAssignment(parent, parentStack) ) { - s.appendLeft( - id.end! + offset, - isProp ? `: __props.${id.name}` : `: ${id.name}.value` - ) + if (isProp) { + s.appendLeft( + id.end! + offset, + `: __props.${propsLocalToPublicMap[id.name]}` + ) + } else { + s.appendLeft(id.end! + offset, `: ${id.name}.value`) + } } } else { if (isProp) { - s.prependRight(id.start! + offset, `__props.`) + s.overwrite( + id.start! + offset, + id.end! + offset, + `__props.${propsLocalToPublicMap[id.name]}` + ) } else { s.appendLeft(id.end! + offset, '.value') } From 96e31476734000e77384dad32e6ea80534f02cb2 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 25 Sep 2021 22:29:47 -0400 Subject: [PATCH 3/4] wip: feature complete --- packages/compiler-core/src/options.ts | 6 +++++ .../src/transforms/transformExpression.ts | 4 +++ packages/compiler-sfc/src/compileScript.ts | 27 +++++++++++++++++-- packages/runtime-core/src/apiSetupHelpers.ts | 18 +++++++++++++ packages/runtime-core/src/index.ts | 1 + 5 files changed, 54 insertions(+), 2 deletions(-) diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index 78ffd4e1625..a0658b4d75d 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -82,6 +82,11 @@ export const enum BindingTypes { * declared as a prop */ PROPS = 'props', + /** + * a local alias of a ` + + `) + expect(content).not.toMatch(`const { foo } =`) + expect(content).toMatch(`console.log(__props.foo)`) + expect(content).toMatch(`_toDisplayString(__props.foo)`) + assertCode(content) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS + }) + }) + + test('nested scope', () => { + const { content, bindings } = compile(` + + `) + expect(content).not.toMatch(`const { foo, bar } =`) + expect(content).toMatch(`console.log(foo)`) + expect(content).toMatch(`console.log(__props.bar)`) + assertCode(content) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + bar: BindingTypes.PROPS, + test: BindingTypes.SETUP_CONST + }) + }) + + test('default values w/ runtime declaration', () => { + const { content } = compile(` + + `) + // literals can be used as-is, non-literals are always returned from a + // function + expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar'], { + foo: 1, + bar: () => {} +})`) + assertCode(content) + }) + + test('default values w/ type declaration', () => { + const { content } = compile(` + + `) + // literals can be used as-is, non-literals are always returned from a + // function + expect(content).toMatch(`props: { + foo: { type: Number, required: false, default: 1 }, + bar: { type: Object, required: false, default: () => {} } + }`) + assertCode(content) + }) + + test('default values w/ type declaration, prod mode', () => { + const { content } = compile( + ` + + `, + { isProd: true } + ) + // literals can be used as-is, non-literals are always returned from a + // function + expect(content).toMatch(`props: { + foo: { default: 1 }, + bar: { default: () => {} }, + baz: null + }`) + assertCode(content) + }) + + test('aliasing', () => { + const { content, bindings } = compile(` + + + `) + expect(content).not.toMatch(`const { foo: bar } =`) + expect(content).toMatch(`let x = foo`) // should not process + expect(content).toMatch(`let y = __props.foo`) + // should convert bar to __props.foo in template expressions + expect(content).toMatch(`_toDisplayString(__props.foo + __props.foo)`) + assertCode(content) + expect(bindings).toStrictEqual({ + x: BindingTypes.SETUP_LET, + y: BindingTypes.SETUP_LET, + foo: BindingTypes.PROPS, + bar: BindingTypes.PROPS_ALIASED, + __propsAliases: { + bar: 'foo' + } + }) + }) + + test('rest spread', () => { + const { content, bindings } = compile(` + + `) + expect(content).toMatch( + `const rest = _createPropsRestProxy(__props, ["foo","bar"])` + ) + assertCode(content) + expect(bindings).toStrictEqual({ + foo: BindingTypes.PROPS, + bar: BindingTypes.PROPS, + baz: BindingTypes.PROPS, + rest: BindingTypes.SETUP_CONST + }) + }) + + describe('errors', () => { + test('should error on deep destructure', () => { + expect(() => + compile( + `` + ) + ).toThrow(`destructure does not support nested patterns`) + + expect(() => + compile( + `` + ) + ).toThrow(`destructure does not support nested patterns`) + }) + + test('should error on computed key', () => { + expect(() => + compile( + `` + ) + ).toThrow(`destructure cannot use computed key`) + }) + + test('should error when used with withDefaults', () => { + expect(() => + compile( + `` + ) + ).toThrow(`withDefaults() is unnecessary when using destructure`) + }) + + test('should error if destructure reference local vars', () => { + expect(() => + compile( + `` + ) + ).toThrow(`cannot reference locally declared variables`) + }) + }) +}) diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 68e05a955e2..94d13f6471a 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -90,9 +90,15 @@ export interface SFCScriptCompileOptions { /** * (Experimental) Enable syntax transform for using refs without `.value` * https://github.com/vuejs/rfcs/discussions/369 - * @default true + * @default false */ refTransform?: boolean + /** + * (Experimental) Enable syntax transform for destructuring from defineProps() + * https://github.com/vuejs/rfcs/discussions/394 + * @default false + */ + propsDestructureTransform?: boolean /** * @deprecated use `refTransform` instead. */ @@ -133,6 +139,8 @@ export function compileScript( let { script, scriptSetup, source, filename } = sfc // feature flags const enableRefTransform = !!options.refSugar || !!options.refTransform + const enablePropsTransform = !!options.propsDestructureTransform + const isProd = !!options.isProd const genSourceMap = options.sourceMap !== false let refBindings: string[] | undefined @@ -205,7 +213,7 @@ export function compileScript( cssVars, bindings, scopeId, - !!options.isProd + isProd ) content += `\nexport default __default__` } @@ -387,7 +395,7 @@ export function compileScript( } if (declId) { - if (declId.type === 'ObjectPattern') { + if (enablePropsTransform && declId.type === 'ObjectPattern') { propsDestructureDecl = declId // props destructure - handle compilation sugar for (const prop of declId.properties) { @@ -683,7 +691,7 @@ export function compileScript( } } - if (__DEV__) { + if (!isProd) { const { type, required } = props[key] return `${key}: { type: ${toRuntimeTypeString( type @@ -1100,7 +1108,7 @@ export function compileScript( // 4. extract runtime props/emits code from setup context type if (propsTypeDecl) { - extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes) + extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes, isProd) } if (emitsTypeDecl) { extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits) @@ -1186,12 +1194,7 @@ export function compileScript( helperImports.add('unref') s.prependRight( startOffset, - `\n${genCssVarsCode( - cssVars, - bindingMetadata, - scopeId, - !!options.isProd - )}\n` + `\n${genCssVarsCode(cssVars, bindingMetadata, scopeId, isProd)}\n` ) } @@ -1612,7 +1615,8 @@ function recordType(node: Node, declaredTypes: Record) { function extractRuntimeProps( node: TSTypeLiteral | TSInterfaceBody, props: Record, - declaredTypes: Record + declaredTypes: Record, + isProd: boolean ) { const members = node.type === 'TSTypeLiteral' ? node.members : node.body for (const m of members) { @@ -1621,7 +1625,7 @@ function extractRuntimeProps( m.key.type === 'Identifier' ) { let type - if (__DEV__) { + if (!isProd) { if (m.type === 'TSMethodSignature') { type = ['Function'] } else if (m.typeAnnotation) { diff --git a/packages/ref-transform/__tests__/refTransform.spec.ts b/packages/ref-transform/__tests__/refTransform.spec.ts index 1b9f3003bfb..37f55bb6ee9 100644 --- a/packages/ref-transform/__tests__/refTransform.spec.ts +++ b/packages/ref-transform/__tests__/refTransform.spec.ts @@ -17,7 +17,7 @@ function assertCode(code: string) { } test('$ unwrapping', () => { - const { code, rootVars } = transform(` + const { code, rootRefs } = transform(` import { ref, shallowRef } from 'vue' let foo = $(ref()) let a = $(ref(1)) @@ -40,12 +40,12 @@ test('$ unwrapping', () => { // normal declarations left untouched expect(code).toMatch(`let c = () => {}`) expect(code).toMatch(`let d`) - expect(rootVars).toStrictEqual(['foo', 'a', 'b']) + expect(rootRefs).toStrictEqual(['foo', 'a', 'b']) assertCode(code) }) test('$ref & $shallowRef declarations', () => { - const { code, rootVars, importedHelpers } = transform(` + const { code, rootRefs, importedHelpers } = transform(` let foo = $ref() let a = $ref(1) let b = $shallowRef({ @@ -70,13 +70,13 @@ test('$ref & $shallowRef declarations', () => { // normal declarations left untouched expect(code).toMatch(`let c = () => {}`) expect(code).toMatch(`let d`) - expect(rootVars).toStrictEqual(['foo', 'a', 'b']) + expect(rootRefs).toStrictEqual(['foo', 'a', 'b']) expect(importedHelpers).toStrictEqual(['ref', 'shallowRef']) assertCode(code) }) test('multi $ref declarations', () => { - const { code, rootVars, importedHelpers } = transform(` + const { code, rootRefs, importedHelpers } = transform(` let a = $ref(1), b = $ref(2), c = $ref({ count: 0 }) @@ -86,31 +86,31 @@ test('multi $ref declarations', () => { count: 0 }) `) - expect(rootVars).toStrictEqual(['a', 'b', 'c']) + expect(rootRefs).toStrictEqual(['a', 'b', 'c']) expect(importedHelpers).toStrictEqual(['ref']) assertCode(code) }) test('$computed declaration', () => { - const { code, rootVars, importedHelpers } = transform(` + const { code, rootRefs, importedHelpers } = transform(` let a = $computed(() => 1) `) expect(code).toMatch(` let a = _computed(() => 1) `) - expect(rootVars).toStrictEqual(['a']) + expect(rootRefs).toStrictEqual(['a']) expect(importedHelpers).toStrictEqual(['computed']) assertCode(code) }) test('mixing $ref & $computed declarations', () => { - const { code, rootVars, importedHelpers } = transform(` + const { code, rootRefs, importedHelpers } = transform(` let a = $ref(1), b = $computed(() => a + 1) `) expect(code).toMatch(` let a = _ref(1), b = _computed(() => a.value + 1) `) - expect(rootVars).toStrictEqual(['a', 'b']) + expect(rootRefs).toStrictEqual(['a', 'b']) expect(importedHelpers).toStrictEqual(['ref', 'computed']) assertCode(code) }) @@ -201,7 +201,7 @@ test('should not rewrite scope variable', () => { }) test('object destructure', () => { - const { code, rootVars } = transform(` + const { code, rootRefs } = transform(` let n = $ref(1), { a, b: c, d = 1, e: f = 2, ...g } = $(useFoo()) let { foo } = $(useSomthing(() => 1)); console.log(n, a, c, d, f, g, foo) @@ -221,12 +221,12 @@ test('object destructure', () => { expect(code).toMatch( `console.log(n.value, a.value, c.value, d.value, f.value, g.value, foo.value)` ) - expect(rootVars).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'g', 'foo']) + expect(rootRefs).toStrictEqual(['n', 'a', 'c', 'd', 'f', 'g', 'foo']) assertCode(code) }) test('array destructure', () => { - const { code, rootVars } = transform(` + const { code, rootRefs } = transform(` let n = $ref(1), [a, b = 1, ...c] = $(useFoo()) console.log(n, a, b, c) `) @@ -235,12 +235,12 @@ test('array destructure', () => { expect(code).toMatch(`\nconst b = _shallowRef(__b);`) expect(code).toMatch(`\nconst c = _shallowRef(__c);`) expect(code).toMatch(`console.log(n.value, a.value, b.value, c.value)`) - expect(rootVars).toStrictEqual(['n', 'a', 'b', 'c']) + expect(rootRefs).toStrictEqual(['n', 'a', 'b', 'c']) assertCode(code) }) test('nested destructure', () => { - const { code, rootVars } = transform(` + const { code, rootRefs } = transform(` let [{ a: { b }}] = $(useFoo()) let { c: [d, e] } = $(useBar()) console.log(b, d, e) @@ -252,7 +252,7 @@ test('nested destructure', () => { expect(code).toMatch(`\nconst b = _shallowRef(__b);`) expect(code).toMatch(`\nconst d = _shallowRef(__d);`) expect(code).toMatch(`\nconst e = _shallowRef(__e);`) - expect(rootVars).toStrictEqual(['b', 'd', 'e']) + expect(rootRefs).toStrictEqual(['b', 'd', 'e']) assertCode(code) }) @@ -270,7 +270,7 @@ test('$$', () => { }) test('nested scopes', () => { - const { code, rootVars } = transform(` + const { code, rootRefs } = transform(` let a = $ref(0) let b = $ref(0) let c = 0 @@ -303,7 +303,7 @@ test('nested scopes', () => { return $$({ a, b, c, d }) } `) - expect(rootVars).toStrictEqual(['a', 'b', 'bar']) + expect(rootRefs).toStrictEqual(['a', 'b', 'bar']) expect(code).toMatch('a.value++ // outer a') expect(code).toMatch('b.value++ // outer b') diff --git a/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts b/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts index 728cd8e732a..e9dd1717bb2 100644 --- a/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts +++ b/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts @@ -11,7 +11,8 @@ import { SetupContext, Suspense, computed, - ComputedRef + ComputedRef, + shallowReactive } from '@vue/runtime-test' import { defineEmits, @@ -21,7 +22,8 @@ import { useAttrs, useSlots, mergeDefaults, - withAsyncContext + withAsyncContext, + createPropsRestProxy } from '../src/apiSetupHelpers' describe('SFC