* declared as a prop
*/
PROPS = 'props',
+ /**
+ * a local alias of a `<script setup>` destructured prop.
+ * the original is stored in __propsAliases of the bindingMetadata object.
+ */
+ PROPS_ALIASED = 'props-aliased',
/**
* a let binding (may or may not be a ref)
*/
[key: string]: BindingTypes | undefined
} & {
__isScriptSetup?: boolean
+ __propsAliases?: Record<string, string>
}
interface SharedTransformCodegenOptions {
// use __props which is generated by compileScript so in ts mode
// it gets correct type
return `__props.${raw}`
+ } else if (type === BindingTypes.PROPS_ALIASED) {
+ // prop with a different local alias (from defineProps() destructure)
+ return `__props.${bindingMetadata.__propsAliases![raw]}`
}
} else {
if (type && type.startsWith('setup')) {
// setup bindings in non-inline mode
return `$setup.${raw}`
+ } else if (type === BindingTypes.PROPS_ALIASED) {
+ return `$props.${bindingMetadata.__propsAliases![raw]}`
} else if (type) {
return `$${type}.${raw}`
}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`sfc props transform aliasing 1`] = `
+"import { toDisplayString as _toDisplayString } from \\"vue\\"
+
+
+export default {
+ props: ['foo'],
+ setup(__props) {
+
+
+ let x = foo
+ let y = __props.foo
+
+return (_ctx, _cache) => {
+ return _toDisplayString(__props.foo + __props.foo)
+}
+}
+
+}"
+`;
+
+exports[`sfc props transform basic usage 1`] = `
+"import { toDisplayString as _toDisplayString } from \\"vue\\"
+
+
+export default {
+ props: ['foo'],
+ setup(__props) {
+
+
+ console.log(__props.foo)
+
+return (_ctx, _cache) => {
+ return _toDisplayString(__props.foo)
+}
+}
+
+}"
+`;
+
+exports[`sfc props transform default values w/ runtime declaration 1`] = `
+"import { mergeDefaults as _mergeDefaults } from 'vue'
+
+export default {
+ props: _mergeDefaults(['foo', 'bar'], {
+ foo: 1,
+ bar: () => {}
+}),
+ setup(__props) {
+
+
+
+return () => {}
+}
+
+}"
+`;
+
+exports[`sfc props transform default values w/ type declaration 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+ props: {
+ foo: { type: Number, required: false, default: 1 },
+ bar: { type: Object, required: false, default: () => {} }
+ },
+ setup(__props: any) {
+
+
+
+return () => {}
+}
+
+})"
+`;
+
+exports[`sfc props transform default values w/ type declaration, prod mode 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+ props: {
+ foo: { default: 1 },
+ bar: { default: () => {} },
+ baz: null
+ },
+ setup(__props: any) {
+
+
+
+return () => {}
+}
+
+})"
+`;
+
+exports[`sfc props transform nested scope 1`] = `
+"export default {
+ props: ['foo', 'bar'],
+ setup(__props) {
+
+
+ function test(foo) {
+ console.log(foo)
+ console.log(__props.bar)
+ }
+
+return () => {}
+}
+
+}"
+`;
+
+exports[`sfc props transform rest spread 1`] = `
+"import { createPropsRestProxy as _createPropsRestProxy } from 'vue'
+
+export default {
+ props: ['foo', 'bar', 'baz'],
+ setup(__props) {
+
+const rest = _createPropsRestProxy(__props, [\\"foo\\",\\"bar\\"])
+
+
+return () => {}
+}
+
+}"
+`;
--- /dev/null
+import { BindingTypes } from '@vue/compiler-core'
+import { SFCScriptCompileOptions } from '../src'
+import { compileSFCScript, assertCode } from './utils'
+
+describe('sfc props transform', () => {
+ function compile(src: string, options?: Partial<SFCScriptCompileOptions>) {
+ return compileSFCScript(src, {
+ inlineTemplate: true,
+ propsDestructureTransform: true,
+ ...options
+ })
+ }
+
+ test('basic usage', () => {
+ const { content, bindings } = compile(`
+ <script setup>
+ const { foo } = defineProps(['foo'])
+ console.log(foo)
+ </script>
+ <template>{{ foo }}</template>
+ `)
+ 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(`
+ <script setup>
+ const { foo, bar } = defineProps(['foo', 'bar'])
+ function test(foo) {
+ console.log(foo)
+ console.log(bar)
+ }
+ </script>
+ `)
+ 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(`
+ <script setup>
+ const { foo = 1, bar = {} } = defineProps(['foo', 'bar'])
+ </script>
+ `)
+ // 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(`
+ <script setup lang="ts">
+ const { foo = 1, bar = {} } = defineProps<{ foo?: number, bar?: object }>()
+ </script>
+ `)
+ // 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(
+ `
+ <script setup lang="ts">
+ const { foo = 1, bar = {} } = defineProps<{ foo?: number, bar?: object, baz?: any }>()
+ </script>
+ `,
+ { 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(`
+ <script setup>
+ const { foo: bar } = defineProps(['foo'])
+ let x = foo
+ let y = bar
+ </script>
+ <template>{{ foo + bar }}</template>
+ `)
+ 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(`
+ <script setup>
+ const { foo, bar, ...rest } = defineProps(['foo', 'bar', 'baz'])
+ </script>
+ `)
+ 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(
+ `<script setup>const { foo: [bar] } = defineProps(['foo'])</script>`
+ )
+ ).toThrow(`destructure does not support nested patterns`)
+
+ expect(() =>
+ compile(
+ `<script setup>const { foo: { bar } } = defineProps(['foo'])</script>`
+ )
+ ).toThrow(`destructure does not support nested patterns`)
+ })
+
+ test('should error on computed key', () => {
+ expect(() =>
+ compile(
+ `<script setup>const { [foo]: bar } = defineProps(['foo'])</script>`
+ )
+ ).toThrow(`destructure cannot use computed key`)
+ })
+
+ test('should error when used with withDefaults', () => {
+ expect(() =>
+ compile(
+ `<script setup lang="ts">
+ const { foo } = withDefaults(defineProps<{ foo: string }>(), { foo: 'foo' })
+ </script>`
+ )
+ ).toThrow(`withDefaults() is unnecessary when using destructure`)
+ })
+
+ test('should error if destructure reference local vars', () => {
+ expect(() =>
+ compile(
+ `<script setup>
+ const x = 1
+ const {
+ foo = () => x
+ } = defineProps(['foo'])
+ </script>`
+ )
+ ).toThrow(`cannot reference locally declared variables`)
+ })
+ })
+})
TSInterfaceBody,
AwaitExpression,
Program,
- ObjectMethod
+ ObjectMethod,
+ LVal,
+ Expression
} from '@babel/types'
import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map'
/**
* (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.
*/
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
cssVars,
bindings,
scopeId,
- !!options.isProd
+ isProd
)
content += `\nexport default __default__`
}
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
const typeDeclaredEmits: Set<string> = new Set()
// record declared types for runtime props type generation
const declaredTypes: Record<string, string[]> = {}
+ // 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)
}
}
- function processDefineProps(node: Node): boolean {
+ function processDefineProps(node: Node, declId?: LVal): boolean {
if (!isCallOf(node, DEFINE_PROPS)) {
return false
}
}
}
+ 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 ` +
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 ||
return true
}
- function processDefineEmits(node: Node): boolean {
+ function processDefineEmits(node: Node, declId?: LVal): boolean {
if (!isCallOf(node, DEFINE_EMITS)) {
return false
}
)
}
}
+
+ if (declId) {
+ emitIdentifier = scriptSetup!.content.slice(declId.start!, declId.end!)
+ }
+
return true
}
* 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' &&
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
}
}
- if (__DEV__) {
+ if (!isProd) {
const { type, required } = props[key]
return `${key}: { type: ${toRuntimeTypeString(
type
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 = `{ `
// apply ref transform
if (enableRefTransform && shouldTransformRef(script.content)) {
- const { rootVars, importedHelpers } = transformRefAST(
+ const { rootRefs: rootVars, importedHelpers } = transformRefAST(
scriptAst,
s,
scriptStartOffset!
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)
}
// 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)
}
// 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)
// variables
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
+ checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS)
checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_PROPS)
// 6. remove non-script content
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
)) {
helperImports.add('unref')
s.prependRight(
startOffset,
- `\n${genCssVarsCode(
- cssVars,
- bindingMetadata,
- scopeId,
- !!options.isProd
- )}\n`
+ `\n${genCssVarsCode(cssVars, bindingMetadata, scopeId, isProd)}\n`
)
}
}`
)
}
+ 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` : ``
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)
}
}
s.trim()
+
return {
...scriptSetup,
bindings: bindingMetadata,
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 (
function extractRuntimeProps(
node: TSTypeLiteral | TSInterfaceBody,
props: Record<string, PropTypeData>,
- declaredTypes: Record<string, string[]>
+ declaredTypes: Record<string, string[]>,
+ isProd: boolean
) {
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
for (const m of members) {
m.key.type === 'Identifier'
) {
let type
- if (__DEV__) {
+ if (!isProd) {
if (m.type === 'TSMethodSignature') {
type = ['Function']
} else if (m.typeAnnotation) {
// @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: [
+ /* ... */
+ ]
})
```
const s = new MagicString(src)
const {
- rootVars, // ['a']
+ rootRefs, // ['a']
importedHelpers // ['ref']
} = transformAST(ast, s)
}
test('$ unwrapping', () => {
- const { code, rootVars } = transform(`
+ const { code, rootRefs } = transform(`
import { ref, shallowRef } from 'vue'
let foo = $(ref())
let a = $(ref(1))
// 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({
// 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
})
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)
})
})
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)
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)
`)
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)
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)
})
})
test('nested scopes', () => {
- const { code, rootVars } = transform(`
+ const { code, rootRefs } = transform(`
let a = $ref(0)
let b = $ref(0)
let c = 0
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')
return transformCheckRE.test(src)
}
-type Scope = Record<string, boolean>
+type Scope = Record<string, boolean | 'prop'>
export interface RefTransformOptions {
filename?: string
export interface RefTransformResults {
code: string
map: SourceMap | null
- rootVars: string[]
+ rootRefs: string[]
importedHelpers: string[]
}
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<string>()
const rootScope: Scope = {}
let currentScope: Scope = rootScope
const excludedIds = new WeakSet<Identifier>()
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
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,
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)
+ }
}
}
}
}
}
- 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
}
// check root scope first
- walkScope(ast)
+ walkScope(ast, true)
;(walk as any)(ast, {
enter(node: Node, parent?: Node) {
parent && parentStack.push(parent)
// 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
}
}
})
return {
- rootVars: Object.keys(rootScope).filter(key => rootScope[key]),
+ rootRefs: Object.keys(rootScope).filter(key => rootScope[key] === true),
importedHelpers: [...importedHelpers]
}
}
SetupContext,
Suspense,
computed,
- ComputedRef
+ ComputedRef,
+ shallowReactive
} from '@vue/runtime-test'
import {
defineEmits,
useAttrs,
useSlots,
mergeDefaults,
- withAsyncContext
+ withAsyncContext,
+ createPropsRestProxy
} from '../src/apiSetupHelpers'
describe('SFC <script setup> helpers', () => {
expect(attrs).toBe(ctx!.attrs)
})
- test('mergeDefaults', () => {
- const merged = mergeDefaults(
- {
- foo: null,
- bar: { type: String, required: false }
- },
- {
+ describe('mergeDefaults', () => {
+ test('object syntax', () => {
+ const merged = mergeDefaults(
+ {
+ foo: null,
+ bar: { type: String, required: false },
+ baz: String
+ },
+ {
+ foo: 1,
+ bar: 'baz',
+ baz: 'qux'
+ }
+ )
+ expect(merged).toMatchObject({
+ foo: { default: 1 },
+ bar: { type: String, required: false, default: 'baz' },
+ baz: { type: String, default: 'qux' }
+ })
+ })
+
+ test('array syntax', () => {
+ const merged = mergeDefaults(['foo', 'bar', 'baz'], {
foo: 1,
- bar: 'baz'
- }
- )
- expect(merged).toMatchObject({
- foo: { default: 1 },
- bar: { type: String, required: false, default: 'baz' }
+ bar: 'baz',
+ baz: 'qux'
+ })
+ expect(merged).toMatchObject({
+ foo: { default: 1 },
+ bar: { default: 'baz' },
+ baz: { default: 'qux' }
+ })
+ })
+
+ test('should warn missing', () => {
+ mergeDefaults({}, { foo: 1 })
+ expect(
+ `props default key "foo" has no corresponding declaration`
+ ).toHaveBeenWarned()
})
+ })
- mergeDefaults({}, { foo: 1 })
- expect(
- `props default key "foo" has no corresponding declaration`
- ).toHaveBeenWarned()
+ describe('createPropsRestProxy', () => {
+ const original = shallowReactive({
+ foo: 1,
+ bar: 2,
+ baz: 3
+ })
+ const rest = createPropsRestProxy(original, ['foo', 'bar'])
+ expect('foo' in rest).toBe(false)
+ expect('bar' in rest).toBe(false)
+ expect(rest.baz).toBe(3)
+ expect(Object.keys(rest)).toEqual(['baz'])
+
+ original.baz = 4
+ expect(rest.baz).toBe(4)
})
describe('withAsyncContext', () => {
-import { isPromise } from '../../shared/src'
+import { ComponentPropsOptions } from '@vue/runtime-core'
+import { isArray, isPromise, isFunction } from '@vue/shared'
import {
getCurrentInstance,
setCurrentInstance,
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
* @internal
*/
export function mergeDefaults(
- // the base props is compiler-generated and guaranteed to be in this shape.
- props: Record<string, PropOptions | null>,
+ raw: ComponentPropsOptions,
defaults: Record<string, any>
-) {
+): ComponentObjectPropsOptions {
+ 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.`)
return props
}
+/**
+ * Used to create a proxy for the rest element when destructuring props with
+ * defineProps().
+ * @internal
+ */
+export function createPropsRestProxy(
+ props: any,
+ excludedKeys: string[]
+): Record<string, any> {
+ const ret: Record<string, any> = {}
+ for (const key in props) {
+ if (!excludedKeys.includes(key)) {
+ Object.defineProperty(ret, key, {
+ enumerable: true,
+ get: () => props[key]
+ })
+ }
+ }
+ return ret
+}
+
/**
* `<script setup>` helper for persisting the current instance context over
* async/await flows.
withDefaults,
// internal
mergeDefaults,
+ createPropsRestProxy,
withAsyncContext
} from './apiSetupHelpers'
"vite": "^2.5.10"
},
"dependencies": {
- "@vue/repl": "^0.4.1",
+ "@vue/repl": "^0.4.2",
"file-saver": "^2.0.5",
"jszip": "^3.6.0"
}
: `${location.origin}/src/vue-dev-proxy`
})
+// enable experimental features
+const sfcOptions = {
+ script: {
+ refTransform: true,
+ propsDestructureTransform: true
+ }
+}
+
// persist state
watchEffect(() => history.replaceState({}, '', store.serialize()))
</script>
<template>
<Header :store="store" />
- <Repl :store="store" :showCompileOutput="true" :autoResize="true" />
+ <Repl
+ :store="store"
+ :showCompileOutput="true"
+ :autoResize="true"
+ :sfcOptions="sfcOptions"
+ :clearConsole="false"
+ />
</template>
<style>
resolved "https://registry.yarnpkg.com/@vue/consolidate/-/consolidate-0.17.3.tgz#9614d25a2eb263fa5df18ce98b0a576142e0ec83"
integrity sha512-nl0SWcTMzaaTnJ5G6V8VlMDA1CVVrNnaQKF1aBZU3kXtjgU9jtHMsEAsgjoRUx+T0EVJk9TgbmxGhK3pOk22zw==
-"@vue/repl@^0.4.1":
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/@vue/repl/-/repl-0.4.1.tgz#b2062bea2baa077520eb01b87df51fff357961be"
- integrity sha512-Rq9q0MHRA0YRGBE2VSFL5ZkllqLK5HnFK9/+6Iu75M39BZAacwf808fHPTN0bhYGkJ63ur2i0sEYnnNzWzluPg==
+"@vue/repl@^0.4.2":
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/@vue/repl/-/repl-0.4.2.tgz#594d36061201195222bc91d187cd766d692f2046"
+ integrity sha512-Spg+M7dENa0jfjEhb2odLP5uqy28fBAMmURL+vFQUIkxcvtFaMkKY3viklhFqacfQqD6YTZfnLWSXyB+F6r/eQ==
"@zeit/schemas@2.6.0":
version "2.6.0"