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 0dac40b2340..94d13f6471a 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' @@ -88,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. */ @@ -131,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 @@ -203,7 +213,7 @@ export function compileScript( cssVars, bindings, scopeId, - !!options.isProd + isProd ) content += `\nexport default __default__` } @@ -248,6 +258,8 @@ export function compileScript( let hasDefineExposeCall = false let propsRuntimeDecl: Node | undefined let propsRuntimeDefaults: ObjectExpression | undefined + let propsDestructureDecl: Node | undefined + let propsDestructureRestId: string | undefined let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined let propsTypeDeclRaw: Node | undefined let propsIdentifier: string | undefined @@ -266,6 +278,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) @@ -337,7 +357,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 +394,62 @@ export function compileScript( } } + if (declId) { + if (enablePropsTransform && 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 + if (left.type !== 'Identifier') { + error( + `${DEFINE_PROPS}() destructure does not support nested patterns.`, + left + ) + } + // store default value + propsDestructuredBindings[propKey] = { + local: left.name, + default: right + } + } else if (prop.value.type === 'Identifier') { + // simple destucture + propsDestructuredBindings[propKey] = { + local: prop.value.name + } + } else { + error( + `${DEFINE_PROPS}() destructure does not support nested patterns.`, + prop.value + ) + } + } else { + // rest spread + propsDestructureRestId = (prop.argument as Identifier).name + } + } + } 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 +457,13 @@ export function compileScript( node ) } + 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(...).`, + node.callee + ) + } propsRuntimeDefaults = node.arguments[1] as ObjectExpression if ( !propsRuntimeDefaults || @@ -408,7 +483,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 +515,11 @@ export function compileScript( ) } } + + if (declId) { + emitIdentifier = scriptSetup!.content.slice(declId.start!, declId.end!) + } + return true } @@ -565,7 +645,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' && @@ -582,13 +662,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 @@ -608,7 +691,7 @@ export function compileScript( } } - if (__DEV__) { + if (!isProd) { const { type, required } = props[key] return `${key}: { type: ${toRuntimeTypeString( type @@ -632,9 +715,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 = `{ ` @@ -754,7 +849,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 +995,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 +1088,19 @@ export function compileScript( } // 3. Apply ref sugar transform - if (enableRefTransform && shouldTransformRef(scriptSetup.content)) { - const { rootVars, importedHelpers } = transformRefAST( + if ( + (enableRefTransform && shouldTransformRef(scriptSetup.content)) || + propsDestructureDecl + ) { + const { rootRefs, importedHelpers } = transformRefAST( scriptSetupAst, s, startOffset, - refBindings + refBindings, + propsDestructuredBindings, + !enableRefTransform ) - refBindings = refBindings ? [...refBindings, ...rootVars] : rootVars + refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs for (const h of importedHelpers) { helperImports.add(h) } @@ -1019,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) @@ -1029,6 +1118,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 @@ -1062,6 +1152,20 @@ export function compileScript( for (const key in typeDeclaredProps) { bindingMetadata[key] = BindingTypes.PROPS } + // props aliases + if (propsDestructureDecl) { + if (propsDestructureRestId) { + bindingMetadata[propsDestructureRestId] = BindingTypes.SETUP_CONST + } + for (const key in propsDestructuredBindings) { + const { local } = propsDestructuredBindings[key] + if (local !== key) { + bindingMetadata[local] = BindingTypes.PROPS_ALIASED + ;(bindingMetadata.__propsAliases || + (bindingMetadata.__propsAliases = {}))[local] = key + } + } + } for (const [key, { isType, imported, source }] of Object.entries( userImports )) { @@ -1090,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` ) } @@ -1118,6 +1217,14 @@ export function compileScript( }` ) } + if (propsDestructureRestId) { + s.prependRight( + startOffset, + `\nconst ${propsDestructureRestId} = ${helper( + `createPropsRestProxy` + )}(__props, ${JSON.stringify(Object.keys(propsDestructuredBindings))})` + ) + } // inject temp variables for async context preservation if (hasAwait) { const any = isTS ? `: any` : `` @@ -1235,9 +1342,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) } @@ -1313,6 +1433,7 @@ export function compileScript( } s.trim() + return { ...scriptSetup, bindings: bindingMetadata, @@ -1376,10 +1497,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 ( @@ -1488,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) { @@ -1497,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/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/__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/ref-transform/src/refTransform.ts b/packages/ref-transform/src/refTransform.ts index d577347661c..4822a4e064c 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,23 @@ export function transformAST( ast: Program, s: MagicString, offset = 0, - knownRootVars?: string[] + knownRefs?: string[], + knownProps?: Record< + string, // public prop key + { + local: string // local identifier, may be different + default?: any + } + >, + 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 = {} @@ -113,14 +123,23 @@ export function transformAST( let currentScope: Scope = rootScope const excludedIds = new WeakSet() const parentStack: Node[] = [] + const propsLocalToPublicMap = Object.create(null) - if (knownRootVars) { - for (const key of knownRootVars) { + if (knownRefs) { + for (const key of knownRefs) { rootScope[key] = true } } + if (knownProps) { + for (const key in knownProps) { + const { local } = knownProps[key] + rootScope[local] = 'prop' + propsLocalToPublicMap[local] = key + } + } function error(msg: string, node: Node) { + if (rewritePropsOnly) return const e = new Error(msg) ;(e as any).node = node throw e @@ -145,17 +164,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 +185,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 +334,48 @@ 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`) + if (isProp) { + s.appendLeft( + id.end! + offset, + `: __props.${propsLocalToPublicMap[id.name]}` + ) + } else { + s.appendLeft(id.end! + offset, `: ${id.name}.value`) + } } } else { - s.appendLeft(id.end! + offset, '.value') + if (isProp) { + s.overwrite( + id.start! + offset, + id.end! + offset, + `__props.${propsLocalToPublicMap[id.name]}` + ) + } else { + s.appendLeft(id.end! + offset, '.value') + } } } return true @@ -331,7 +384,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 +424,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 +477,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/__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