wip: useOptions -> defineOptions

This commit is contained in:
Evan You 2020-11-13 17:38:28 -05:00
parent 91d990d45a
commit 1ff5960971
6 changed files with 123 additions and 124 deletions

View File

@ -56,7 +56,25 @@ return { color }
}" }"
`; `;
exports[`SFC compile <script setup> errors should allow useOptions() referencing imported binding 1`] = ` exports[`SFC compile <script setup> defineOptions() 1`] = `
"export default {
props: {
foo: String
},
emit: ['a', 'b'],
setup(__props, { props, emit }) {
const bar = 1
return { props, emit, bar }
}
}"
`;
exports[`SFC compile <script setup> errors should allow defineOptions() referencing imported binding 1`] = `
"import { bar } from './bar' "import { bar } from './bar'
export default { export default {
@ -75,7 +93,7 @@ return { bar }
}" }"
`; `;
exports[`SFC compile <script setup> errors should allow useOptions() referencing scope var 1`] = ` exports[`SFC compile <script setup> errors should allow defineOptions() referencing scope var 1`] = `
"export default { "export default {
props: { props: {
foo: { foo: {
@ -375,40 +393,7 @@ return { a, b, c, d, x }
}" }"
`; `;
exports[`SFC compile <script setup> useOptions() 1`] = ` exports[`SFC compile <script setup> with TypeScript defineOptions w/ runtime options 1`] = `
"export default {
props: {
foo: String
},
emit: ['a', 'b'],
setup(__props, { props, emit }) {
const bar = 1
return { props, emit, bar }
}
}"
`;
exports[`SFC compile <script setup> with TypeScript hoist type declarations 1`] = `
"import { defineComponent } from 'vue'
export interface Foo {}
type Bar = {}
export default defineComponent({
setup() {
return { }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript useOptions w/ runtime options 1`] = `
"import { defineComponent } from 'vue' "import { defineComponent } from 'vue'
@ -425,7 +410,7 @@ return { props, emit }
})" })"
`; `;
exports[`SFC compile <script setup> with TypeScript useOptions w/ type / extract emits (union) 1`] = ` exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract emits (union) 1`] = `
"import { Slots, defineComponent } from 'vue' "import { Slots, defineComponent } from 'vue'
@ -446,7 +431,7 @@ return { emit }
})" })"
`; `;
exports[`SFC compile <script setup> with TypeScript useOptions w/ type / extract emits 1`] = ` exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract emits 1`] = `
"import { Slots, defineComponent } from 'vue' "import { Slots, defineComponent } from 'vue'
@ -467,7 +452,7 @@ return { emit }
})" })"
`; `;
exports[`SFC compile <script setup> with TypeScript useOptions w/ type / extract props 1`] = ` exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract props 1`] = `
"import { defineComponent } from 'vue' "import { defineComponent } from 'vue'
interface Test {} interface Test {}
@ -503,6 +488,21 @@ export default defineComponent({
return { }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript hoist type declarations 1`] = `
"import { defineComponent } from 'vue'
export interface Foo {}
type Bar = {}
export default defineComponent({
setup() {
return { } return { }
} }

View File

@ -36,11 +36,11 @@ describe('SFC compile <script setup>', () => {
expect(content).toMatch('return { a, b, c, d, x }') expect(content).toMatch('return { a, b, c, d, x }')
}) })
test('useOptions()', () => { test('defineOptions()', () => {
const { content, bindings } = compile(` const { content, bindings } = compile(`
<script setup> <script setup>
import { useOptions } from 'vue' import { defineOptions } from 'vue'
const { props, emit } = useOptions({ const { props, emit } = defineOptions({
props: { props: {
foo: String foo: String
}, },
@ -60,8 +60,8 @@ const bar = 1
emit: 'const' emit: 'const'
}) })
// should remove useOptions import and call // should remove defineOptions import and call
expect(content).not.toMatch('useOptions') expect(content).not.toMatch('defineOptions')
// should generate correct setup signature // should generate correct setup signature
expect(content).toMatch(`setup(__props, { props, emit }) {`) expect(content).toMatch(`setup(__props, { props, emit }) {`)
// should include context options in default export // should include context options in default export
@ -143,7 +143,7 @@ const bar = 1
const { content } = compile( const { content } = compile(
` `
<script setup> <script setup>
import { ref, useOptions } from 'vue' import { ref, defineOptions } from 'vue'
import Foo from './Foo.vue' import Foo from './Foo.vue'
import other from './util' import other from './util'
const count = ref(0) const count = ref(0)
@ -183,11 +183,11 @@ const bar = 1
assertCode(content) assertCode(content)
}) })
test('useOptions w/ runtime options', () => { test('defineOptions w/ runtime options', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
import { useOptions } from 'vue' import { defineOptions } from 'vue'
const { props, emit } = useOptions({ const { props, emit } = defineOptions({
props: { foo: String }, props: { foo: String },
emits: ['a', 'b'] emits: ['a', 'b']
}) })
@ -200,15 +200,15 @@ const { props, emit } = useOptions({
setup(__props, { props, emit }) {`) setup(__props, { props, emit }) {`)
}) })
test('useOptions w/ type / extract props', () => { test('defineOptions w/ type / extract props', () => {
const { content, bindings } = compile(` const { content, bindings } = compile(`
<script setup lang="ts"> <script setup lang="ts">
import { useOptions } from 'vue' import { defineOptions } from 'vue'
interface Test {} interface Test {}
type Alias = number[] type Alias = number[]
useOptions<{ defineOptions<{
props: { props: {
string: string string: string
number: number number: number
@ -288,11 +288,11 @@ const { props, emit } = useOptions({
}) })
}) })
test('useOptions w/ type / extract emits', () => { test('defineOptions w/ type / extract emits', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
import { useOptions } from 'vue' import { defineOptions } from 'vue'
const { emit } = useOptions<{ const { emit } = defineOptions<{
emit: (e: 'foo' | 'bar') => void emit: (e: 'foo' | 'bar') => void
}>() }>()
</script> </script>
@ -302,11 +302,11 @@ const { props, emit } = useOptions({
expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`) expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
}) })
test('useOptions w/ type / extract emits (union)', () => { test('defineOptions w/ type / extract emits (union)', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
import { useOptions } from 'vue' import { defineOptions } from 'vue'
const { emit } = useOptions<{ const { emit } = defineOptions<{
emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void) emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)
}>() }>()
</script> </script>
@ -633,21 +633,21 @@ const { props, emit } = useOptions({
).toThrow(`ref: statements can only contain assignment expressions`) ).toThrow(`ref: statements can only contain assignment expressions`)
}) })
test('useOptions() w/ both type and non-type args', () => { test('defineOptions() w/ both type and non-type args', () => {
expect(() => { expect(() => {
compile(`<script setup lang="ts"> compile(`<script setup lang="ts">
import { useOptions } from 'vue' import { defineOptions } from 'vue'
useOptions<{}>({}) defineOptions<{}>({})
</script>`) </script>`)
}).toThrow(`cannot accept both type and non-type arguments`) }).toThrow(`cannot accept both type and non-type arguments`)
}) })
test('useOptions() referencing local var', () => { test('defineOptions() referencing local var', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
import { useOptions } from 'vue' import { defineOptions } from 'vue'
const bar = 1 const bar = 1
useOptions({ defineOptions({
props: { props: {
foo: { foo: {
default: () => bar default: () => bar
@ -658,24 +658,24 @@ const { props, emit } = useOptions({
).toThrow(`cannot reference locally declared variables`) ).toThrow(`cannot reference locally declared variables`)
}) })
test('useOptions() referencing ref declarations', () => { test('defineOptions() referencing ref declarations', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
import { useOptions } from 'vue' import { defineOptions } from 'vue'
ref: bar = 1 ref: bar = 1
useOptions({ defineOptions({
props: { bar } props: { bar }
}) })
</script>`) </script>`)
).toThrow(`cannot reference locally declared variables`) ).toThrow(`cannot reference locally declared variables`)
}) })
test('should allow useOptions() referencing scope var', () => { test('should allow defineOptions() referencing scope var', () => {
assertCode( assertCode(
compile(`<script setup> compile(`<script setup>
import { useOptions } from 'vue' import { defineOptions } from 'vue'
const bar = 1 const bar = 1
useOptions({ defineOptions({
props: { props: {
foo: { foo: {
default: bar => bar + 1 default: bar => bar + 1
@ -686,12 +686,12 @@ const { props, emit } = useOptions({
) )
}) })
test('should allow useOptions() referencing imported binding', () => { test('should allow defineOptions() referencing imported binding', () => {
assertCode( assertCode(
compile(`<script setup> compile(`<script setup>
import { useOptions } from 'vue' import { defineOptions } from 'vue'
import { bar } from './bar' import { bar } from './bar'
useOptions({ defineOptions({
props: { props: {
foo: { foo: {
default: () => bar default: () => bar
@ -901,8 +901,8 @@ describe('SFC analyze <script> bindings', () => {
it('works for script setup', () => { it('works for script setup', () => {
const { bindings } = compile(` const { bindings } = compile(`
<script setup> <script setup>
import { useOptions } from 'vue' import { defineOptions } from 'vue'
useOptions({ defineOptions({
props: { props: {
foo: String, foo: String,
} }

View File

@ -27,7 +27,7 @@ import { RawSourceMap } from 'source-map'
import { genCssVarsCode, injectCssVarsCalls } from './genCssVars' import { genCssVarsCode, injectCssVarsCalls } from './genCssVars'
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate' import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
const USE_OPTIONS = 'useOptions' const DEFINE_OPTIONS = 'defineOptions'
export interface SFCScriptCompileOptions { export interface SFCScriptCompileOptions {
/** /**
@ -147,7 +147,18 @@ export function compileScript(
let optionsArg: ObjectExpression | undefined let optionsArg: ObjectExpression | undefined
let optionsType: TSTypeLiteral | undefined let optionsType: TSTypeLiteral | undefined
let hasAwait = false let hasAwait = false
// context types to generate
let propsType = `{}`
let emitType = `(e: string, ...args: any[]) => void`
let slotsType = `Slots`
let attrsType = `Record<string, any>`
// props/emits declared via types
const typeDeclaredProps: Record<string, PropTypeData> = {}
const typeDeclaredEmits: Set<string> = new Set()
// record declared types for runtime props type generation
const declaredTypes: Record<string, string[]> = {}
// magic-string state
const s = new MagicString(source) const s = new MagicString(source)
const startOffset = scriptSetup.loc.start.offset const startOffset = scriptSetup.loc.start.offset
const endOffset = scriptSetup.loc.end.offset const endOffset = scriptSetup.loc.end.offset
@ -182,14 +193,14 @@ export function compileScript(
) )
} }
function processUseOptions(node: Node): boolean { function processDefineOptions(node: Node): boolean {
if ( if (
node.type === 'CallExpression' && node.type === 'CallExpression' &&
node.callee.type === 'Identifier' && node.callee.type === 'Identifier' &&
node.callee.name === USE_OPTIONS node.callee.name === DEFINE_OPTIONS
) { ) {
if (hasOptionsCall) { if (hasOptionsCall) {
error(`duplicate ${USE_OPTIONS}() call`, node) error(`duplicate ${DEFINE_OPTIONS}() call`, node)
} }
hasOptionsCall = true hasOptionsCall = true
const optsArg = node.arguments[0] const optsArg = node.arguments[0]
@ -197,14 +208,17 @@ export function compileScript(
if (optsArg.type === 'ObjectExpression') { if (optsArg.type === 'ObjectExpression') {
optionsArg = optsArg optionsArg = optsArg
} else { } else {
error(`${USE_OPTIONS}() argument must be an object literal.`, optsArg) error(
`${DEFINE_OPTIONS}() argument must be an object literal.`,
optsArg
)
} }
} }
// context call has type parameters - infer runtime types from it // context call has type parameters - infer runtime types from it
if (node.typeParameters) { if (node.typeParameters) {
if (optionsArg) { if (optionsArg) {
error( error(
`${USE_OPTIONS}() cannot accept both type and non-type arguments ` + `${DEFINE_OPTIONS}() cannot accept both type and non-type arguments ` +
`at the same time. Use one or the other.`, `at the same time. Use one or the other.`,
node node
) )
@ -214,7 +228,7 @@ export function compileScript(
optionsType = typeArg optionsType = typeArg
} else { } else {
error( error(
`type argument passed to ${USE_OPTIONS}() must be a literal type.`, `type argument passed to ${DEFINE_OPTIONS}() must be a literal type.`,
typeArg typeArg
) )
} }
@ -427,18 +441,7 @@ export function compileScript(
} }
} }
let propsType = `{}` // 2. parse <script setup> and walk over top level statements
let emitType = `(e: string, ...args: any[]) => void`
let slotsType = `Slots`
let attrsType = `Record<string, any>`
// props/emits declared via types
const typeDeclaredProps: Record<string, PropTypeData> = {}
const typeDeclaredEmits: Set<string> = new Set()
// record declared types for runtime props type generation
const declaredTypes: Record<string, string[]> = {}
// 3. parse <script setup> and walk over top level statements
const scriptSetupAst = parse( const scriptSetupAst = parse(
scriptSetup.content, scriptSetup.content,
{ {
@ -512,7 +515,7 @@ export function compileScript(
specifier.imported.name specifier.imported.name
const source = node.source.value const source = node.source.value
const existing = userImports[local] const existing = userImports[local]
if (source === 'vue' && imported === USE_OPTIONS) { if (source === 'vue' && imported === DEFINE_OPTIONS) {
removed++ removed++
s.remove( s.remove(
prev ? prev.end! + startOffset : specifier.start! + startOffset, prev ? prev.end! + startOffset : specifier.start! + startOffset,
@ -544,14 +547,14 @@ export function compileScript(
if ( if (
node.type === 'ExpressionStatement' && node.type === 'ExpressionStatement' &&
processUseOptions(node.expression) processDefineOptions(node.expression)
) { ) {
s.remove(node.start! + startOffset, node.end! + startOffset) s.remove(node.start! + startOffset, node.end! + startOffset)
} }
if (node.type === 'VariableDeclaration' && !node.declare) { if (node.type === 'VariableDeclaration' && !node.declare) {
for (const decl of node.declarations) { for (const decl of node.declarations) {
if (decl.init && processUseOptions(decl.init)) { if (decl.init && processDefineOptions(decl.init)) {
optionsExp = scriptSetup.content.slice(decl.id.start!, decl.id.end!) optionsExp = scriptSetup.content.slice(decl.id.start!, decl.id.end!)
if (node.declarations.length === 1) { if (node.declarations.length === 1) {
s.remove(node.start! + startOffset, node.end! + startOffset) s.remove(node.start! + startOffset, node.end! + startOffset)
@ -618,7 +621,7 @@ export function compileScript(
} }
} }
// 4. Do a full walk to rewrite identifiers referencing let exports with ref // 3. Do a full walk to rewrite identifiers referencing let exports with ref
// value access // value access
if (enableRefSugar && Object.keys(refBindings).length) { if (enableRefSugar && Object.keys(refBindings).length) {
for (const node of scriptSetupAst) { for (const node of scriptSetupAst) {
@ -644,7 +647,7 @@ export function compileScript(
} }
} }
// 5. extract runtime props/emits code from setup context type // 4. extract runtime props/emits code from setup context type
if (optionsType) { if (optionsType) {
for (const m of optionsType.members) { for (const m of optionsType.members) {
if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') { if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') {
@ -690,7 +693,7 @@ export function compileScript(
walkIdentifiers(optionsArg, id => { walkIdentifiers(optionsArg, id => {
if (setupBindings[id.name]) { if (setupBindings[id.name]) {
error( error(
`\`${USE_OPTIONS}()\` in <script setup> cannot reference locally ` + `\`${DEFINE_OPTIONS}()\` in <script setup> cannot reference locally ` +
`declared variables because it will be hoisted outside of the ` + `declared variables because it will be hoisted outside of the ` +
`setup() function. If your component options requires initialization ` + `setup() function. If your component options requires initialization ` +
`in the module scope, use a separate normal <script> to export ` + `in the module scope, use a separate normal <script> to export ` +
@ -739,7 +742,7 @@ export function compileScript(
allBindings[key] = true allBindings[key] = true
} }
// 9. inject `useCssVars` calls // 8. inject `useCssVars` calls
if (hasCssVars) { if (hasCssVars) {
helperImports.add(`useCssVars`) helperImports.add(`useCssVars`)
for (const style of styles) { for (const style of styles) {
@ -753,7 +756,7 @@ export function compileScript(
} }
} }
// 10. analyze binding metadata // 9. analyze binding metadata
if (scriptAst) { if (scriptAst) {
Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst)) Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
} }
@ -774,7 +777,7 @@ export function compileScript(
bindingMetadata[key] = setupBindings[key] bindingMetadata[key] = setupBindings[key]
} }
// 11. generate return statement // 10. generate return statement
let returned let returned
if (options.inlineTemplate) { if (options.inlineTemplate) {
if (sfc.template) { if (sfc.template) {
@ -812,7 +815,7 @@ export function compileScript(
} }
s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`) s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
// 12. finalize default export // 11. finalize default export
let runtimeOptions = `` let runtimeOptions = ``
if (optionsArg) { if (optionsArg) {
runtimeOptions = `\n ${scriptSetup.content runtimeOptions = `\n ${scriptSetup.content
@ -822,7 +825,6 @@ export function compileScript(
runtimeOptions = runtimeOptions =
genRuntimeProps(typeDeclaredProps) + genRuntimeEmits(typeDeclaredEmits) genRuntimeProps(typeDeclaredProps) + genRuntimeEmits(typeDeclaredEmits)
} }
if (isTS) { if (isTS) {
// for TS, make sure the exported type is still valid type with // for TS, make sure the exported type is still valid type with
// correct props information // correct props information
@ -861,7 +863,7 @@ export function compileScript(
} }
} }
// 13. finalize Vue helper imports // 12. finalize Vue helper imports
// TODO account for cases where user imports a helper with the same name // TODO account for cases where user imports a helper with the same name
// from a non-vue source // from a non-vue source
const helpers = [...helperImports].filter(i => !userImports[i]) const helpers = [...helperImports].filter(i => !userImports[i])
@ -897,7 +899,7 @@ function walkDeclaration(
init && init &&
init.type === 'CallExpression' && init.type === 'CallExpression' &&
init.callee.type === 'Identifier' && init.callee.type === 'Identifier' &&
init.callee.name === USE_OPTIONS init.callee.name === DEFINE_OPTIONS
) )
if (id.type === 'Identifier') { if (id.type === 'Identifier') {
bindings[id.name] = bindings[id.name] =

View File

@ -1,11 +1,8 @@
import { EmitFn, EmitsOptions } from '../componentEmits' import { EmitFn, EmitsOptions } from './componentEmits'
import { import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
ComponentObjectPropsOptions, import { Slots } from './componentSlots'
ExtractPropTypes import { Directive } from './directives'
} from '../componentProps' import { warn } from './warning'
import { Slots } from '../componentSlots'
import { Directive } from '../directives'
import { warn } from '../warning'
interface DefaultContext { interface DefaultContext {
props: {} props: {}
@ -45,7 +42,7 @@ interface Options<E extends EmitsOptions, EE extends string> {
* called at runtime. * called at runtime.
*/ */
// overload 1: no props // overload 1: no props
export function useOptions< export function defineOptions<
T extends Partial<DefaultContext> = {}, T extends Partial<DefaultContext> = {},
E extends EmitsOptions = EmitsOptions, E extends EmitsOptions = EmitsOptions,
EE extends string = string EE extends string = string
@ -56,7 +53,7 @@ export function useOptions<
): InferContext<T, {}, E> ): InferContext<T, {}, E>
// overload 2: object props // overload 2: object props
export function useOptions< export function defineOptions<
T extends Partial<DefaultContext> = {}, T extends Partial<DefaultContext> = {},
E extends EmitsOptions = EmitsOptions, E extends EmitsOptions = EmitsOptions,
EE extends string = string, EE extends string = string,
@ -69,7 +66,7 @@ export function useOptions<
): InferContext<T, P, E> ): InferContext<T, P, E>
// overload 3: object props // overload 3: object props
export function useOptions< export function defineOptions<
T extends Partial<DefaultContext> = {}, T extends Partial<DefaultContext> = {},
E extends EmitsOptions = EmitsOptions, E extends EmitsOptions = EmitsOptions,
EE extends string = string, EE extends string = string,
@ -82,7 +79,7 @@ export function useOptions<
): InferContext<T, P, E> ): InferContext<T, P, E>
// implementation // implementation
export function useOptions() { export function defineOptions() {
if (__DEV__) { if (__DEV__) {
warn( warn(
`defineContext() is a compiler-hint helper that is only usable inside ` + `defineContext() is a compiler-hint helper that is only usable inside ` +

View File

@ -43,7 +43,7 @@ export { provide, inject } from './apiInject'
export { nextTick } from './scheduler' export { nextTick } from './scheduler'
export { defineComponent } from './apiDefineComponent' export { defineComponent } from './apiDefineComponent'
export { defineAsyncComponent } from './apiAsyncComponent' export { defineAsyncComponent } from './apiAsyncComponent'
export { useOptions } from './helpers/useOptions' export { defineOptions } from './apiDefineOptions'
// Advanced API ---------------------------------------------------------------- // Advanced API ----------------------------------------------------------------

View File

@ -1,7 +1,7 @@
import { expectType, useOptions, Slots, describe } from './index' import { expectType, defineOptions, Slots, describe } from './index'
describe('no args', () => { describe('no args', () => {
const { props, attrs, emit, slots } = useOptions() const { props, attrs, emit, slots } = defineOptions()
expectType<{}>(props) expectType<{}>(props)
expectType<Record<string, unknown>>(attrs) expectType<Record<string, unknown>>(attrs)
expectType<(...args: any[]) => void>(emit) expectType<(...args: any[]) => void>(emit)
@ -15,7 +15,7 @@ describe('no args', () => {
}) })
describe('with type arg', () => { describe('with type arg', () => {
const { props, attrs, emit, slots } = useOptions<{ const { props, attrs, emit, slots } = defineOptions<{
props: { props: {
foo: string foo: string
} }
@ -40,7 +40,7 @@ describe('with type arg', () => {
// with runtime arg // with runtime arg
describe('with runtime arg (array syntax)', () => { describe('with runtime arg (array syntax)', () => {
const { props, emit } = useOptions({ const { props, emit } = defineOptions({
props: ['foo', 'bar'], props: ['foo', 'bar'],
emits: ['foo', 'bar'] emits: ['foo', 'bar']
}) })
@ -59,7 +59,7 @@ describe('with runtime arg (array syntax)', () => {
}) })
describe('with runtime arg (object syntax)', () => { describe('with runtime arg (object syntax)', () => {
const { props, emit } = useOptions({ const { props, emit } = defineOptions({
props: { props: {
foo: String, foo: String,
bar: { bar: {