]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-sfc): compileScript inline render function mode
authorEvan You <yyx990803@gmail.com>
Tue, 10 Nov 2020 21:28:34 +0000 (16:28 -0500)
committerEvan You <yyx990803@gmail.com>
Tue, 10 Nov 2020 21:28:34 +0000 (16:28 -0500)
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/options.ts
packages/compiler-core/src/runtimeHelpers.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/compileTemplate.ts

index 31fe534ca78270f5d589856c7ad7a163f14aa654..0ab78993d415a4594a5d5bf69f3b3222b0908167 100644 (file)
@@ -60,12 +60,16 @@ type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode
 
 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
@@ -199,12 +203,18 @@ export function generate(
   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
@@ -213,10 +223,17 @@ export function generate(
     : ``
   // 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(`)
@@ -290,6 +307,7 @@ export function generate(
   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
   }
@@ -356,7 +374,8 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
 function genModulePreamble(
   ast: RootNode,
   context: CodegenContext,
-  genScopeId: boolean
+  genScopeId: boolean,
+  inline?: boolean
 ) {
   const {
     push,
@@ -423,7 +442,10 @@ function genModulePreamble(
 
   genHoists(ast.hoists, context)
   newline()
-  push(`export `)
+
+  if (!inline) {
+    push(`export `)
+  }
 }
 
 function genAssets(
index 22d8a086b2fe0a84002dc57800388e60f7a3c1bc..b0af0cefabe0b30b4fd59049e68bd169c774ff8c 100644 (file)
@@ -65,7 +65,39 @@ export interface BindingMetadata {
   [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.
    */
@@ -128,26 +160,15 @@ export interface TransformOptions {
    * 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.
@@ -189,11 +210,6 @@ export interface CodegenOptions {
    * @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
index dea6f460b19bd24833960e9e5f6a82a1d46aa826..1cf8c7bfc1209f5fcebb64722f8a534140df5f5e 100644 (file)
@@ -29,6 +29,7 @@ export const PUSH_SCOPE_ID = Symbol(__DEV__ ? `pushScopeId` : ``)
 export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``)
 export const WITH_SCOPE_ID = Symbol(__DEV__ ? `withScopeId` : ``)
 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
 // generated code. Make sure these are correctly exported in the runtime!
@@ -62,7 +63,8 @@ export const helperNameMap: any = {
   [PUSH_SCOPE_ID]: `pushScopeId`,
   [POP_SCOPE_ID]: `popScopeId`,
   [WITH_SCOPE_ID]: `withScopeId`,
-  [WITH_CTX]: `withCtx`
+  [WITH_CTX]: `withCtx`,
+  [UNREF]: `unref`
 }
 
 export function registerRuntimeHelpers(helpers: any) {
index fc56ddf56492b6064bad115ac20bf11638e95d32..aab29d000acef0257297d56b7053f3dc24df56c8 100644 (file)
@@ -124,6 +124,8 @@ export function createTransformContext(
     ssr = false,
     ssrCssVars = ``,
     bindingMetadata = EMPTY_OBJ,
+    inline = false,
+    inlinePropsIdentifier = `$props`,
     onError = defaultOnError
   }: TransformOptions
 ): TransformContext {
@@ -142,6 +144,8 @@ export function createTransformContext(
     ssr,
     ssrCssVars,
     bindingMetadata,
+    inline,
+    inlinePropsIdentifier,
     onError,
 
     // state
index c2f00a4ce274c711cb432c221e498965881c22f7..0f10d88fe023c11bb9a84672eec927567ae67f05 100644 (file)
@@ -28,6 +28,7 @@ import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
 import { validateBrowserExpression } from '../validateExpression'
 import { parse } from '@babel/parser'
 import { walk } from 'estree-walker'
+import { UNREF } from '../runtimeHelpers'
 
 const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
 
@@ -97,12 +98,21 @@ export function processExpression(
     return node
   }
 
-  const { bindingMetadata } = context
+  const { inline, inlinePropsIdentifier, bindingMetadata } = context
   const prefix = (raw: string) => {
-    const source = hasOwn(bindingMetadata, raw)
-      ? `$` + bindingMetadata[raw]
-      : `_ctx`
-    return `${source}.${raw}`
+    if (inline) {
+      // setup inline mode, it's either props or setup
+      if (bindingMetadata[raw] !== 'setup') {
+        return `${inlinePropsIdentifier}.${raw}`
+      } else {
+        return `${context.helperString(UNREF)}(${raw})`
+      }
+    } else {
+      const source = hasOwn(bindingMetadata, raw)
+        ? `$` + bindingMetadata[raw]
+        : `_ctx`
+      return `${source}.${raw}`
+    }
   }
 
   // fast path if expression is a simple identifier.
index e1f2c8ae8124660123a7c8e04b1c34ab357cbabe..d9fffd2f2a02e1f00febb74f0e9983dbb04ca4de 100644 (file)
@@ -464,14 +464,20 @@ describe('SFC compile <script setup>', () => {
         compile(`<script setup>
         export const a = 1
         </script>`)
-      ).toThrow(`cannot contain non-type named exports`)
+      ).toThrow(`cannot contain non-type named or * exports`)
+
+      expect(() =>
+        compile(`<script setup>
+        export * from './foo'
+        </script>`)
+      ).toThrow(`cannot contain non-type named or * exports`)
 
       expect(() =>
         compile(`<script setup>
           const bar = 1
           export { bar as default }
         </script>`)
-      ).toThrow(`cannot contain non-type named exports`)
+      ).toThrow(`cannot contain non-type named or * exports`)
     })
 
     test('ref: non-assignment expressions', () => {
index 45cdea722566ef96c7dabb2315857e2eaada04a5..94eb0e56f8deaea5754ab41150dfd5f4ac6acb0e 100644 (file)
@@ -27,13 +27,28 @@ import {
 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> = {}
@@ -356,10 +371,10 @@ export function compileScript(
   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`
@@ -390,16 +405,20 @@ export function compileScript(
       )
     }
 
+    // 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) {
@@ -409,11 +428,11 @@ export function compileScript(
             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
             }
           }
         }
@@ -560,16 +579,16 @@ export function compileScript(
               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
@@ -583,10 +602,10 @@ export function compileScript(
     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)
     }
@@ -681,7 +700,7 @@ export function compileScript(
   }
 
   // 7. finalize setup argument signature.
-  let args = ``
+  let args = options.inlineTemplate ? `$props` : ``
   if (isTS) {
     if (slotsType === 'Slots') {
       helperImports.add('Slots')
@@ -704,8 +723,8 @@ export function compileScript(
       }
       args = ss.toString()
     }
-  } else {
-    args = hasExplicitSignature ? (setupValue as string) : ``
+  } else if (hasExplicitSignature) {
+    args = setupValue as string
   }
 
   // 8. wrap setup code with function.
@@ -716,11 +735,9 @@ export function compileScript(
     `\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) {
@@ -734,9 +751,58 @@ export function compileScript(
     }
   }
 
+  // 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
@@ -759,24 +825,12 @@ export function compileScript(
     }
   }
 
-  // 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,
index 5d72cf75dd9d3bf8dbfb7e40509c6d18881375f6..85c26eae886611c7cfdeb058db0b32d86052d4ea 100644 (file)
@@ -30,6 +30,7 @@ export interface TemplateCompiler {
 
 export interface SFCTemplateCompileResults {
   code: string
+  preamble?: string
   source: string
   tips: string[]
   errors: (string | CompilerError)[]
@@ -168,7 +169,7 @@ function doCompileTemplate({
     nodeTransforms = [transformAssetUrl, transformSrcset]
   }
 
-  let { code, map } = compiler.compile(source, {
+  let { code, preamble, map } = compiler.compile(source, {
     mode: 'module',
     prefixIdentifiers: 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 {