Skip to content

Commit

Permalink
feat(compiler-sfc): <script setup> defineProps destructure transform (
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Sep 27, 2021
1 parent d84d5ec commit 467e113
Show file tree
Hide file tree
Showing 14 changed files with 716 additions and 123 deletions.
6 changes: 6 additions & 0 deletions packages/compiler-core/src/options.ts
Expand Up @@ -82,6 +82,11 @@ export const enum BindingTypes {
* 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)
*/
Expand Down Expand Up @@ -110,6 +115,7 @@ export type BindingMetadata = {
[key: string]: BindingTypes | undefined
} & {
__isScriptSetup?: boolean
__propsAliases?: Record<string, string>
}

interface SharedTransformCodegenOptions {
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler-core/src/transforms/transformExpression.ts
Expand Up @@ -188,11 +188,16 @@ export function processExpression(
// 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}`
}
Expand Down
@@ -0,0 +1,128 @@
// 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 () => {}
}
}"
`;
191 changes: 191 additions & 0 deletions packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts
@@ -0,0 +1,191 @@
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`)
})
})
})

0 comments on commit 467e113

Please sign in to comment.