Merge branch 'script-setup-2'

This commit is contained in:
Evan You 2020-11-16 15:42:39 -05:00
commit e521de1663
25 changed files with 2373 additions and 1217 deletions

View File

@ -60,12 +60,13 @@ type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode
export interface CodegenResult { export interface CodegenResult {
code: string code: string
preamble: string
ast: RootNode ast: RootNode
map?: RawSourceMap map?: RawSourceMap
} }
export interface CodegenContext export interface CodegenContext
extends Omit<Required<CodegenOptions>, 'bindingMetadata'> { extends Omit<Required<CodegenOptions>, 'bindingMetadata' | 'inline'> {
source: string source: string
code: string code: string
line: number line: number
@ -199,12 +200,18 @@ export function generate(
const hasHelpers = ast.helpers.length > 0 const hasHelpers = ast.helpers.length > 0
const useWithBlock = !prefixIdentifiers && mode !== 'module' const useWithBlock = !prefixIdentifiers && mode !== 'module'
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module' const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
const isSetupInlined = !!options.inline
// preambles // preambles
// in setup() inline mode, the preamble is generated in a sub context
// and returned separately.
const preambleContext = isSetupInlined
? createCodegenContext(ast, options)
: context
if (!__BROWSER__ && mode === 'module') { if (!__BROWSER__ && mode === 'module') {
genModulePreamble(ast, context, genScopeId) genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
} else { } else {
genFunctionPreamble(ast, context) genFunctionPreamble(ast, preambleContext)
} }
// binding optimizations // binding optimizations
@ -213,10 +220,17 @@ export function generate(
: `` : ``
// enter render function // enter render function
if (!ssr) { if (!ssr) {
if (genScopeId) { if (isSetupInlined) {
push(`const render = ${PURE_ANNOTATION}_withId(`) if (genScopeId) {
push(`${PURE_ANNOTATION}_withId(`)
}
push(`(_ctx, _cache${optimizeSources}) => {`)
} else {
if (genScopeId) {
push(`const render = ${PURE_ANNOTATION}_withId(`)
}
push(`function render(_ctx, _cache${optimizeSources}) {`)
} }
push(`function render(_ctx, _cache${optimizeSources}) {`)
} else { } else {
if (genScopeId) { if (genScopeId) {
push(`const ssrRender = ${PURE_ANNOTATION}_withId(`) push(`const ssrRender = ${PURE_ANNOTATION}_withId(`)
@ -290,6 +304,7 @@ export function generate(
return { return {
ast, ast,
code: context.code, code: context.code,
preamble: isSetupInlined ? preambleContext.code : ``,
// SourceMapGenerator does have toJSON() method but it's not in the types // SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined map: context.map ? (context.map as any).toJSON() : undefined
} }
@ -356,7 +371,8 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
function genModulePreamble( function genModulePreamble(
ast: RootNode, ast: RootNode,
context: CodegenContext, context: CodegenContext,
genScopeId: boolean genScopeId: boolean,
inline?: boolean
) { ) {
const { const {
push, push,
@ -423,7 +439,10 @@ function genModulePreamble(
genHoists(ast.hoists, context) genHoists(ast.hoists, context)
newline() newline()
push(`export `)
if (!inline) {
push(`export `)
}
} }
function genAssets( function genAssets(

View File

@ -7,7 +7,8 @@ export {
TransformOptions, TransformOptions,
CodegenOptions, CodegenOptions,
HoistTransform, HoistTransform,
BindingMetadata BindingMetadata,
BindingTypes
} from './options' } from './options'
export { baseParse, TextModes } from './parse' export { baseParse, TextModes } from './parse'
export { export {

View File

@ -61,11 +61,47 @@ export type HoistTransform = (
parent: ParentNode parent: ParentNode
) => void ) => void
export interface BindingMetadata { export const enum BindingTypes {
[key: string]: 'data' | 'props' | 'setup' | 'options' DATA = 'data',
PROPS = 'props',
SETUP = 'setup',
CONST = 'const',
OPTIONS = 'options'
} }
export interface TransformOptions { export interface BindingMetadata {
[key: string]: BindingTypes
}
interface SharedTransformCodegenOptions {
/**
* Transform expressions like {{ foo }} to `_ctx.foo`.
* If this option is false, the generated code will be wrapped in a
* `with (this) { ... }` block.
* - This is force-enabled in module mode, since modules are by default strict
* and cannot use `with`
* @default mode === 'module'
*/
prefixIdentifiers?: boolean
/**
* Generate SSR-optimized render functions instead.
* The resulting function must be attached to the component via the
* `ssrRender` option instead of `render`.
*/
ssr?: boolean
/**
* Optional binding metadata analyzed from script - used to optimize
* binding access when `prefixIdentifiers` is enabled.
*/
bindingMetadata?: BindingMetadata
/**
* Compile the function for inlining inside setup().
* This allows the function to directly access setup() local bindings.
*/
inline?: boolean
}
export interface TransformOptions extends SharedTransformCodegenOptions {
/** /**
* An array of node transforms to be applied to every AST node. * An array of node transforms to be applied to every AST node.
*/ */
@ -128,26 +164,15 @@ export interface TransformOptions {
* SFC scoped styles ID * SFC scoped styles ID
*/ */
scopeId?: string | null scopeId?: string | null
/**
* Generate SSR-optimized render functions instead.
* The resulting function must be attached to the component via the
* `ssrRender` option instead of `render`.
*/
ssr?: boolean
/** /**
* SFC `<style vars>` injection string * SFC `<style vars>` injection string
* needed to render inline CSS variables on component root * needed to render inline CSS variables on component root
*/ */
ssrCssVars?: string ssrCssVars?: string
/**
* Optional binding metadata analyzed from script - used to optimize
* binding access when `prefixIdentifiers` is enabled.
*/
bindingMetadata?: BindingMetadata
onError?: (error: CompilerError) => void onError?: (error: CompilerError) => void
} }
export interface CodegenOptions { export interface CodegenOptions extends SharedTransformCodegenOptions {
/** /**
* - `module` mode will generate ES module import statements for helpers * - `module` mode will generate ES module import statements for helpers
* and export the render function as the default export. * and export the render function as the default export.
@ -189,11 +214,6 @@ export interface CodegenOptions {
* @default 'Vue' * @default 'Vue'
*/ */
runtimeGlobalName?: string runtimeGlobalName?: string
// we need to know this during codegen to generate proper preambles
prefixIdentifiers?: boolean
bindingMetadata?: BindingMetadata
// generate ssr-specific code?
ssr?: boolean
} }
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions

View File

@ -29,6 +29,7 @@ export const PUSH_SCOPE_ID = Symbol(__DEV__ ? `pushScopeId` : ``)
export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``) export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``)
export const WITH_SCOPE_ID = Symbol(__DEV__ ? `withScopeId` : ``) export const WITH_SCOPE_ID = Symbol(__DEV__ ? `withScopeId` : ``)
export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``) export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``)
export const UNREF = Symbol(__DEV__ ? `unref` : ``)
// Name mapping for runtime helpers that need to be imported from 'vue' in // Name mapping for runtime helpers that need to be imported from 'vue' in
// generated code. Make sure these are correctly exported in the runtime! // generated code. Make sure these are correctly exported in the runtime!
@ -62,7 +63,8 @@ export const helperNameMap: any = {
[PUSH_SCOPE_ID]: `pushScopeId`, [PUSH_SCOPE_ID]: `pushScopeId`,
[POP_SCOPE_ID]: `popScopeId`, [POP_SCOPE_ID]: `popScopeId`,
[WITH_SCOPE_ID]: `withScopeId`, [WITH_SCOPE_ID]: `withScopeId`,
[WITH_CTX]: `withCtx` [WITH_CTX]: `withCtx`,
[UNREF]: `unref`
} }
export function registerRuntimeHelpers(helpers: any) { export function registerRuntimeHelpers(helpers: any) {

View File

@ -22,7 +22,8 @@ import {
isArray, isArray,
NOOP, NOOP,
PatchFlags, PatchFlags,
PatchFlagNames PatchFlagNames,
EMPTY_OBJ
} from '@vue/shared' } from '@vue/shared'
import { defaultOnError } from './errors' import { defaultOnError } from './errors'
import { import {
@ -122,7 +123,8 @@ export function createTransformContext(
scopeId = null, scopeId = null,
ssr = false, ssr = false,
ssrCssVars = ``, ssrCssVars = ``,
bindingMetadata = {}, bindingMetadata = EMPTY_OBJ,
inline = false,
onError = defaultOnError onError = defaultOnError
}: TransformOptions }: TransformOptions
): TransformContext { ): TransformContext {
@ -141,6 +143,7 @@ export function createTransformContext(
ssr, ssr,
ssrCssVars, ssrCssVars,
bindingMetadata, bindingMetadata,
inline,
onError, onError,
// state // state

View File

@ -207,11 +207,11 @@ export function getStaticType(
case NodeTypes.TEXT_CALL: case NodeTypes.TEXT_CALL:
return getStaticType(node.content, resultCache) return getStaticType(node.content, resultCache)
case NodeTypes.SIMPLE_EXPRESSION: case NodeTypes.SIMPLE_EXPRESSION:
return node.isConstant return node.isRuntimeConstant
? node.isRuntimeConstant ? StaticType.HAS_RUNTIME_CONSTANT
? StaticType.HAS_RUNTIME_CONSTANT : node.isConstant
: StaticType.FULL_STATIC ? StaticType.FULL_STATIC
: StaticType.NOT_STATIC : StaticType.NOT_STATIC
case NodeTypes.COMPOUND_EXPRESSION: case NodeTypes.COMPOUND_EXPRESSION:
let returnType = StaticType.FULL_STATIC let returnType = StaticType.FULL_STATIC
for (let i = 0; i < node.children.length; i++) { for (let i = 0; i < node.children.length; i++) {

View File

@ -26,7 +26,10 @@ import {
isSymbol, isSymbol,
isOn, isOn,
isObject, isObject,
isReservedProp isReservedProp,
capitalize,
camelize,
EMPTY_OBJ
} from '@vue/shared' } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { import {
@ -37,7 +40,8 @@ import {
TO_HANDLERS, TO_HANDLERS,
TELEPORT, TELEPORT,
KEEP_ALIVE, KEEP_ALIVE,
SUSPENSE SUSPENSE,
UNREF
} from '../runtimeHelpers' } from '../runtimeHelpers'
import { import {
getInnerRange, getInnerRange,
@ -50,6 +54,7 @@ import {
} from '../utils' } from '../utils'
import { buildSlots } from './vSlot' import { buildSlots } from './vSlot'
import { getStaticType } from './hoistStatic' import { getStaticType } from './hoistStatic'
import { BindingTypes } from '../options'
// some directive transforms (e.g. v-model) may return a symbol for runtime // some directive transforms (e.g. v-model) may return a symbol for runtime
// import, which should be used instead of a resolveDirective call. // import, which should be used instead of a resolveDirective call.
@ -246,8 +251,32 @@ export function resolveComponentType(
} }
// 3. user component (from setup bindings) // 3. user component (from setup bindings)
if (context.bindingMetadata[tag] === 'setup') { const bindings = context.bindingMetadata
return `$setup[${JSON.stringify(tag)}]` if (bindings !== EMPTY_OBJ) {
const checkType = (type: BindingTypes) => {
let resolvedTag = tag
if (
bindings[resolvedTag] === type ||
bindings[(resolvedTag = camelize(tag))] === type ||
bindings[(resolvedTag = capitalize(camelize(tag)))] === type
) {
return resolvedTag
}
}
const tagFromSetup = checkType(BindingTypes.SETUP)
if (tagFromSetup) {
return context.inline
? // setup scope bindings may be refs so they need to be unrefed
`${context.helperString(UNREF)}(${tagFromSetup})`
: `$setup[${JSON.stringify(tagFromSetup)}]`
}
const tagFromConst = checkType(BindingTypes.CONST)
if (tagFromConst) {
return context.inline
? // in inline mode, const setup bindings (e.g. imports) can be used as-is
tagFromConst
: `$setup[${JSON.stringify(tagFromConst)}]`
}
} }
// 4. user component (resolve) // 4. user component (resolve)

View File

@ -28,6 +28,8 @@ import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
import { validateBrowserExpression } from '../validateExpression' import { validateBrowserExpression } from '../validateExpression'
import { parse } from '@babel/parser' import { parse } from '@babel/parser'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
import { UNREF } from '../runtimeHelpers'
import { BindingTypes } from '../options'
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this') const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
@ -97,12 +99,31 @@ export function processExpression(
return node return node
} }
const { bindingMetadata } = context const { inline, bindingMetadata } = context
const prefix = (raw: string) => { const prefix = (raw: string) => {
const source = hasOwn(bindingMetadata, raw) const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
? `$` + bindingMetadata[raw] if (inline) {
: `_ctx` // setup inline mode
return `${source}.${raw}` if (type === BindingTypes.CONST) {
return raw
} else if (type === BindingTypes.SETUP) {
return `${context.helperString(UNREF)}(${raw})`
} else if (type === BindingTypes.PROPS) {
// use __props which is generated by compileScript so in ts mode
// it gets correct type
return `__props.${raw}`
}
}
if (type === BindingTypes.CONST) {
// setup const binding in non-inline mode
return `$setup.${raw}`
} else if (type) {
return `$${type}.${raw}`
} else {
// fallback to ctx
return `_ctx.${raw}`
}
} }
// fast path if expression is a simple identifier. // fast path if expression is a simple identifier.
@ -110,6 +131,10 @@ export function processExpression(
// bail on parens to prevent any possible function invocations. // bail on parens to prevent any possible function invocations.
const bailConstant = rawExp.indexOf(`(`) > -1 const bailConstant = rawExp.indexOf(`(`) > -1
if (isSimpleIdentifier(rawExp)) { if (isSimpleIdentifier(rawExp)) {
// const bindings exposed from setup - we know they never change
if (bindingMetadata[node.content] === BindingTypes.CONST) {
node.isRuntimeConstant = true
}
if ( if (
!asParams && !asParams &&
!context.identifiers[rawExp] && !context.identifiers[rawExp] &&
@ -161,9 +186,9 @@ export function processExpression(
if (!isDuplicate(node)) { if (!isDuplicate(node)) {
const needPrefix = shouldPrefix(node, parent) const needPrefix = shouldPrefix(node, parent)
if (!knownIds[node.name] && needPrefix) { if (!knownIds[node.name] && needPrefix) {
if (isPropertyShorthand(node, parent)) { if (isStaticProperty(parent) && parent.shorthand) {
// property shorthand like { foo }, we need to add the key since we // property shorthand like { foo }, we need to add the key since
// rewrite the value // we rewrite the value
node.prefix = `${node.name}: ` node.prefix = `${node.name}: `
} }
node.name = prefix(node.name) node.name = prefix(node.name)
@ -278,46 +303,65 @@ const isStaticProperty = (node: Node): node is ObjectProperty =>
(node.type === 'ObjectProperty' || node.type === 'ObjectMethod') && (node.type === 'ObjectProperty' || node.type === 'ObjectMethod') &&
!node.computed !node.computed
const isPropertyShorthand = (node: Node, parent: Node) => {
return (
isStaticProperty(parent) &&
parent.value === node &&
parent.key.type === 'Identifier' &&
parent.key.name === (node as Identifier).name &&
parent.key.start === node.start
)
}
const isStaticPropertyKey = (node: Node, parent: Node) => const isStaticPropertyKey = (node: Node, parent: Node) =>
isStaticProperty(parent) && parent.key === node isStaticProperty(parent) && parent.key === node
function shouldPrefix(identifier: Identifier, parent: Node) { function shouldPrefix(id: Identifier, parent: Node) {
// declaration id
if ( if (
!( (parent.type === 'VariableDeclarator' ||
isFunction(parent) && parent.type === 'ClassDeclaration') &&
// not id of a FunctionDeclaration parent.id === id
((parent as any).id === identifier ||
// not a params of a function
parent.params.includes(identifier))
) &&
// not a key of Property
!isStaticPropertyKey(identifier, parent) &&
// not a property of a MemberExpression
!(
(parent.type === 'MemberExpression' ||
parent.type === 'OptionalMemberExpression') &&
parent.property === identifier &&
!parent.computed
) &&
// not in an Array destructure pattern
!(parent.type === 'ArrayPattern') &&
// skip whitelisted globals
!isGloballyWhitelisted(identifier.name) &&
// special case for webpack compilation
identifier.name !== `require` &&
// is a special keyword but parsed as identifier
identifier.name !== `arguments`
) { ) {
return true return false
} }
if (isFunction(parent)) {
// function decalration/expression id
if ((parent as any).id === id) {
return false
}
// params list
if (parent.params.includes(id)) {
return false
}
}
// property key
// this also covers object destructure pattern
if (isStaticPropertyKey(id, parent)) {
return false
}
// array destructure pattern
if (parent.type === 'ArrayPattern') {
return false
}
// member expression property
if (
(parent.type === 'MemberExpression' ||
parent.type === 'OptionalMemberExpression') &&
parent.property === id &&
!parent.computed
) {
return false
}
// is a special keyword but parsed as identifier
if (id.name === 'arguments') {
return false
}
// skip whitelisted globals
if (isGloballyWhitelisted(id.name)) {
return false
}
// special case for webpack compilation
if (id.name === 'require') {
return false
}
return true
} }

View File

@ -70,7 +70,7 @@ export const transformOn: DirectiveTransform = (
if (exp && !exp.content.trim()) { if (exp && !exp.content.trim()) {
exp = undefined exp = undefined
} }
let isCacheable: boolean = context.cacheHandlers && !exp let shouldCache: boolean = context.cacheHandlers && !exp
if (exp) { if (exp) {
const isMemberExp = isMemberExpression(exp.content) const isMemberExp = isMemberExpression(exp.content)
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content)) const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
@ -83,8 +83,11 @@ export const transformOn: DirectiveTransform = (
isInlineStatement && context.removeIdentifiers(`$event`) isInlineStatement && context.removeIdentifiers(`$event`)
// with scope analysis, the function is hoistable if it has no reference // with scope analysis, the function is hoistable if it has no reference
// to scope variables. // to scope variables.
isCacheable = shouldCache =
context.cacheHandlers && context.cacheHandlers &&
// runtime constants don't need to be cached
// (this is analyzed by compileScript in SFC <script setup>)
!(exp.type === NodeTypes.SIMPLE_EXPRESSION && exp.isRuntimeConstant) &&
// #1541 bail if this is a member exp handler passed to a component - // #1541 bail if this is a member exp handler passed to a component -
// we need to use the original function to preserve arity, // we need to use the original function to preserve arity,
// e.g. <transition> relies on checking cb.length to determine // e.g. <transition> relies on checking cb.length to determine
@ -98,7 +101,7 @@ export const transformOn: DirectiveTransform = (
// to a function, turn it into invocation (and wrap in an arrow function // to a function, turn it into invocation (and wrap in an arrow function
// below) so that it always accesses the latest value when called - thus // below) so that it always accesses the latest value when called - thus
// avoiding the need to be patched. // avoiding the need to be patched.
if (isCacheable && isMemberExp) { if (shouldCache && isMemberExp) {
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) { if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
exp.content += `(...args)` exp.content += `(...args)`
} else { } else {
@ -116,7 +119,7 @@ export const transformOn: DirectiveTransform = (
) )
} }
if (isInlineStatement || (isCacheable && isMemberExp)) { if (isInlineStatement || (shouldCache && isMemberExp)) {
// wrap inline statement in a function expression // wrap inline statement in a function expression
exp = createCompoundExpression([ exp = createCompoundExpression([
`${isInlineStatement ? `$event` : `(...args)`} => ${ `${isInlineStatement ? `$event` : `(...args)`} => ${
@ -142,7 +145,7 @@ export const transformOn: DirectiveTransform = (
ret = augmentor(ret) ret = augmentor(ret)
} }
if (isCacheable) { if (shouldCache) {
// cache handlers so that it's always the same handler being passed down. // cache handlers so that it's always the same handler being passed down.
// this avoids unnecessary re-renders when users use inline handlers on // this avoids unnecessary re-renders when users use inline handlers on
// components. // components.

View File

@ -1,65 +1,491 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SFC compile <script setup> <script setup lang="ts"> extract emits 1`] = ` exports[`SFC compile <script setup> CSS vars injection <script> w/ default export 1`] = `
"import { defineComponent as __define__ } from 'vue' "const __default__ = { setup() {} }
import { Slots as __Slots__ } from 'vue' import { useCssVars as _useCssVars } from 'vue'
declare function __emit__(e: 'foo' | 'bar'): void const __injectCSSVars__ = () => {
declare function __emit__(e: 'baz', id: number): void _useCssVars(_ctx => ({ color: _ctx.color }))
}
const __setup__ = __default__.setup
__default__.setup = __setup__
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
: __injectCSSVars__
export default __default__"
`;
exports[`SFC compile <script setup> CSS vars injection <script> w/ default export in strings/comments 1`] = `
"
// export default {}
const __default__ = {}
import { useCssVars as _useCssVars } from 'vue'
const __injectCSSVars__ = () => {
_useCssVars(_ctx => ({ color: _ctx.color }))
}
const __setup__ = __default__.setup
__default__.setup = __setup__
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
: __injectCSSVars__
export default __default__"
`;
exports[`SFC compile <script setup> CSS vars injection <script> w/ no default export 1`] = `
"const a = 1
const __default__ = {}
import { useCssVars as _useCssVars } from 'vue'
const __injectCSSVars__ = () => {
_useCssVars(_ctx => ({ color: _ctx.color }))
}
const __setup__ = __default__.setup
__default__.setup = __setup__
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
: __injectCSSVars__
export default __default__"
`;
exports[`SFC compile <script setup> CSS vars injection w/ <script setup> 1`] = `
"import { useCssVars as _useCssVars } from 'vue'
export default {
expose: [],
setup() {
const color = 'red'
_useCssVars(_ctx => ({ color }))
return { color }
}
}"
`;
exports[`SFC compile <script setup> defineOptions() 1`] = `
"export default {
expose: [],
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'
export default {
expose: [],
props: {
foo: {
default: () => bar
}
},
setup() {
return { bar }
}
}"
`;
exports[`SFC compile <script setup> errors should allow defineOptions() referencing scope var 1`] = `
"export default {
expose: [],
props: {
foo: {
default: bar => bar + 1
}
},
setup() {
const bar = 1
return { bar }
}
}"
`;
exports[`SFC compile <script setup> imports dedupe between user & helper 1`] = `
"import { ref as _ref } from 'vue'
import { ref } from 'vue'
export function setup(_: {}, { emit: myEmit }: { export default {
emit: typeof __emit__, expose: [],
slots: __Slots__, setup() {
const foo = _ref(1)
return { foo, ref }
}
}"
`;
exports[`SFC compile <script setup> imports import dedupe between <script> and <script setup> 1`] = `
"import { x } from './x'
export default {
expose: [],
setup() {
x()
return { x }
}
}"
`;
exports[`SFC compile <script setup> imports should extract comment for import or type declarations 1`] = `
"import a from 'a' // comment
import b from 'b'
export default {
expose: [],
setup() {
return { a, b }
}
}"
`;
exports[`SFC compile <script setup> imports should hoist and expose imports 1`] = `
"import { ref } from 'vue'
export default {
expose: [],
setup() {
return { ref }
}
}"
`;
exports[`SFC compile <script setup> inlineTemplate mode avoid unref() when necessary 1`] = `
"import { createVNode as _createVNode, unref as _unref, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
import { ref } from 'vue'
import Foo from './Foo.vue'
import other from './util'
export default {
expose: [],
setup() {
const count = ref(0)
const constant = {}
function fn() {}
return (_ctx, _cache, $props, $setup, $data, $options) => {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(Foo),
_createVNode(\\"div\\", { onClick: fn }, _toDisplayString(_unref(count)) + \\" \\" + _toDisplayString(constant) + \\" \\" + _toDisplayString(_unref(other)), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}
}
}"
`;
exports[`SFC compile <script setup> inlineTemplate mode should work 1`] = `
"import { unref as _unref, toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
const _hoisted_1 = /*#__PURE__*/_createVNode(\\"div\\", null, \\"static\\", -1 /* HOISTED */)
import { ref } from 'vue'
export default {
expose: [],
setup() {
const count = ref(0)
return (_ctx, _cache, $props, $setup, $data, $options) => {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode(\\"div\\", null, _toDisplayString(_unref(count)), 1 /* TEXT */),
_hoisted_1
], 64 /* STABLE_FRAGMENT */))
}
}
}"
`;
exports[`SFC compile <script setup> ref: syntax sugar accessing ref binding 1`] = `
"import { ref as _ref } from 'vue'
export default {
expose: [],
setup() {
const a = _ref(1)
console.log(a.value)
function get() {
return a.value + 1
}
return { a, get }
}
}"
`;
exports[`SFC compile <script setup> ref: syntax sugar array destructure 1`] = `
"import { ref as _ref } from 'vue'
export default {
expose: [],
setup() {
const n = _ref(1), [__a, __b = 1, ...__c] = useFoo()
const a = _ref(__a);
const b = _ref(__b);
const c = _ref(__c);
console.log(n.value, a.value, b.value, c.value)
return { n, a, b, c }
}
}"
`;
exports[`SFC compile <script setup> ref: syntax sugar convert ref declarations 1`] = `
"import { ref as _ref } from 'vue'
export default {
expose: [],
setup() {
const foo = _ref()
const a = _ref(1)
const b = _ref({
count: 0
})
let c = () => {}
let d
return { foo, a, b, c, d }
}
}"
`;
exports[`SFC compile <script setup> ref: syntax sugar multi ref declarations 1`] = `
"import { ref as _ref } from 'vue'
export default {
expose: [],
setup() {
const a = _ref(1), b = _ref(2), c = _ref({
count: 0
})
return { a, b, c }
}
}"
`;
exports[`SFC compile <script setup> ref: syntax sugar mutating ref binding 1`] = `
"import { ref as _ref } from 'vue'
export default {
expose: [],
setup() {
const a = _ref(1)
const b = _ref({ count: 0 })
function inc() {
a.value++
a.value = a.value + 1
b.value.count++
b.value.count = b.value.count + 1
}
return { a, b, inc }
}
}"
`;
exports[`SFC compile <script setup> ref: syntax sugar nested destructure 1`] = `
"import { ref as _ref } from 'vue'
export default {
expose: [],
setup() {
const [{ a: { b: __b }}] = useFoo()
const b = _ref(__b);
const { c: [__d, __e] } = useBar()
const d = _ref(__d);
const e = _ref(__e);
console.log(b.value, d.value, e.value)
return { b, d, e }
}
}"
`;
exports[`SFC compile <script setup> ref: syntax sugar object destructure 1`] = `
"import { ref as _ref } from 'vue'
export default {
expose: [],
setup() {
const n = _ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = useFoo()
const a = _ref(__a);
const c = _ref(__c);
const d = _ref(__d);
const f = _ref(__f);
const g = _ref(__g);
console.log(n.value, a.value, c.value, d.value, f.value, g.value)
return { n, a, c, d, f, g }
}
}"
`;
exports[`SFC compile <script setup> ref: syntax sugar should not convert non ref labels 1`] = `
"export default {
expose: [],
setup() {
foo: a = 1, b = 2, c = {
count: 0
}
return { }
}
}"
`;
exports[`SFC compile <script setup> ref: syntax sugar using ref binding in property shorthand 1`] = `
"import { ref as _ref } from 'vue'
export default {
expose: [],
setup() {
const a = _ref(1)
const b = { a: a.value }
function test() {
const { a } = b
}
return { a, b, test }
}
}"
`;
exports[`SFC compile <script setup> should expose top level declarations 1`] = `
"import { x } from './x'
export default {
expose: [],
setup() {
let a = 1
const b = 2
function c() {}
class d {}
return { a, b, c, d, x }
}
}"
`;
exports[`SFC compile <script setup> with TypeScript defineOptions w/ runtime options 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default _defineComponent({
expose: [],
props: { foo: String },
emits: ['a', 'b'],
setup(__props, { props, emit }) {
return { props, emit }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract emits (union) 1`] = `
"import { Slots as _Slots, defineComponent as _defineComponent } from 'vue'
export default _defineComponent({
expose: [],
emits: [\\"foo\\", \\"bar\\", \\"baz\\"] as unknown as undefined,
setup(__props, { emit }: {
props: {},
emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void),
slots: Slots,
attrs: Record<string, any> attrs: Record<string, any>
}) { }) {
return { }
return { emit }
} }
export default __define__({
emits: [\\"foo\\", \\"bar\\", \\"baz\\"] as unknown as undefined,
setup
})" })"
`; `;
exports[`SFC compile <script setup> <script setup lang="ts"> extract props 1`] = ` exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract emits 1`] = `
"import { defineComponent as __define__ } from 'vue' "import { Slots as _Slots, defineComponent as _defineComponent } from 'vue'
import { Slots as __Slots__ } from 'vue'
interface Test {}
export default _defineComponent({
expose: [],
emits: [\\"foo\\", \\"bar\\"] as unknown as undefined,
setup(__props, { emit }: {
props: {},
emit: (e: 'foo' | 'bar') => void,
slots: Slots,
attrs: Record<string, any>
}) {
return { emit }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract props 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
interface Test {}
type Alias = number[] type Alias = number[]
export function setup(myProps: { export default _defineComponent({
string: string expose: [],
number: number
boolean: boolean
object: object
objectLiteral: { a: number }
fn: (n: number) => void
functionRef: Function
objectRef: Object
array: string[]
arrayRef: Array<any>
tuple: [number, number]
set: Set<string>
literal: 'foo'
optional?: any
recordRef: Record<string, null>
interface: Test
alias: Alias
union: string | number
literalUnion: 'foo' | 'bar'
literalUnionMixed: 'foo' | 1 | boolean
intersection: Test & {}
}) {
return { }
}
export default __define__({
props: { props: {
string: { type: String, required: true }, string: { type: String, required: true },
number: { type: Number, required: true }, number: { type: Number, required: true },
@ -83,321 +509,28 @@ export default __define__({
literalUnionMixed: { type: [String, Number, Boolean], required: true }, literalUnionMixed: { type: [String, Number, Boolean], required: true },
intersection: { type: Object, required: true } intersection: { type: Object, required: true }
} as unknown as undefined, } as unknown as undefined,
setup setup() {
return { }
}
})" })"
`; `;
exports[`SFC compile <script setup> <script setup lang="ts"> hoist type declarations 1`] = ` exports[`SFC compile <script setup> with TypeScript hoist type declarations 1`] = `
"import { defineComponent as __define__ } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { Slots as __Slots__ } from 'vue'
export interface Foo {} export interface Foo {}
type Bar = {} type Bar = {}
export function setup() {
const a = 1
return { a } export default _defineComponent({
expose: [],
setup() {
return { }
} }
export default __define__({
setup
})" })"
`; `;
exports[`SFC compile <script setup> CSS vars injection <script> w/ default export 1`] = `
"const __default__ = { setup() {} }
import { useCssVars as __useCssVars__ } from 'vue'
const __injectCSSVars__ = () => {
__useCssVars__(_ctx => ({ color: _ctx.color }))
}
const __setup__ = __default__.setup
__default__.setup = __setup__
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
: __injectCSSVars__
export default __default__"
`;
exports[`SFC compile <script setup> CSS vars injection <script> w/ default export in strings/comments 1`] = `
"
// export default {}
const __default__ = {}
import { useCssVars as __useCssVars__ } from 'vue'
const __injectCSSVars__ = () => {
__useCssVars__(_ctx => ({ color: _ctx.color }))
}
const __setup__ = __default__.setup
__default__.setup = __setup__
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
: __injectCSSVars__
export default __default__"
`;
exports[`SFC compile <script setup> CSS vars injection <script> w/ no default export 1`] = `
"const a = 1
const __default__ = {}
import { useCssVars as __useCssVars__ } from 'vue'
const __injectCSSVars__ = () => {
__useCssVars__(_ctx => ({ color: _ctx.color }))
}
const __setup__ = __default__.setup
__default__.setup = __setup__
? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }
: __injectCSSVars__
export default __default__"
`;
exports[`SFC compile <script setup> CSS vars injection w/ <script setup> 1`] = `
"import { useCssVars as __useCssVars__ } from 'vue'
export function setup() {
const color = 'red'
__useCssVars__(_ctx => ({ color }))
return { color }
}
export default { setup }"
`;
exports[`SFC compile <script setup> errors should allow export default referencing imported binding 1`] = `
"import { bar } from './bar'
export function setup() {
return { bar }
}
const __default__ = {
props: {
foo: {
default: () => bar
}
}
}
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> errors should allow export default referencing re-exported binding 1`] = `
"import { bar } from './bar'
export function setup() {
return { bar }
}
const __default__ = {
props: {
foo: {
default: () => bar
}
}
}
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> errors should allow export default referencing scope var 1`] = `
"export function setup() {
const bar = 1
return { }
}
const __default__ = {
props: {
foo: {
default: bar => bar + 1
}
}
}
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> explicit setup signature 1`] = `
"export function setup(props, { emit }) {
emit('foo')
return { }
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export * from './x' 1`] = `
"import { toRefs as __toRefs__ } from 'vue'
import * as __export_all_0__ from './x'
export function setup() {
const y = 1
return Object.assign(
{ y },
__toRefs__(__export_all_0__)
)
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export { x } 1`] = `
"export function setup() {
const x = 1
const y = 2
return { x, y }
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export { x } from './x' 1`] = `
"import { x, y } from './x'
export function setup() {
return { x, y }
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export { x as default } 1`] = `
"import x from './x'
export function setup() {
const y = 1
return { y }
}
const __default__ = x
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> exports export { x as default } from './x' 1`] = `
"import { x as __default__ } from './x'
import { y } from './x'
export function setup() {
return { y }
}
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> exports export class X() {} 1`] = `
"export function setup() {
class X {}
return { X }
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export const { x } = ... (destructuring) 1`] = `
"export function setup() {
const [a = 1, { b } = { b: 123 }, ...c] = useFoo()
const { d = 2, _: [e], ...f } = useBar()
return { a, b, c, d, e, f }
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export const x = ... 1`] = `
"export function setup() {
const x = 1
return { x }
}
export default { setup }"
`;
exports[`SFC compile <script setup> exports export default from './x' 1`] = `
"import __default__ from './x'
export function setup() {
return { }
}
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> exports export default in <script setup> 1`] = `
"export function setup() {
const y = 1
return { y }
}
const __default__ = {
props: ['foo']
}
__default__.setup = setup
export default __default__"
`;
exports[`SFC compile <script setup> exports export function x() {} 1`] = `
"export function setup() {
function x(){}
return { x }
}
export default { setup }"
`;
exports[`SFC compile <script setup> import dedupe between <script> and <script setup> 1`] = `
"import { x } from './x'
export function setup() {
x()
return { }
}
export default { setup }"
`;
exports[`SFC compile <script setup> should extract comment for import or type declarations 1`] = `
"import a from 'a' // comment
import b from 'b'
export function setup() {
return { }
}
export default { setup }"
`;
exports[`SFC compile <script setup> should hoist imports 1`] = `
"import { ref } from 'vue'
export function setup() {
return { }
}
export default { setup }"
`;

View File

@ -22,232 +22,220 @@ function assertCode(code: string) {
} }
describe('SFC compile <script setup>', () => { describe('SFC compile <script setup>', () => {
test('should hoist imports', () => { test('should expose top level declarations', () => {
assertCode(
compile(`<script setup>import { ref } from 'vue'</script>`).content
)
})
test('should extract comment for import or type declarations', () => {
assertCode(
compile(`<script setup>
import a from 'a' // comment
import b from 'b'
</script>`).content
)
})
test('explicit setup signature', () => {
assertCode(
compile(`<script setup="props, { emit }">emit('foo')</script>`).content
)
})
test('import dedupe between <script> and <script setup>', () => {
const { content } = compile(` const { content } = compile(`
<script>
import { x } from './x'
</script>
<script setup> <script setup>
import { x } from './x' import { x } from './x'
x() let a = 1
const b = 2
function c() {}
class d {}
</script> </script>
`) `)
assertCode(content) assertCode(content)
expect(content.indexOf(`import { x }`)).toEqual( expect(content).toMatch('return { a, b, c, d, x }')
content.lastIndexOf(`import { x }`)
)
}) })
describe('exports', () => { test('defineOptions()', () => {
test('export const x = ...', () => { const { content, bindings } = compile(`
const { content, bindings } = compile( <script setup>
`<script setup>export const x = 1</script>` import { defineOptions } from 'vue'
) const { props, emit } = defineOptions({
assertCode(content) props: {
expect(bindings).toStrictEqual({ foo: String
x: 'setup' },
}) emit: ['a', 'b']
})
const bar = 1
</script>
`)
// should generate working code
assertCode(content)
// should anayze bindings
expect(bindings).toStrictEqual({
foo: 'props',
bar: 'const',
props: 'const',
emit: 'const'
}) })
test('export const { x } = ... (destructuring)', () => { // should remove defineOptions import and call
const { content, bindings } = compile(`<script setup> expect(content).not.toMatch('defineOptions')
export const [a = 1, { b } = { b: 123 }, ...c] = useFoo() // should generate correct setup signature
export const { d = 2, _: [e], ...f } = useBar() expect(content).toMatch(`setup(__props, { props, emit }) {`)
</script>`) // should include context options in default export
assertCode(content) expect(content).toMatch(`export default {
expect(bindings).toStrictEqual({ expose: [],
a: 'setup', props: {
b: 'setup', foo: String
c: 'setup', },
d: 'setup', emit: ['a', 'b'],`)
e: 'setup', })
f: 'setup'
}) describe('imports', () => {
test('should hoist and expose imports', () => {
assertCode(
compile(`<script setup>import { ref } from 'vue'</script>`).content
)
}) })
test('export function x() {}', () => { test('should extract comment for import or type declarations', () => {
const { content, bindings } = compile( assertCode(
`<script setup>export function x(){}</script>` compile(`
<script setup>
import a from 'a' // comment
import b from 'b'
</script>
`).content
) )
assertCode(content)
expect(bindings).toStrictEqual({
x: 'setup'
})
}) })
test('export class X() {}', () => { test('dedupe between user & helper', () => {
const { content, bindings } = compile( const { content } = compile(`
`<script setup>export class X {}</script>` <script setup>
) import { ref } from 'vue'
ref: foo = 1
</script>
`)
assertCode(content) assertCode(content)
expect(bindings).toStrictEqual({ expect(content).toMatch(`import { ref } from 'vue'`)
X: 'setup'
})
}) })
test('export { x }', () => { test('import dedupe between <script> and <script setup>', () => {
const { content, bindings } = compile( const { content } = compile(`
`<script setup> <script>
const x = 1 import { x } from './x'
const y = 2 </script>
export { x, y } <script setup>
</script>` import { x } from './x'
) x()
</script>
`)
assertCode(content) assertCode(content)
expect(bindings).toStrictEqual({ expect(content.indexOf(`import { x }`)).toEqual(
x: 'setup', content.lastIndexOf(`import { x }`)
y: 'setup'
})
})
test(`export { x } from './x'`, () => {
const { content, bindings } = compile(
`<script setup>
export { x, y } from './x'
</script>`
) )
assertCode(content)
expect(bindings).toStrictEqual({
x: 'setup',
y: 'setup'
})
})
test(`export default from './x'`, () => {
const { content, bindings } = compile(
`<script setup>
export default from './x'
</script>`,
{
babelParserPlugins: ['exportDefaultFrom']
}
)
assertCode(content)
expect(bindings).toStrictEqual({})
})
test(`export { x as default }`, () => {
const { content, bindings } = compile(
`<script setup>
import x from './x'
const y = 1
export { x as default, y }
</script>`
)
assertCode(content)
expect(bindings).toStrictEqual({
y: 'setup'
})
})
test(`export { x as default } from './x'`, () => {
const { content, bindings } = compile(
`<script setup>
export { x as default, y } from './x'
</script>`
)
assertCode(content)
expect(bindings).toStrictEqual({
y: 'setup'
})
})
test(`export * from './x'`, () => {
const { content, bindings } = compile(
`<script setup>
export * from './x'
export const y = 1
</script>`
)
assertCode(content)
expect(bindings).toStrictEqual({
y: 'setup'
// in this case we cannot extract bindings from ./x so it falls back
// to runtime proxy dispatching
})
})
test('export default in <script setup>', () => {
const { content, bindings } = compile(
`<script setup>
export default {
props: ['foo']
}
export const y = 1
</script>`
)
assertCode(content)
expect(bindings).toStrictEqual({
foo: 'props',
y: 'setup'
})
}) })
}) })
describe('<script setup lang="ts">', () => { describe('inlineTemplate mode', () => {
test('should work', () => {
const { content } = compile(
`
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<div>{{ count }}</div>
<div>static</div>
</template>
`,
{ inlineTemplate: true }
)
// check snapshot and make sure helper imports and
// hoists are placed correctly.
assertCode(content)
})
test('avoid unref() when necessary', () => {
// function, const, component import
const { content } = compile(
`
<script setup>
import { ref, defineOptions } from 'vue'
import Foo from './Foo.vue'
import other from './util'
const count = ref(0)
const constant = {}
function fn() {}
</script>
<template>
<Foo/>
<div @click="fn">{{ count }} {{ constant }} {{ other }}</div>
</template>
`,
{ inlineTemplate: true }
)
assertCode(content)
// no need to unref vue component import
expect(content).toMatch(`createVNode(Foo)`)
// should unref other imports
expect(content).toMatch(`unref(other)`)
// no need to unref constant literals
expect(content).not.toMatch(`unref(constant)`)
// should unref const w/ call init (e.g. ref())
expect(content).toMatch(`unref(count)`)
// no need to unref function declarations
expect(content).toMatch(`{ onClick: fn }`)
// no need to mark constant fns in patch flag
expect(content).not.toMatch(`PROPS`)
})
})
describe('with TypeScript', () => {
test('hoist type declarations', () => { test('hoist type declarations', () => {
const { content, bindings } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
export interface Foo {} export interface Foo {}
type Bar = {} type Bar = {}
export const a = 1
</script>`) </script>`)
assertCode(content) assertCode(content)
expect(bindings).toStrictEqual({ a: 'setup' })
}) })
test('extract props', () => { test('defineOptions w/ runtime options', () => {
const { content } = compile(` const { content } = compile(`
<script setup="myProps" lang="ts"> <script setup lang="ts">
import { defineOptions } from 'vue'
const { props, emit } = defineOptions({
props: { foo: String },
emits: ['a', 'b']
})
</script>
`)
assertCode(content)
expect(content).toMatch(`export default _defineComponent({
expose: [],
props: { foo: String },
emits: ['a', 'b'],
setup(__props, { props, emit }) {`)
})
test('defineOptions w/ type / extract props', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
import { defineOptions } from 'vue'
interface Test {} interface Test {}
type Alias = number[] type Alias = number[]
declare const myProps: { defineOptions<{
string: string props: {
number: number string: string
boolean: boolean number: number
object: object boolean: boolean
objectLiteral: { a: number } object: object
fn: (n: number) => void objectLiteral: { a: number }
functionRef: Function fn: (n: number) => void
objectRef: Object functionRef: Function
array: string[] objectRef: Object
arrayRef: Array<any> array: string[]
tuple: [number, number] arrayRef: Array<any>
set: Set<string> tuple: [number, number]
literal: 'foo' set: Set<string>
optional?: any literal: 'foo'
recordRef: Record<string, null> optional?: any
interface: Test recordRef: Record<string, null>
alias: Alias interface: Test
alias: Alias
union: string | number union: string | number
literalUnion: 'foo' | 'bar' literalUnion: 'foo' | 'bar'
literalUnionMixed: 'foo' | 1 | boolean literalUnionMixed: 'foo' | 1 | boolean
intersection: Test & {} intersection: Test & {}
} }
}>()
</script>`) </script>`)
assertCode(content) assertCode(content)
expect(content).toMatch(`string: { type: String, required: true }`) expect(content).toMatch(`string: { type: String, required: true }`)
@ -277,21 +265,57 @@ import b from 'b'
`literalUnionMixed: { type: [String, Number, Boolean], required: true }` `literalUnionMixed: { type: [String, Number, Boolean], required: true }`
) )
expect(content).toMatch(`intersection: { type: Object, required: true }`) expect(content).toMatch(`intersection: { type: Object, required: true }`)
expect(bindings).toStrictEqual({
string: 'props',
number: 'props',
boolean: 'props',
object: 'props',
objectLiteral: 'props',
fn: 'props',
functionRef: 'props',
objectRef: 'props',
array: 'props',
arrayRef: 'props',
tuple: 'props',
set: 'props',
literal: 'props',
optional: 'props',
recordRef: 'props',
interface: 'props',
alias: 'props',
union: 'props',
literalUnion: 'props',
literalUnionMixed: 'props',
intersection: 'props'
})
}) })
test('extract emits', () => { test('defineOptions w/ type / extract emits', () => {
const { content } = compile(` const { content } = compile(`
<script setup="_, { emit: myEmit }" lang="ts"> <script setup lang="ts">
declare function myEmit(e: 'foo' | 'bar'): void import { defineOptions } from 'vue'
declare function myEmit(e: 'baz', id: number): void const { emit } = defineOptions<{
emit: (e: 'foo' | 'bar') => void
}>()
</script>
`)
assertCode(content)
expect(content).toMatch(`props: {},\n emit: (e: 'foo' | 'bar') => void,`)
expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
})
test('defineOptions w/ type / extract emits (union)', () => {
const { content } = compile(`
<script setup lang="ts">
import { defineOptions } from 'vue'
const { emit } = defineOptions<{
emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)
}>()
</script> </script>
`) `)
assertCode(content) assertCode(content)
expect(content).toMatch( expect(content).toMatch(
`declare function __emit__(e: 'foo' | 'bar'): void` `props: {},\n emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void),`
)
expect(content).toMatch(
`declare function __emit__(e: 'baz', id: number): void`
) )
expect(content).toMatch( expect(content).toMatch(
`emits: ["foo", "bar", "baz"] as unknown as undefined` `emits: ["foo", "bar", "baz"] as unknown as undefined`
@ -333,7 +357,7 @@ import b from 'b'
test('w/ <script setup>', () => { test('w/ <script setup>', () => {
assertCode( assertCode(
compile( compile(
`<script setup>export const color = 'red'</script>\n` + `<script setup>const color = 'red'</script>\n` +
`<style vars="{ color }">div{ color: var(--color); }</style>` `<style vars="{ color }">div{ color: var(--color); }</style>`
).content ).content
) )
@ -343,9 +367,7 @@ import b from 'b'
describe('async/await detection', () => { describe('async/await detection', () => {
function assertAwaitDetection(code: string, shouldAsync = true) { function assertAwaitDetection(code: string, shouldAsync = true) {
const { content } = compile(`<script setup>${code}</script>`) const { content } = compile(`<script setup>${code}</script>`)
expect(content).toMatch( expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup()`)
`export ${shouldAsync ? `async ` : ``}function setup`
)
} }
test('expression statement', () => { test('expression statement', () => {
@ -356,8 +378,8 @@ import b from 'b'
assertAwaitDetection(`const a = 1 + (await foo)`) assertAwaitDetection(`const a = 1 + (await foo)`)
}) })
test('export', () => { test('ref', () => {
assertAwaitDetection(`export const a = 1 + (await foo)`) assertAwaitDetection(`ref: a = 1 + (await foo)`)
}) })
test('nested statements', () => { test('nested statements', () => {
@ -366,7 +388,7 @@ import b from 'b'
test('should ignore await inside functions', () => { test('should ignore await inside functions', () => {
// function declaration // function declaration
assertAwaitDetection(`export async function foo() { await bar }`, false) assertAwaitDetection(`async function foo() { await bar }`, false)
// function expression // function expression
assertAwaitDetection(`const foo = async () => { await bar }`, false) assertAwaitDetection(`const foo = async () => { await bar }`, false)
// object method // object method
@ -379,6 +401,202 @@ import b from 'b'
}) })
}) })
describe('ref: syntax sugar', () => {
test('convert ref declarations', () => {
const { content, bindings } = compile(`<script setup>
ref: foo
ref: a = 1
ref: b = {
count: 0
}
let c = () => {}
let d
</script>`)
expect(content).toMatch(`import { ref as _ref } from 'vue'`)
expect(content).not.toMatch(`ref: foo`)
expect(content).not.toMatch(`ref: a`)
expect(content).not.toMatch(`ref: b`)
expect(content).toMatch(`const foo = _ref()`)
expect(content).toMatch(`const a = _ref(1)`)
expect(content).toMatch(`
const b = _ref({
count: 0
})
`)
// normal declarations left untouched
expect(content).toMatch(`let c = () => {}`)
expect(content).toMatch(`let d`)
assertCode(content)
expect(bindings).toStrictEqual({
foo: 'setup',
a: 'setup',
b: 'setup',
c: 'setup',
d: 'setup'
})
})
test('multi ref declarations', () => {
const { content, bindings } = compile(`<script setup>
ref: a = 1, b = 2, c = {
count: 0
}
</script>`)
expect(content).toMatch(`
const a = _ref(1), b = _ref(2), c = _ref({
count: 0
})
`)
expect(content).toMatch(`return { a, b, c }`)
assertCode(content)
expect(bindings).toStrictEqual({
a: 'setup',
b: 'setup',
c: 'setup'
})
})
test('should not convert non ref labels', () => {
const { content } = compile(`<script setup>
foo: a = 1, b = 2, c = {
count: 0
}
</script>`)
expect(content).toMatch(`foo: a = 1, b = 2`)
assertCode(content)
})
test('accessing ref binding', () => {
const { content } = compile(`<script setup>
ref: a = 1
console.log(a)
function get() {
return a + 1
}
</script>`)
expect(content).toMatch(`console.log(a.value)`)
expect(content).toMatch(`return a.value + 1`)
assertCode(content)
})
test('cases that should not append .value', () => {
const { content } = compile(`<script setup>
ref: a = 1
console.log(b.a)
function get(a) {
return a + 1
}
</script>`)
expect(content).not.toMatch(`a.value`)
})
test('mutating ref binding', () => {
const { content } = compile(`<script setup>
ref: a = 1
ref: b = { count: 0 }
function inc() {
a++
a = a + 1
b.count++
b.count = b.count + 1
}
</script>`)
expect(content).toMatch(`a.value++`)
expect(content).toMatch(`a.value = a.value + 1`)
expect(content).toMatch(`b.value.count++`)
expect(content).toMatch(`b.value.count = b.value.count + 1`)
assertCode(content)
})
test('using ref binding in property shorthand', () => {
const { content } = compile(`<script setup>
ref: a = 1
const b = { a }
function test() {
const { a } = b
}
</script>`)
expect(content).toMatch(`const b = { a: a.value }`)
// should not convert destructure
expect(content).toMatch(`const { a } = b`)
assertCode(content)
})
test('object destructure', () => {
const { content, bindings } = compile(`<script setup>
ref: n = 1, ({ a, b: c, d = 1, e: f = 2, ...g } = useFoo())
console.log(n, a, c, d, f, g)
</script>`)
expect(content).toMatch(
`const n = _ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = useFoo()`
)
expect(content).toMatch(`\nconst a = _ref(__a);`)
expect(content).not.toMatch(`\nconst b = _ref(__b);`)
expect(content).toMatch(`\nconst c = _ref(__c);`)
expect(content).toMatch(`\nconst d = _ref(__d);`)
expect(content).not.toMatch(`\nconst e = _ref(__e);`)
expect(content).toMatch(`\nconst f = _ref(__f);`)
expect(content).toMatch(`\nconst g = _ref(__g);`)
expect(content).toMatch(
`console.log(n.value, a.value, c.value, d.value, f.value, g.value)`
)
expect(content).toMatch(`return { n, a, c, d, f, g }`)
expect(bindings).toStrictEqual({
n: 'setup',
a: 'setup',
c: 'setup',
d: 'setup',
f: 'setup',
g: 'setup'
})
assertCode(content)
})
test('array destructure', () => {
const { content, bindings } = compile(`<script setup>
ref: n = 1, [a, b = 1, ...c] = useFoo()
console.log(n, a, b, c)
</script>`)
expect(content).toMatch(
`const n = _ref(1), [__a, __b = 1, ...__c] = useFoo()`
)
expect(content).toMatch(`\nconst a = _ref(__a);`)
expect(content).toMatch(`\nconst b = _ref(__b);`)
expect(content).toMatch(`\nconst c = _ref(__c);`)
expect(content).toMatch(`console.log(n.value, a.value, b.value, c.value)`)
expect(content).toMatch(`return { n, a, b, c }`)
expect(bindings).toStrictEqual({
n: 'setup',
a: 'setup',
b: 'setup',
c: 'setup'
})
assertCode(content)
})
test('nested destructure', () => {
const { content, bindings } = compile(`<script setup>
ref: [{ a: { b }}] = useFoo()
ref: ({ c: [d, e] } = useBar())
console.log(b, d, e)
</script>`)
expect(content).toMatch(`const [{ a: { b: __b }}] = useFoo()`)
expect(content).toMatch(`const { c: [__d, __e] } = useBar()`)
expect(content).not.toMatch(`\nconst a = _ref(__a);`)
expect(content).not.toMatch(`\nconst c = _ref(__c);`)
expect(content).toMatch(`\nconst b = _ref(__b);`)
expect(content).toMatch(`\nconst d = _ref(__d);`)
expect(content).toMatch(`\nconst e = _ref(__e);`)
expect(content).toMatch(`return { b, d, e }`)
expect(bindings).toStrictEqual({
b: 'setup',
d: 'setup',
e: 'setup'
})
assertCode(content)
})
})
describe('errors', () => { describe('errors', () => {
test('<script> and <script setup> must have same lang', () => { test('<script> and <script setup> must have same lang', () => {
expect(() => expect(() =>
@ -386,145 +604,105 @@ import b from 'b'
).toThrow(`<script> and <script setup> must have the same language type`) ).toThrow(`<script> and <script setup> must have the same language type`)
}) })
test('export local as default', () => { const moduleErrorMsg = `cannot contain ES module exports`
test('non-type named exports', () => {
expect(() =>
compile(`<script setup>
export const a = 1
</script>`)
).toThrow(moduleErrorMsg)
expect(() =>
compile(`<script setup>
export * from './foo'
</script>`)
).toThrow(moduleErrorMsg)
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
const bar = 1 const bar = 1
export { bar as default } export { bar as default }
</script>`) </script>`)
).toThrow(`Cannot export locally defined variable as default`) ).toThrow(moduleErrorMsg)
}) })
test('export default referencing local var', () => { test('ref: non-assignment expressions', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
const bar = 1 ref: a = 1, foo()
export default { </script>`)
props: { ).toThrow(`ref: statements can only contain assignment expressions`)
foo: { })
default: () => bar
} test('defineOptions() w/ both type and non-type args', () => {
expect(() => {
compile(`<script setup lang="ts">
import { defineOptions } from 'vue'
defineOptions<{}>({})
</script>`)
}).toThrow(`cannot accept both type and non-type arguments`)
})
test('defineOptions() referencing local var', () => {
expect(() =>
compile(`<script setup>
import { defineOptions } from 'vue'
const bar = 1
defineOptions({
props: {
foo: {
default: () => bar
} }
} }
})
</script>`) </script>`)
).toThrow(`cannot reference locally declared variables`) ).toThrow(`cannot reference locally declared variables`)
}) })
test('export default referencing exports', () => { test('defineOptions() referencing ref declarations', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
export const bar = 1 import { defineOptions } from 'vue'
export default { ref: bar = 1
props: bar defineOptions({
} props: { bar }
})
</script>`) </script>`)
).toThrow(`cannot reference locally declared variables`) ).toThrow(`cannot reference locally declared variables`)
}) })
test('should allow export default referencing scope var', () => { test('should allow defineOptions() referencing scope var', () => {
assertCode( assertCode(
compile(`<script setup> compile(`<script setup>
import { defineOptions } from 'vue'
const bar = 1 const bar = 1
export default { defineOptions({
props: { props: {
foo: { foo: {
default: bar => bar + 1 default: bar => bar + 1
} }
} }
} })
</script>`).content </script>`).content
) )
}) })
test('should allow export default referencing imported binding', () => { test('should allow defineOptions() referencing imported binding', () => {
assertCode( assertCode(
compile(`<script setup> compile(`<script setup>
import { defineOptions } from 'vue'
import { bar } from './bar' import { bar } from './bar'
export { bar } defineOptions({
export default {
props: { props: {
foo: { foo: {
default: () => bar default: () => bar
} }
} }
} })
</script>`).content </script>`).content
) )
}) })
test('should allow export default referencing re-exported binding', () => {
assertCode(
compile(`<script setup>
export { bar } from './bar'
export default {
props: {
foo: {
default: () => bar
}
}
}
</script>`).content
)
})
test('error on duplicated default export', () => {
expect(() =>
compile(`
<script>
export default {}
</script>
<script setup>
export default {}
</script>
`)
).toThrow(`Default export is already declared`)
expect(() =>
compile(`
<script>
export default {}
</script>
<script setup>
const x = {}
export { x as default }
</script>
`)
).toThrow(`Default export is already declared`)
expect(() =>
compile(`
<script>
export default {}
</script>
<script setup>
export { x as default } from './y'
</script>
`)
).toThrow(`Default export is already declared`)
expect(() =>
compile(`
<script>
export { x as default } from './y'
</script>
<script setup>
export default {}
</script>
`)
).toThrow(`Default export is already declared`)
expect(() =>
compile(`
<script>
const x = {}
export { x as default }
</script>
<script setup>
export default {}
</script>
`)
).toThrow(`Default export is already declared`)
})
}) })
}) })
@ -725,11 +903,12 @@ 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>
export default { import { defineOptions } from 'vue'
props: { defineOptions({
foo: String, props: {
}, foo: String,
} }
})
</script> </script>
`) `)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({

File diff suppressed because it is too large Load Diff

View File

@ -30,6 +30,7 @@ export interface TemplateCompiler {
export interface SFCTemplateCompileResults { export interface SFCTemplateCompileResults {
code: string code: string
preamble?: string
source: string source: string
tips: string[] tips: string[]
errors: (string | CompilerError)[] errors: (string | CompilerError)[]
@ -168,7 +169,7 @@ function doCompileTemplate({
nodeTransforms = [transformAssetUrl, transformSrcset] nodeTransforms = [transformAssetUrl, transformSrcset]
} }
let { code, map } = compiler.compile(source, { let { code, preamble, map } = compiler.compile(source, {
mode: 'module', mode: 'module',
prefixIdentifiers: true, prefixIdentifiers: true,
hoistStatic: true, hoistStatic: true,
@ -192,7 +193,7 @@ function doCompileTemplate({
} }
} }
return { code, source, errors, tips: [], map } return { code, preamble, source, errors, tips: [], map }
} }
function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap { function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {

View File

@ -10,10 +10,12 @@ import { SFCDescriptor } from './parse'
import { rewriteDefault } from './rewriteDefault' import { rewriteDefault } from './rewriteDefault'
import { ParserPlugin } from '@babel/parser' import { ParserPlugin } from '@babel/parser'
export const CSS_VARS_HELPER = `useCssVars`
export function genCssVarsCode( export function genCssVarsCode(
varsExp: string, varsExp: string,
scoped: boolean, scoped: boolean,
knownBindings?: Record<string, boolean> knownBindings?: Record<string, any>
) { ) {
const exp = createSimpleExpression(varsExp, false) const exp = createSimpleExpression(varsExp, false)
const context = createTransformContext(createRoot([]), { const context = createTransformContext(createRoot([]), {
@ -38,7 +40,7 @@ export function genCssVarsCode(
}) })
.join('') .join('')
return `__useCssVars__(_ctx => (${transformedString})${ return `_${CSS_VARS_HELPER}(_ctx => (${transformedString})${
scoped ? `, true` : `` scoped ? `, true` : ``
})` })`
} }
@ -65,7 +67,7 @@ export function injectCssVarsCalls(
return ( return (
script + script +
`\nimport { useCssVars as __useCssVars__ } from 'vue'\n` + `\nimport { ${CSS_VARS_HELPER} as _${CSS_VARS_HELPER} } from 'vue'\n` +
`const __injectCSSVars__ = () => {\n${calls}}\n` + `const __injectCSSVars__ = () => {\n${calls}}\n` +
`const __setup__ = __default__.setup\n` + `const __setup__ = __default__.setup\n` +
`__default__.setup = __setup__\n` + `__default__.setup = __setup__\n` +

View File

@ -0,0 +1,144 @@
import { nodeOps, render } from '@vue/runtime-test'
import { defineComponent, h, ref } from '../src'
describe('api: expose', () => {
test('via setup context', () => {
const Child = defineComponent({
render() {},
setup(_, { expose }) {
expose({
foo: ref(1),
bar: ref(2)
})
return {
bar: ref(3),
baz: ref(4)
}
}
})
const childRef = ref()
const Parent = {
setup() {
return () => h(Child, { ref: childRef })
}
}
const root = nodeOps.createElement('div')
render(h(Parent), root)
expect(childRef.value).toBeTruthy()
expect(childRef.value.foo).toBe(1)
expect(childRef.value.bar).toBe(2)
expect(childRef.value.baz).toBeUndefined()
})
test('via options', () => {
const Child = defineComponent({
render() {},
data() {
return {
foo: 1
}
},
setup() {
return {
bar: ref(2),
baz: ref(3)
}
},
expose: ['foo', 'bar']
})
const childRef = ref()
const Parent = {
setup() {
return () => h(Child, { ref: childRef })
}
}
const root = nodeOps.createElement('div')
render(h(Parent), root)
expect(childRef.value).toBeTruthy()
expect(childRef.value.foo).toBe(1)
expect(childRef.value.bar).toBe(2)
expect(childRef.value.baz).toBeUndefined()
})
test('options + context', () => {
const Child = defineComponent({
render() {},
expose: ['foo'],
data() {
return {
foo: 1
}
},
setup(_, { expose }) {
expose({
bar: ref(2)
})
return {
bar: ref(3),
baz: ref(4)
}
}
})
const childRef = ref()
const Parent = {
setup() {
return () => h(Child, { ref: childRef })
}
}
const root = nodeOps.createElement('div')
render(h(Parent), root)
expect(childRef.value).toBeTruthy()
expect(childRef.value.foo).toBe(1)
expect(childRef.value.bar).toBe(2)
expect(childRef.value.baz).toBeUndefined()
})
test('options: empty', () => {
const Child = defineComponent({
render() {},
expose: [],
data() {
return {
foo: 1
}
}
})
const childRef = ref()
const Parent = {
setup() {
return () => h(Child, { ref: childRef })
}
}
const root = nodeOps.createElement('div')
render(h(Parent), root)
expect(childRef.value).toBeTruthy()
expect('foo' in childRef.value).toBe(false)
})
test('options: empty + setup context', () => {
const Child = defineComponent({
render() {},
expose: [],
setup(_, { expose }) {
expose({
foo: 1
})
}
})
const childRef = ref()
const Parent = {
setup() {
return () => h(Child, { ref: childRef })
}
}
const root = nodeOps.createElement('div')
render(h(Parent), root)
expect(childRef.value).toBeTruthy()
expect(childRef.value.foo).toBe(1)
})
})

View File

@ -0,0 +1,91 @@
import { EmitFn, EmitsOptions } from './componentEmits'
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
import { Slots } from './componentSlots'
import { Directive } from './directives'
import { warn } from './warning'
interface DefaultContext {
props: {}
attrs: Record<string, unknown>
emit: (...args: any[]) => void
slots: Slots
}
interface InferredContext<P, E> {
props: Readonly<P>
attrs: Record<string, unknown>
emit: EmitFn<E>
slots: Slots
}
type InferContext<T extends Partial<DefaultContext>, P, E> = {
[K in keyof DefaultContext]: T[K] extends {} ? T[K] : InferredContext<P, E>[K]
}
/**
* This is a subset of full options that are still useful in the context of
* <script setup>. Technically, other options can be used too, but are
* discouraged - if using TypeScript, we nudge users away from doing so by
* disallowing them in types.
*/
interface Options<E extends EmitsOptions, EE extends string> {
emits?: E | EE[]
name?: string
inhertiAttrs?: boolean
directives?: Record<string, Directive>
}
/**
* Compile-time-only helper used for declaring options and retrieving props
* and the setup context inside `<script setup>`.
* This is stripped away in the compiled code and should never be actually
* called at runtime.
*/
// overload 1: no props
export function defineOptions<
T extends Partial<DefaultContext> = {},
E extends EmitsOptions = EmitsOptions,
EE extends string = string
>(
options?: Options<E, EE> & {
props?: undefined
}
): InferContext<T, {}, E>
// overload 2: object props
export function defineOptions<
T extends Partial<DefaultContext> = {},
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
PP extends string = string,
P = Readonly<{ [key in PP]?: any }>
>(
options?: Options<E, EE> & {
props?: PP[]
}
): InferContext<T, P, E>
// overload 3: object props
export function defineOptions<
T extends Partial<DefaultContext> = {},
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions,
P = ExtractPropTypes<PP>
>(
options?: Options<E, EE> & {
props?: PP
}
): InferContext<T, P, E>
// implementation
export function defineOptions() {
if (__DEV__) {
warn(
`defineContext() is a compiler-hint helper that is only usable inside ` +
`<script setup> of a single file component. It will be compiled away ` +
`and should not be used in final distributed code.`
)
}
return 0 as any
}

View File

@ -105,7 +105,7 @@ export interface ComponentInternalOptions {
export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}> export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
extends ComponentInternalOptions { extends ComponentInternalOptions {
// use of any here is intentional so it can be a valid JSX Element constructor // use of any here is intentional so it can be a valid JSX Element constructor
(props: P, ctx: SetupContext<E>): any (props: P, ctx: Omit<SetupContext<E, P>, 'expose'>): any
props?: ComponentPropsOptions<P> props?: ComponentPropsOptions<P>
emits?: E | (keyof E)[] emits?: E | (keyof E)[]
inheritAttrs?: boolean inheritAttrs?: boolean
@ -167,10 +167,12 @@ export const enum LifecycleHooks {
ERROR_CAPTURED = 'ec' ERROR_CAPTURED = 'ec'
} }
export interface SetupContext<E = EmitsOptions> { export interface SetupContext<E = EmitsOptions, P = Data> {
props: P
attrs: Data attrs: Data
slots: Slots slots: Slots
emit: EmitFn<E> emit: EmitFn<E>
expose: (exposed: Record<string, any>) => void
} }
/** /**
@ -270,6 +272,9 @@ export interface ComponentInternalInstance {
// main proxy that serves as the public instance (`this`) // main proxy that serves as the public instance (`this`)
proxy: ComponentPublicInstance | null proxy: ComponentPublicInstance | null
// exposed properties via expose()
exposed: Record<string, any> | null
/** /**
* alternative proxy used only for runtime-compiled render functions using * alternative proxy used only for runtime-compiled render functions using
* `with` block * `with` block
@ -415,6 +420,7 @@ export function createComponentInstance(
update: null!, // will be set synchronously right after creation update: null!, // will be set synchronously right after creation
render: null, render: null,
proxy: null, proxy: null,
exposed: null,
withProxy: null, withProxy: null,
effects: null, effects: null,
provides: parent ? parent.provides : Object.create(appContext.provides), provides: parent ? parent.provides : Object.create(appContext.provides),
@ -731,10 +737,20 @@ const attrHandlers: ProxyHandler<Data> = {
} }
function createSetupContext(instance: ComponentInternalInstance): SetupContext { function createSetupContext(instance: ComponentInternalInstance): SetupContext {
const expose: SetupContext['expose'] = exposed => {
if (__DEV__ && instance.exposed) {
warn(`expose() should be called only once per setup().`)
}
instance.exposed = proxyRefs(exposed)
}
if (__DEV__) { if (__DEV__) {
// We use getters in dev in case libs like test-utils overwrite instance // We use getters in dev in case libs like test-utils overwrite instance
// properties (overwrites should not be done in prod) // properties (overwrites should not be done in prod)
return Object.freeze({ return Object.freeze({
get props() {
return instance.props
},
get attrs() { get attrs() {
return new Proxy(instance.attrs, attrHandlers) return new Proxy(instance.attrs, attrHandlers)
}, },
@ -743,13 +759,16 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
}, },
get emit() { get emit() {
return (event: string, ...args: any[]) => instance.emit(event, ...args) return (event: string, ...args: any[]) => instance.emit(event, ...args)
} },
expose
}) })
} else { } else {
return { return {
props: instance.props,
attrs: instance.attrs, attrs: instance.attrs,
slots: instance.slots, slots: instance.slots,
emit: instance.emit emit: instance.emit,
expose
} }
} }
} }

View File

@ -41,7 +41,9 @@ import {
reactive, reactive,
ComputedGetter, ComputedGetter,
WritableComputedOptions, WritableComputedOptions,
toRaw toRaw,
proxyRefs,
toRef
} from '@vue/reactivity' } from '@vue/reactivity'
import { import {
ComponentObjectPropsOptions, ComponentObjectPropsOptions,
@ -96,7 +98,7 @@ export interface ComponentOptionsBase<
setup?: ( setup?: (
this: void, this: void,
props: Props, props: Props,
ctx: SetupContext<E> ctx: SetupContext<E, Props>
) => Promise<RawBindings> | RawBindings | RenderFunction | void ) => Promise<RawBindings> | RawBindings | RenderFunction | void
name?: string name?: string
template?: string | object // can be a direct DOM node template?: string | object // can be a direct DOM node
@ -110,6 +112,8 @@ export interface ComponentOptionsBase<
directives?: Record<string, Directive> directives?: Record<string, Directive>
inheritAttrs?: boolean inheritAttrs?: boolean
emits?: (E | EE[]) & ThisType<void> emits?: (E | EE[]) & ThisType<void>
// TODO infer public instance type based on exposed keys
expose?: string[]
serverPrefetch?(): Promise<any> serverPrefetch?(): Promise<any>
// Internal ------------------------------------------------------------------ // Internal ------------------------------------------------------------------
@ -461,7 +465,9 @@ export function applyOptions(
render, render,
renderTracked, renderTracked,
renderTriggered, renderTriggered,
errorCaptured errorCaptured,
// public API
expose
} = options } = options
const publicThis = instance.proxy! const publicThis = instance.proxy!
@ -736,6 +742,21 @@ export function applyOptions(
if (unmounted) { if (unmounted) {
onUnmounted(unmounted.bind(publicThis)) onUnmounted(unmounted.bind(publicThis))
} }
if (isArray(expose)) {
if (!asMixin) {
if (expose.length) {
const exposed = instance.exposed || (instance.exposed = proxyRefs({}))
expose.forEach(key => {
exposed[key] = toRef(publicThis, key as any)
})
} else if (!instance.exposed) {
instance.exposed = EMPTY_OBJ
}
} else if (__DEV__) {
warn(`The \`expose\` option is ignored when used in mixins.`)
}
}
} }
function callSyncHook( function callSyncHook(

View File

@ -95,6 +95,7 @@ export function renderComponentRoot(
props, props,
__DEV__ __DEV__
? { ? {
props,
get attrs() { get attrs() {
markAttrsAccessed() markAttrsAccessed()
return attrs return attrs
@ -102,7 +103,7 @@ export function renderComponentRoot(
slots, slots,
emit emit
} }
: { attrs, slots, emit } : { props, attrs, slots, emit }
) )
: render(props, null as any /* we know it doesn't need it */) : render(props, null as any /* we know it doesn't need it */)
) )

View File

@ -43,6 +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 { defineOptions } from './apiDefineOptions'
// Advanced API ---------------------------------------------------------------- // Advanced API ----------------------------------------------------------------

View File

@ -306,12 +306,12 @@ export const setRef = (
return return
} }
let value: ComponentPublicInstance | RendererNode | null let value: ComponentPublicInstance | RendererNode | Record<string, any> | null
if (!vnode) { if (!vnode) {
value = null value = null
} else { } else {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
value = vnode.component!.proxy value = vnode.component!.exposed || vnode.component!.proxy
} else { } else {
value = vnode.el value = vnode.el
} }

View File

@ -1,5 +1,6 @@
import { h, reactive, createApp, ref } from 'vue' import { h, reactive, createApp, ref } from 'vue'
import { CompilerOptions } from '@vue/compiler-dom' import { CompilerOptions } from '@vue/compiler-dom'
import { BindingTypes } from '@vue/compiler-core'
export const ssrMode = ref(false) export const ssrMode = ref(false)
@ -12,9 +13,9 @@ export const compilerOptions: CompilerOptions = reactive({
scopeId: null, scopeId: null,
ssrCssVars: `{ color }`, ssrCssVars: `{ color }`,
bindingMetadata: { bindingMetadata: {
TestComponent: 'setup', TestComponent: BindingTypes.SETUP,
foo: 'setup', foo: BindingTypes.SETUP,
bar: 'props' bar: BindingTypes.PROPS
} }
}) })

View File

@ -0,0 +1,96 @@
import { expectType, defineOptions, Slots, describe } from './index'
describe('no args', () => {
const { props, attrs, emit, slots } = defineOptions()
expectType<{}>(props)
expectType<Record<string, unknown>>(attrs)
expectType<(...args: any[]) => void>(emit)
expectType<Slots>(slots)
// @ts-expect-error
props.foo
// should be able to emit anything
emit('foo')
emit('bar')
})
describe('with type arg', () => {
const { props, attrs, emit, slots } = defineOptions<{
props: {
foo: string
}
emit: (e: 'change') => void
}>()
// explicitly declared type should be refined
expectType<string>(props.foo)
// @ts-expect-error
props.bar
emit('change')
// @ts-expect-error
emit()
// @ts-expect-error
emit('bar')
// non explicitly declared type should fallback to default type
expectType<Record<string, unknown>>(attrs)
expectType<Slots>(slots)
})
// with runtime arg
describe('with runtime arg (array syntax)', () => {
const { props, emit } = defineOptions({
props: ['foo', 'bar'],
emits: ['foo', 'bar']
})
expectType<{
foo?: any
bar?: any
}>(props)
// @ts-expect-error
props.baz
emit('foo')
emit('bar', 123)
// @ts-expect-error
emit('baz')
})
describe('with runtime arg (object syntax)', () => {
const { props, emit } = defineOptions({
props: {
foo: String,
bar: {
type: Number,
default: 1
},
baz: {
type: Array,
required: true
}
},
emits: {
foo: () => {},
bar: null
}
})
expectType<{
foo?: string
bar: number
baz: unknown[]
}>(props)
props.foo && props.foo + 'bar'
props.bar + 1
// @ts-expect-error should be readonly
props.bar++
props.baz.push(1)
emit('foo')
emit('bar')
// @ts-expect-error
emit('baz')
})

View File

@ -7,7 +7,11 @@
"vue": ["../packages/vue/dist"] "vue": ["../packages/vue/dist"]
} }
}, },
"exclude": ["../packages/*/__tests__", "../packages/*/src"], "exclude": [
"../packages/*/__tests__",
"../packages/*/src",
"../packages/template-explorer"
],
"include": [ "include": [
"../packages/global.d.ts", "../packages/global.d.ts",
"../packages/*/dist", "../packages/*/dist",

View File

@ -4,5 +4,5 @@
"noEmit": true, "noEmit": true,
"declaration": true "declaration": true
}, },
"exclude": ["../packages/*/__tests__"] "exclude": ["../packages/*/__tests__", "../packages/template-explorer"]
} }