export interface CodegenResult {
code: string
+ preamble: string
ast: RootNode
map?: RawSourceMap
}
export interface CodegenContext
- extends Omit<Required<CodegenOptions>, 'bindingMetadata'> {
+ extends Omit<
+ Required<CodegenOptions>,
+ 'bindingMetadata' | 'inline' | 'inlinePropsIdentifier'
+ > {
source: string
code: string
line: number
const hasHelpers = ast.helpers.length > 0
const useWithBlock = !prefixIdentifiers && mode !== 'module'
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
+ const isSetupInlined = !!options.inline
// 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') {
- genModulePreamble(ast, context, genScopeId)
+ genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
} else {
- genFunctionPreamble(ast, context)
+ genFunctionPreamble(ast, preambleContext)
}
// binding optimizations
: ``
// enter render function
if (!ssr) {
- if (genScopeId) {
- push(`const render = ${PURE_ANNOTATION}_withId(`)
+ if (isSetupInlined) {
+ if (genScopeId) {
+ push(`${PURE_ANNOTATION}_withId(`)
+ }
+ push(`() => {`)
+ } else {
+ if (genScopeId) {
+ push(`const render = ${PURE_ANNOTATION}_withId(`)
+ }
+ push(`function render(_ctx, _cache${optimizeSources}) {`)
}
- push(`function render(_ctx, _cache${optimizeSources}) {`)
} else {
if (genScopeId) {
push(`const ssrRender = ${PURE_ANNOTATION}_withId(`)
return {
ast,
code: context.code,
+ preamble: isSetupInlined ? preambleContext.code : ``,
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined
}
function genModulePreamble(
ast: RootNode,
context: CodegenContext,
- genScopeId: boolean
+ genScopeId: boolean,
+ inline?: boolean
) {
const {
push,
genHoists(ast.hoists, context)
newline()
- push(`export `)
+
+ if (!inline) {
+ push(`export `)
+ }
}
function genAssets(
[key: string]: 'data' | 'props' | 'setup' | 'options'
}
-export interface TransformOptions {
+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
+ /**
+ * Identifier for props in setup() inline mode.
+ */
+ inlinePropsIdentifier?: string
+}
+
+export interface TransformOptions extends SharedTransformCodegenOptions {
/**
* An array of node transforms to be applied to every AST node.
*/
* SFC scoped styles ID
*/
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
* needed to render inline CSS variables on component root
*/
ssrCssVars?: string
- /**
- * Optional binding metadata analyzed from script - used to optimize
- * binding access when `prefixIdentifiers` is enabled.
- */
- bindingMetadata?: BindingMetadata
onError?: (error: CompilerError) => void
}
-export interface CodegenOptions {
+export interface CodegenOptions extends SharedTransformCodegenOptions {
/**
* - `module` mode will generate ES module import statements for helpers
* and export the render function as the default export.
* @default 'Vue'
*/
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
import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map'
import { genCssVarsCode, injectCssVarsCalls } from './genCssVars'
+import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
export interface SFCScriptCompileOptions {
/**
* https://babeljs.io/docs/en/babel-parser#plugins
*/
babelParserPlugins?: ParserPlugin[]
+ /**
+ * Enable ref: label sugar
+ * https://github.com/vuejs/rfcs/pull/228
+ * @default true
+ */
refSugar?: boolean
+ /**
+ * Compile the template and inline the resulting render function
+ * directly inside setup().
+ * - Only affects <script setup>
+ * - This should only be used in production because it prevents the template
+ * from being hot-reloaded separately from component state.
+ */
+ inlineTemplate?: boolean
+ templateOptions?: SFCTemplateCompileOptions
}
const hasWarned: Record<string, boolean> = {}
const setupValue = scriptSetup.setup
const hasExplicitSignature = typeof setupValue === 'string'
- let propsVar: string | undefined
- let emitVar: string | undefined
- let slotsVar: string | undefined
- let attrsVar: string | undefined
+ let propsIdentifier: string | undefined
+ let emitIdentifier: string | undefined
+ let slotsIdentifier: string | undefined
+ let attrsIdentifier: string | undefined
let propsType = `{}`
let emitType = `(e: string, ...args: any[]) => void`
)
}
+ // parse the signature to extract the identifiers users are assigning to
+ // the arguments. props identifier is always needed for inline mode
+ // template compilation
+ const params = ((signatureAST as ExpressionStatement)
+ .expression as ArrowFunctionExpression).params
+ if (params[0] && params[0].type === 'Identifier') {
+ propsASTNode = params[0]
+ propsIdentifier = propsASTNode.name
+ }
+
if (isTS) {
// <script setup="xxx" lang="ts">
- // parse the signature to extract the props/emit variables the user wants
- // we need them to find corresponding type declarations.
- const params = ((signatureAST as ExpressionStatement)
- .expression as ArrowFunctionExpression).params
- if (params[0] && params[0].type === 'Identifier') {
- propsASTNode = params[0]
- propsVar = propsASTNode.name
- }
+ // additional identifiers are needed for TS in order to match declared
+ // types
if (params[1] && params[1].type === 'ObjectPattern') {
setupCtxASTNode = params[1]
for (const p of params[1].properties) {
p.value.type === 'Identifier'
) {
if (p.key.name === 'emit') {
- emitVar = p.value.name
+ emitIdentifier = p.value.name
} else if (p.key.name === 'slots') {
- slotsVar = p.value.name
+ slotsIdentifier = p.value.name
} else if (p.key.name === 'attrs') {
- attrsVar = p.value.name
+ attrsIdentifier = p.value.name
}
}
}
typeNode.end! + startOffset
)
if (typeNode.type === 'TSTypeLiteral') {
- if (id.name === propsVar) {
+ if (id.name === propsIdentifier) {
propsType = typeString
extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes)
- } else if (id.name === slotsVar) {
+ } else if (id.name === slotsIdentifier) {
slotsType = typeString
- } else if (id.name === attrsVar) {
+ } else if (id.name === attrsIdentifier) {
attrsType = typeString
}
} else if (
- id.name === emitVar &&
+ id.name === emitIdentifier &&
typeNode.type === 'TSFunctionType'
) {
emitType = typeString
if (
node.type === 'TSDeclareFunction' &&
node.id &&
- node.id.name === emitVar
+ node.id.name === emitIdentifier
) {
const index = node.id.start! + startOffset
- s.overwrite(index, index + emitVar.length, '__emit__')
+ s.overwrite(index, index + emitIdentifier.length, '__emit__')
emitType = `typeof __emit__`
extractRuntimeEmits(node, typeDeclaredEmits)
}
}
// 7. finalize setup argument signature.
- let args = ``
+ let args = options.inlineTemplate ? `$props` : ``
if (isTS) {
if (slotsType === 'Slots') {
helperImports.add('Slots')
}
args = ss.toString()
}
- } else {
- args = hasExplicitSignature ? (setupValue as string) : ``
+ } else if (hasExplicitSignature) {
+ args = setupValue as string
}
// 8. wrap setup code with function.
`\nexport ${hasAwait ? `async ` : ``}function setup(${args}) {\n`
)
- // generate return statement
const exposedBindings = { ...userImports, ...setupBindings }
- let returned = `{ ${Object.keys(exposedBindings).join(', ')} }`
- // inject `useCssVars` calls
+ // 9. inject `useCssVars` calls
if (hasCssVars) {
helperImports.add(`useCssVars`)
for (const style of styles) {
}
}
+ // 10. analyze binding metadata
+ if (scriptAst) {
+ Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
+ }
+ Object.keys(exposedBindings).forEach(key => {
+ bindingMetadata[key] = 'setup'
+ })
+ Object.keys(typeDeclaredProps).forEach(key => {
+ bindingMetadata[key] = 'props'
+ })
+ Object.assign(bindingMetadata, analyzeScriptBindings(scriptSetupAst))
+
+ // 11. generate return statement
+ let returned
+ if (options.inlineTemplate) {
+ if (sfc.template) {
+ // inline render function mode - we are going to compile the template and
+ // inline it right here
+ const { code, preamble, tips, errors } = compileTemplate({
+ ...options.templateOptions,
+ filename,
+ source: sfc.template.content,
+ compilerOptions: {
+ inline: true,
+ inlinePropsIdentifier: propsIdentifier,
+ bindingMetadata
+ }
+ // TODO source map
+ })
+ if (tips.length) {
+ tips.forEach(warnOnce)
+ }
+ const err = errors[0]
+ if (typeof err === 'string') {
+ throw new Error(err)
+ } else if (err) {
+ throw err
+ }
+ if (preamble) {
+ s.prepend(preamble)
+ }
+ returned = code
+ } else {
+ returned = `() => {}`
+ }
+ } else {
+ // return bindings from setup
+ returned = `{ ${Object.keys(exposedBindings).join(', ')} }`
+ }
s.appendRight(endOffset, `\nreturn ${returned}\n}\n\n`)
- // 9. finalize default export
+ // 12. finalize default export
if (isTS) {
// for TS, make sure the exported type is still valid type with
// correct props information
}
}
- // 10. finalize Vue helper imports
+ // 13. finalize Vue helper imports
const helpers = [...helperImports].filter(i => userImports[i] !== 'vue')
if (helpers.length) {
s.prepend(`import { ${helpers.join(', ')} } from 'vue'\n`)
}
- // 11. expose bindings for template compiler optimization
- if (scriptAst) {
- Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
- }
- Object.keys(exposedBindings).forEach(key => {
- bindingMetadata[key] = 'setup'
- })
- Object.keys(typeDeclaredProps).forEach(key => {
- bindingMetadata[key] = 'props'
- })
- Object.assign(bindingMetadata, analyzeScriptBindings(scriptSetupAst))
-
s.trim()
return {
...scriptSetup,