Skip to content

Commit

Permalink
feat(compiler-sfc): add defineOptions macro (#5738)
Browse files Browse the repository at this point in the history
  • Loading branch information
sxzz committed Mar 28, 2023
1 parent f77bd36 commit bcf5841
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 5 deletions.
Expand Up @@ -634,6 +634,19 @@ return { }
}"
`;

exports[`SFC compile <script setup> > defineOptions() > basic usage 1`] = `
"export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, {
setup(__props, { expose }) {
expose();



return { }
}

})"
`;

exports[`SFC compile <script setup> > defineProps w/ external definition 1`] = `
"import { propsModel } from './props'

Expand Down
65 changes: 63 additions & 2 deletions packages/compiler-sfc/__tests__/compileScript.spec.ts
Expand Up @@ -172,6 +172,68 @@ const myEmit = defineEmits(['foo', 'bar'])
expect(content).toMatch(`emits: ['a'],`)
})

describe('defineOptions()', () => {
test('basic usage', () => {
const { content } = compile(`
<script setup>
defineOptions({ name: 'FooApp' })
</script>
`)
assertCode(content)
// should remove defineOptions import and call
expect(content).not.toMatch('defineOptions')
// should include context options in default export
expect(content).toMatch(
`export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, `
)
})

it('should emit an error with two defineProps', () => {
expect(() =>
compile(`
<script setup>
defineOptions({ name: 'FooApp' })
defineOptions({ name: 'BarApp' })
</script>
`)
).toThrowError('[@vue/compiler-sfc] duplicate defineOptions() call')
})

it('should emit an error with props or emits property', () => {
expect(() =>
compile(`
<script setup>
defineOptions({ props: { foo: String } })
</script>
`)
).toThrowError(
'[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.'
)

expect(() =>
compile(`
<script setup>
defineOptions({ emits: ['update'] })
</script>
`)
).toThrowError(
'[@vue/compiler-sfc] defineOptions() cannot be used to declare emits. Use defineEmits() instead.'
)
})

it('should emit an error with type generic', () => {
expect(() =>
compile(`
<script setup lang="ts">
defineOptions<{ name: 'FooApp' }>()
</script>
`)
).toThrowError(
'[@vue/compiler-sfc] defineOptions() cannot accept type arguments'
)
})
})

test('defineExpose()', () => {
const { content } = compile(`
<script setup>
Expand Down Expand Up @@ -1136,7 +1198,7 @@ const emit = defineEmits(['a', 'b'])
`)
assertCode(content)
})

// #7111
test('withDefaults (static) w/ production mode', () => {
const { content } = compile(
Expand Down Expand Up @@ -1277,7 +1339,6 @@ const emit = defineEmits(['a', 'b'])
expect(content).toMatch(`emits: ["foo", "bar"]`)
})


test('defineEmits w/ type from normal script', () => {
const { content } = compile(`
<script lang="ts">
Expand Down
73 changes: 70 additions & 3 deletions packages/compiler-sfc/src/compileScript.ts
Expand Up @@ -62,6 +62,7 @@ const DEFINE_PROPS = 'defineProps'
const DEFINE_EMITS = 'defineEmits'
const DEFINE_EXPOSE = 'defineExpose'
const WITH_DEFAULTS = 'withDefaults'
const DEFINE_OPTIONS = 'defineOptions'

// constants
const DEFAULT_VAR = `__default__`
Expand Down Expand Up @@ -270,6 +271,7 @@ export function compileScript(
let hasDefineExposeCall = false
let hasDefaultExportName = false
let hasDefaultExportRender = false
let hasDefineOptionsCall = false
let propsRuntimeDecl: Node | undefined
let propsRuntimeDefaults: ObjectExpression | undefined
let propsDestructureDecl: Node | undefined
Expand All @@ -281,6 +283,7 @@ export function compileScript(
let emitsTypeDecl: EmitsDeclType | undefined
let emitsTypeDeclRaw: Node | undefined
let emitIdentifier: string | undefined
let optionsRuntimeDecl: Node | undefined
let hasAwait = false
let hasInlinedSsrRenderFn = false
// props/emits declared via types
Expand Down Expand Up @@ -647,6 +650,50 @@ export function compileScript(
})
}

function processDefineOptions(node: Node): boolean {
if (!isCallOf(node, DEFINE_OPTIONS)) {
return false
}
if (hasDefineOptionsCall) {
error(`duplicate ${DEFINE_OPTIONS}() call`, node)
}
if (node.typeParameters) {
error(`${DEFINE_OPTIONS}() cannot accept type arguments`, node)
}

hasDefineOptionsCall = true
optionsRuntimeDecl = node.arguments[0]

let propsOption = undefined
let emitsOption = undefined
if (optionsRuntimeDecl.type === 'ObjectExpression') {
for (const prop of optionsRuntimeDecl.properties) {
if (
(prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
prop.key.type === 'Identifier'
) {
if (prop.key.name === 'props') propsOption = prop
if (prop.key.name === 'emits') emitsOption = prop
}
}
}

if (propsOption) {
error(
`${DEFINE_OPTIONS}() cannot be used to declare props. Use ${DEFINE_PROPS}() instead.`,
propsOption
)
}
if (emitsOption) {
error(
`${DEFINE_OPTIONS}() cannot be used to declare emits. Use ${DEFINE_EMITS}() instead.`,
emitsOption
)
}

return true
}

function resolveQualifiedType(
node: Node,
qualifier: (node: Node) => boolean
Expand Down Expand Up @@ -1175,6 +1222,7 @@ export function compileScript(
if (
processDefineProps(node.expression) ||
processDefineEmits(node.expression) ||
processDefineOptions(node.expression) ||
processWithDefaults(node.expression)
) {
s.remove(node.start! + startOffset, node.end! + startOffset)
Expand All @@ -1195,6 +1243,13 @@ export function compileScript(
for (let i = 0; i < total; i++) {
const decl = node.declarations[i]
if (decl.init) {
if (processDefineOptions(decl.init)) {
error(
`${DEFINE_OPTIONS}() has no returning value, it cannot be assigned.`,
node
)
}

// defineProps / defineEmits
const isDefineProps =
processDefineProps(decl.init, decl.id, node.kind) ||
Expand Down Expand Up @@ -1339,6 +1394,7 @@ export function compileScript(
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS)
checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_EMITS)
checkInvalidScopeReference(optionsRuntimeDecl, DEFINE_OPTIONS)

// 6. remove non-script content
if (script) {
Expand Down Expand Up @@ -1626,6 +1682,13 @@ export function compileScript(
runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
}

let definedOptions = ''
if (optionsRuntimeDecl) {
definedOptions = scriptSetup.content
.slice(optionsRuntimeDecl.start!, optionsRuntimeDecl.end!)
.trim()
}

// <script setup> components are closed by default. If the user did not
// explicitly call `defineExpose`, call expose() with no args.
const exposeCall =
Expand All @@ -1637,7 +1700,9 @@ export function compileScript(
// we have to use object spread for types to be merged properly
// user's TS setting should compile it down to proper targets
// export default defineComponent({ ...__default__, ... })
const def = defaultExport ? `\n ...${DEFAULT_VAR},` : ``
const def =
(defaultExport ? `\n ...${DEFAULT_VAR},` : ``) +
(definedOptions ? `\n ...${definedOptions},` : '')
s.prependLeft(
startOffset,
`\nexport default /*#__PURE__*/${helper(
Expand All @@ -1648,12 +1713,14 @@ export function compileScript(
)
s.appendRight(endOffset, `})`)
} else {
if (defaultExport) {
if (defaultExport || definedOptions) {
// without TS, can't rely on rest spread, so we use Object.assign
// export default Object.assign(__default__, { ... })
s.prependLeft(
startOffset,
`\nexport default /*#__PURE__*/Object.assign(${DEFAULT_VAR}, {${runtimeOptions}\n ` +
`\nexport default /*#__PURE__*/Object.assign(${
defaultExport ? `${DEFAULT_VAR}, ` : ''
}${definedOptions ? `${definedOptions}, ` : ''}{${runtimeOptions}\n ` +
`${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`
)
s.appendRight(endOffset, `})`)
Expand Down
33 changes: 33 additions & 0 deletions packages/runtime-core/src/apiSetupHelpers.ts
Expand Up @@ -7,6 +7,12 @@ import {
unsetCurrentInstance
} from './component'
import { EmitFn, EmitsOptions } from './componentEmits'
import {
ComponentOptionsMixin,
ComponentOptionsWithoutProps,
ComputedOptions,
MethodOptions
} from './componentOptions'
import {
ComponentPropsOptions,
ComponentObjectPropsOptions,
Expand Down Expand Up @@ -143,6 +149,33 @@ export function defineExpose<
}
}

export function defineOptions<
RawBindings = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string
>(
options?: ComponentOptionsWithoutProps<
{},
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
EE
> & { emits?: undefined }
): void {
if (__DEV__) {
warnRuntimeUsage(`defineOptions`)
}
}

type NotUndefined<T> = T extends undefined ? never : T

type InferDefaults<T> = {
Expand Down
1 change: 1 addition & 0 deletions packages/runtime-core/src/index.ts
Expand Up @@ -68,6 +68,7 @@ export {
defineProps,
defineEmits,
defineExpose,
defineOptions,
withDefaults,
// internal
mergeDefaults,
Expand Down
2 changes: 2 additions & 0 deletions packages/runtime-core/types/scriptSetupHelpers.d.ts
Expand Up @@ -3,11 +3,13 @@
type _defineProps = typeof defineProps
type _defineEmits = typeof defineEmits
type _defineExpose = typeof defineExpose
type _defineOptions = typeof defineOptions
type _withDefaults = typeof withDefaults

declare global {
const defineProps: _defineProps
const defineEmits: _defineEmits
const defineExpose: _defineExpose
const defineOptions: _defineOptions
const withDefaults: _withDefaults
}

0 comments on commit bcf5841

Please sign in to comment.