]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-vapor): node transform
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Thu, 30 Nov 2023 23:34:18 +0000 (07:34 +0800)
committer三咲智子 Kevin Deng <sxzz@sxzz.moe>
Thu, 30 Nov 2023 23:42:43 +0000 (07:42 +0800)
14 files changed:
README.md
packages/compiler-core/src/ast.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/utils.ts
packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap
packages/compiler-vapor/__tests__/compile.test.ts
packages/compiler-vapor/__tests__/fixtures.test.ts
packages/compiler-vapor/src/compile.ts
packages/compiler-vapor/src/errors.ts
packages/compiler-vapor/src/hack.ts [new file with mode: 0644]
packages/compiler-vapor/src/index.ts
packages/compiler-vapor/src/transform.ts
packages/compiler-vapor/src/transforms/vOnce.ts [new file with mode: 0644]
playground/vite.config.ts

index 0c1ad0197224e03c813a91f81755e7204361a0e6..b7100cc01c31f326de3a3412baaa003a1a9259f0 100644 (file)
--- a/README.md
+++ b/README.md
@@ -16,6 +16,9 @@ PR are welcome! However, please create an issue before you start to work on it,
   - [x] simple bindings
   - [x] simple events
 - [ ] TODO-MVC App
+- [ ] transform
+  - [x] NodeTransform
+  - [ ] DirectiveTransform
 - [ ] directives
   - [x] `v-once`
   - [x] `v-html`
index 2bc85bf53d8775187b8be5d38843451d20dece9c..07741aea1a8ad1c1f41194191f17523c1a2770b9 100644 (file)
@@ -85,6 +85,13 @@ export interface Position {
   column: number
 }
 
+export type AllNode =
+  | ParentNode
+  | ExpressionNode
+  | TemplateChildNode
+  | AttributeNode
+  | DirectiveNode
+
 export type ParentNode = RootNode | ElementNode | IfBranchNode | ForNode
 
 export type ExpressionNode = SimpleExpressionNode | CompoundExpressionNode
index 74ca59e69eee733e73dd5db360ca5ef3e2bc6811..09259cc7b9d5ab27ad4f43a91fcd47c9d70a9c91 100644 (file)
@@ -68,5 +68,6 @@ export { generateCodeFrame } from '@vue/shared'
 export {
   checkCompatEnabled,
   warnDeprecation,
-  CompilerDeprecationTypes
+  CompilerDeprecationTypes,
+  type CompilerCompatOptions
 } from './compat/compatConfig'
index a159d2eedc729a75239c3691f36bf1048c708ecf..a1e084eb974405e63d3b7e9b3ba0ed719f9a8b1c 100644 (file)
@@ -224,6 +224,7 @@ export function assert(condition: boolean, msg?: string) {
   }
 }
 
+/** find directive */
 export function findDir(
   node: ElementNode,
   name: string | RegExp,
index 5090fc42e97b4be941a35b805b29d931664bd828..1682d354dab1e871aa700329afba2794960509db 100644 (file)
@@ -104,16 +104,14 @@ export function render(_ctx) {
 `;
 
 exports[`compile > directives > v-once > as root node 1`] = `
-"import { template, children, effect, setAttr } from 'vue/vapor';
+"import { template, children, setAttr } from 'vue/vapor';
 const t0 = template('<div></div>');
 export function render(_ctx) {
   const n0 = t0();
   const {
     0: [n1],
   } = children(n0);
-  effect(() => {
-    setAttr(n1, 'id', undefined, foo);
-  });
+  setAttr(n1, 'id', undefined, foo);
   return n0;
 }
 "
index b73270c38949cb3870f085b9a03a707d3aa714a9..21046f388c0dea4976bd66cb90ec62240ed917ae 100644 (file)
@@ -1,8 +1,12 @@
-import { BindingTypes, CompilerOptions, RootNode } from '@vue/compiler-dom'
+import { type RootNode, BindingTypes } from '@vue/compiler-dom'
+import {
+  type CompilerOptions,
+  VaporErrorCodes,
+  compile as _compile,
+} from '../src'
+
 // TODO remove it
 import { format } from 'prettier'
-import { compile as _compile } from '../src'
-import { ErrorCodes } from '../src/errors'
 
 async function compile(
   template: string | RootNode,
@@ -78,7 +82,7 @@ describe('compile', () => {
         await compile(`<div v-bind:arg />`, { onError })
 
         expect(onError.mock.calls[0][0]).toMatchObject({
-          code: ErrorCodes.VAPOR_BIND_NO_EXPRESSION,
+          code: VaporErrorCodes.X_VAPOR_BIND_NO_EXPRESSION,
           loc: {
             start: {
               line: 1,
@@ -107,7 +111,7 @@ describe('compile', () => {
         const onError = vi.fn()
         await compile(`<div v-on:click />`, { onError })
         expect(onError.mock.calls[0][0]).toMatchObject({
-          code: ErrorCodes.VAPOR_ON_NO_EXPRESSION,
+          code: VaporErrorCodes.X_VAPOR_ON_NO_EXPRESSION,
           loc: {
             start: {
               line: 1,
@@ -170,9 +174,9 @@ describe('compile', () => {
       test('basic', async () => {
         const code = await compile(
           `<div v-once>
-          {{ msg }}
-          <span :class="clz" />
-        </div>`,
+            {{ msg }}
+            <span :class="clz" />
+          </div>`,
           {
             bindingMetadata: {
               msg: BindingTypes.SETUP_REF,
@@ -183,7 +187,7 @@ describe('compile', () => {
         expect(code).matchSnapshot()
       })
 
-      test.fails('as root node', async () => {
+      test('as root node', async () => {
         const code = await compile(`<div :id="foo" v-once />`)
         expect(code).toMatchSnapshot()
         expect(code).not.contains('effect')
index 7ece9c981d5c6e668d9ee2ed77931c1483d1fae2..223c8eb505f114e7d942f1ec9d5fe7d16b415cec 100644 (file)
@@ -3,11 +3,11 @@ import { parse, compileScript } from '@vue/compiler-sfc'
 import source from './fixtures/counter.vue?raw'
 
 test('fixtures', async () => {
-  const { descriptor } = parse(source, { compiler: CompilerVapor })
+  const { descriptor } = parse(source, { compiler: CompilerVapor as any })
   const script = compileScript(descriptor, {
     id: 'counter.vue',
     inlineTemplate: true,
-    templateOptions: { compiler: CompilerVapor },
+    templateOptions: { compiler: CompilerVapor as any },
   })
   expect(script.content).matchSnapshot()
 })
index c8a23dd631b682b19a21631922426c37c993c44c..62b4f27dea5b5974800b2fe7a24a509b48535624 100644 (file)
@@ -1,19 +1,86 @@
 import {
   type CodegenResult,
-  type CompilerOptions,
+  type CompilerOptions as BaseCompilerOptions,
   type RootNode,
+  type DirectiveTransform,
   parse,
 } from '@vue/compiler-dom'
-import { isString } from '@vue/shared'
-import { transform } from './transform'
+import { extend, isString } from '@vue/shared'
+import { NodeTransform, transform } from './transform'
 import { generate } from './generate'
+import { defaultOnError, createCompilerError, VaporErrorCodes } from './errors'
+import { transformOnce } from './transforms/vOnce'
+import { HackOptions } from './hack'
 
+export type CompilerOptions = HackOptions<BaseCompilerOptions>
+
+// TODO: copied from @vue/compiler-core
 // code/AST -> IR -> JS codegen
 export function compile(
-  template: string | RootNode,
+  source: string | RootNode,
   options: CompilerOptions = {},
 ): CodegenResult {
-  const ast = isString(template) ? parse(template, options) : template
-  const ir = transform(ast, options)
-  return generate(ir, options)
+  const onError = options.onError || defaultOnError
+  const isModuleMode = options.mode === 'module'
+  /* istanbul ignore if */
+  if (__BROWSER__) {
+    if (options.prefixIdentifiers === true) {
+      onError(createCompilerError(VaporErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
+    } else if (isModuleMode) {
+      onError(createCompilerError(VaporErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
+    }
+  }
+
+  const prefixIdentifiers =
+    !__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
+
+  // TODO scope id
+  // if (options.scopeId && !isModuleMode) {
+  //   onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
+  // }
+
+  const ast = isString(source) ? parse(source, options) : source
+  const [nodeTransforms, directiveTransforms] =
+    getBaseTransformPreset(prefixIdentifiers)
+
+  if (!__BROWSER__ && options.isTS) {
+    const { expressionPlugins } = options
+    if (!expressionPlugins || !expressionPlugins.includes('typescript')) {
+      options.expressionPlugins = [...(expressionPlugins || []), 'typescript']
+    }
+  }
+
+  const ir = transform(
+    ast,
+    extend({}, options, {
+      prefixIdentifiers,
+      nodeTransforms: [
+        ...nodeTransforms,
+        ...(options.nodeTransforms || []), // user transforms
+      ],
+      directiveTransforms: extend(
+        {},
+        directiveTransforms,
+        options.directiveTransforms || {}, // user transforms
+      ),
+    }),
+  )
+
+  return generate(
+    ir,
+    extend({}, options, {
+      prefixIdentifiers,
+    }),
+  )
+}
+
+export type TransformPreset = [
+  NodeTransform[],
+  Record<string, DirectiveTransform>,
+]
+
+export function getBaseTransformPreset(
+  prefixIdentifiers?: boolean,
+): TransformPreset {
+  return [[transformOnce], {}]
 }
index 4f89b5eadc7195fa562dbf4a474f35e0e1787dac..b95eca3c7b57f8efa028d96fcc5081f6c566c328 100644 (file)
@@ -1,7 +1,6 @@
-import { CompilerError } from '@vue/compiler-dom'
+import type { CompilerError } from '@vue/compiler-dom'
 
 export { createCompilerError } from '@vue/compiler-dom'
-
 export function defaultOnError(error: CompilerError) {
   throw error
 }
@@ -10,14 +9,21 @@ export function defaultOnWarn(msg: CompilerError) {
   __DEV__ && console.warn(`[Vue warn] ${msg.message}`)
 }
 
-export enum ErrorCodes {
+export enum VaporErrorCodes {
   // transform errors
-  VAPOR_BIND_NO_EXPRESSION,
-  VAPOR_ON_NO_EXPRESSION,
+  X_VAPOR_BIND_NO_EXPRESSION,
+  X_VAPOR_ON_NO_EXPRESSION,
+
+  // generic errors
+  X_PREFIX_ID_NOT_SUPPORTED,
+  X_MODULE_MODE_NOT_SUPPORTED,
 }
 
-export const errorMessages: Record<ErrorCodes, string> = {
+export const errorMessages: Record<VaporErrorCodes, string> = {
   // transform errors
-  [ErrorCodes.VAPOR_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
-  [ErrorCodes.VAPOR_ON_NO_EXPRESSION]: `v-on is missing expression.`,
+  [VaporErrorCodes.X_VAPOR_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
+  [VaporErrorCodes.X_VAPOR_ON_NO_EXPRESSION]: `v-on is missing expression.`,
+
+  [VaporErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
+  [VaporErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`,
 }
diff --git a/packages/compiler-vapor/src/hack.ts b/packages/compiler-vapor/src/hack.ts
new file mode 100644 (file)
index 0000000..6b74dce
--- /dev/null
@@ -0,0 +1,9 @@
+import type { Prettify } from '@vue/shared'
+import type { NodeTransform } from './transform'
+
+type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> &
+  Pick<U, Extract<keyof U, keyof T>>
+
+export type HackOptions<T> = Prettify<
+  Overwrite<T, { nodeTransforms?: NodeTransform[] }>
+>
index d64408d700bc2c49a85a81ad4a91e834babcc46a..1e7b4bd9fcda8baee0fa5a44587f3000cbf2be25 100644 (file)
@@ -1,5 +1,6 @@
 export { parse } from '@vue/compiler-dom'
 export { transform } from './transform'
 export { generate } from './generate'
-export { compile } from './compile'
+export { compile, type CompilerOptions } from './compile'
 export * from './ir'
+export * from './errors'
index fed7b5a9dc35525294ad875c55b190c0259cd59d..dd88feceec81ebd813ed18f30dd953051108d802 100644 (file)
@@ -1,15 +1,17 @@
 import {
   type RootNode,
-  type Node,
   type TemplateChildNode,
   type ElementNode,
   type AttributeNode,
   type InterpolationNode,
-  type TransformOptions,
+  type TransformOptions as BaseTransformOptions,
   type DirectiveNode,
   type ExpressionNode,
+  type ParentNode,
+  type AllNode,
   NodeTypes,
   BindingTypes,
+  CompilerCompatOptions,
 } from '@vue/compiler-dom'
 import {
   type OperationNode,
@@ -17,25 +19,35 @@ import {
   IRNodeTypes,
   DynamicInfo,
 } from './ir'
-import { isVoidTag } from '@vue/shared'
+import { EMPTY_OBJ, NOOP, isArray, isVoidTag } from '@vue/shared'
 import {
-  ErrorCodes,
+  VaporErrorCodes,
   createCompilerError,
   defaultOnError,
   defaultOnWarn,
 } from './errors'
+import { HackOptions } from './hack'
 
-export interface TransformContext<T extends Node = Node> {
+export type NodeTransform = (
+  node: RootNode | TemplateChildNode,
+  context: TransformContext,
+) => void | (() => void) | (() => void)[]
+
+export type TransformOptions = HackOptions<BaseTransformOptions>
+
+export interface TransformContext<T extends AllNode = AllNode> {
   node: T
-  parent: TransformContext | null
+  parent: TransformContext<ParentNode> | null
   root: TransformContext<RootNode>
   index: number
-  options: TransformOptions
+  options: Required<
+    Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
+  >
 
   template: string
   dynamic: DynamicInfo
 
-  once: boolean
+  inVOnce: boolean
 
   reference(): number
   increaseId(): number
@@ -45,10 +57,11 @@ export interface TransformContext<T extends Node = Node> {
   helper(name: string): string
 }
 
+// TODO use class for better perf
 function createRootContext(
   ir: RootIRNode,
   node: RootNode,
-  options: TransformOptions,
+  options: TransformOptions = {},
 ): TransformContext<RootNode> {
   let globalId = 0
   const { effect, operation: operation, helpers, vaporHelpers } = ir
@@ -58,9 +71,32 @@ function createRootContext(
     parent: null,
     index: 0,
     root: null!, // set later
-    options,
+    options: {
+      filename: '',
+      prefixIdentifiers: false,
+      hoistStatic: false,
+      hmr: false,
+      cacheHandlers: false,
+      nodeTransforms: [],
+      directiveTransforms: {},
+      transformHoist: null,
+      isBuiltInComponent: NOOP,
+      isCustomElement: NOOP,
+      expressionPlugins: [],
+      scopeId: null,
+      slotted: true,
+      ssr: false,
+      inSSR: false,
+      ssrCssVars: ``,
+      bindingMetadata: EMPTY_OBJ,
+      inline: false,
+      isTS: false,
+      onError: defaultOnError,
+      onWarn: defaultOnWarn,
+      ...options,
+    },
     dynamic: ir.dynamic,
-    once: false,
+    inVOnce: false,
 
     increaseId: () => globalId++,
     reference() {
@@ -69,7 +105,7 @@ function createRootContext(
       return (this.dynamic.id = this.increaseId())
     },
     registerEffect(expr, operation) {
-      if (this.once) {
+      if (this.inVOnce) {
         return this.registerOperation(operation)
       }
       if (!effect[expr]) effect[expr] = []
@@ -110,7 +146,7 @@ function createRootContext(
 
 function createContext<T extends TemplateChildNode>(
   node: T,
-  parent: TransformContext,
+  parent: TransformContext<ParentNode>,
   index: number,
 ): TransformContext<T> {
   const ctx: TransformContext<T> = {
@@ -159,7 +195,7 @@ export function transform(
   const ctx = createRootContext(ir, root, options)
 
   // TODO: transform presets, see packages/compiler-core/src/transforms
-  transformChildren(ctx, true)
+  transformNode(ctx)
   if (ir.template.length === 0) {
     ir.template.push({
       type: IRNodeTypes.FRAGMENT_FACTORY,
@@ -170,20 +206,108 @@ export function transform(
   return ir
 }
 
-function transformChildren(
-  ctx: TransformContext<RootNode | ElementNode>,
-  root?: boolean,
+function transformNode(
+  context: TransformContext<RootNode | TemplateChildNode>,
 ) {
+  let { node, index } = context
+
+  // apply transform plugins
+  const { nodeTransforms } = context.options
+  const exitFns = []
+  for (const nodeTransform of nodeTransforms) {
+    // TODO nodeTransform type
+    const onExit = nodeTransform(node, context as any)
+    if (onExit) {
+      if (isArray(onExit)) {
+        exitFns.push(...onExit)
+      } else {
+        exitFns.push(onExit)
+      }
+    }
+    if (!context.node) {
+      // node was removed
+      return
+    } else {
+      // node may have been replaced
+      node = context.node
+    }
+  }
+
+  if (node.type === NodeTypes.ROOT) {
+    transformChildren(context as TransformContext<RootNode>)
+    return
+  }
+
+  const parentChildren = context.parent!.node.children
+  const isFirst = index === 0
+  const isLast = index === parentChildren.length - 1
+
+  switch (node.type) {
+    case NodeTypes.ELEMENT: {
+      transformElement(context as TransformContext<ElementNode>)
+      break
+    }
+    case NodeTypes.TEXT: {
+      context.template += node.content
+      break
+    }
+    case NodeTypes.COMMENT: {
+      context.template += `<!--${node.content}-->`
+      break
+    }
+    case NodeTypes.INTERPOLATION: {
+      transformInterpolation(
+        context as TransformContext<InterpolationNode>,
+        isFirst,
+        isLast,
+      )
+      break
+    }
+    case NodeTypes.TEXT_CALL:
+      // never
+      break
+    default: {
+      // TODO handle other types
+      // CompoundExpressionNode
+      // IfNode
+      // IfBranchNode
+      // ForNode
+      context.template += `[type: ${node.type}]`
+    }
+  }
+
+  // exit transforms
+  context.node = node
+  let i = exitFns.length
+  while (i--) {
+    exitFns[i]()
+  }
+}
+
+function transformChildren(ctx: TransformContext<RootNode | ElementNode>) {
   const {
     node: { children },
   } = ctx
   const childrenTemplate: string[] = []
-  children.forEach((child, i) => walkNode(child, i))
+  children.forEach((child, index) => {
+    const childContext = createContext(child, ctx, index)
+    transformNode(childContext)
+
+    childrenTemplate.push(childContext.template)
+    if (
+      childContext.dynamic.ghost ||
+      childContext.dynamic.referenced ||
+      childContext.dynamic.placeholder ||
+      Object.keys(childContext.dynamic.children).length
+    ) {
+      ctx.dynamic.children[index] = childContext.dynamic
+    }
+  })
 
   processDynamicChildren()
   ctx.template += childrenTemplate.join('')
 
-  if (root) ctx.registerTemplate()
+  if (ctx.node.type === NodeTypes.ROOT) ctx.registerTemplate()
 
   function processDynamicChildren() {
     let prevChildren: DynamicInfo[] = []
@@ -229,57 +353,6 @@ function transformChildren(
       }
     }
   }
-
-  function walkNode(node: TemplateChildNode, index: number) {
-    const child = createContext(node, ctx, index)
-    const isFirst = index === 0
-    const isLast = index === children.length - 1
-
-    switch (node.type) {
-      case NodeTypes.ELEMENT: {
-        transformElement(child as TransformContext<ElementNode>)
-        break
-      }
-      case NodeTypes.TEXT: {
-        child.template += node.content
-        break
-      }
-      case NodeTypes.COMMENT: {
-        child.template += `<!--${node.content}-->`
-        break
-      }
-      case NodeTypes.INTERPOLATION: {
-        transformInterpolation(
-          child as TransformContext<InterpolationNode>,
-          isFirst,
-          isLast,
-        )
-        break
-      }
-      case NodeTypes.TEXT_CALL:
-        // never?
-        break
-      default: {
-        // TODO handle other types
-        // CompoundExpressionNode
-        // IfNode
-        // IfBranchNode
-        // ForNode
-        child.template += `[type: ${node.type}]`
-      }
-    }
-
-    childrenTemplate.push(child.template)
-
-    if (
-      child.dynamic.ghost ||
-      child.dynamic.referenced ||
-      child.dynamic.placeholder ||
-      Object.keys(child.dynamic.children).length
-    ) {
-      ctx.dynamic.children[index] = child.dynamic
-    }
-  }
 }
 
 function transformElement(ctx: TransformContext<ElementNode>) {
@@ -365,7 +438,7 @@ function transformProp(
         (exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content.trim())
       ) {
         ctx.options.onError!(
-          createCompilerError(ErrorCodes.VAPOR_BIND_NO_EXPRESSION, loc),
+          createCompilerError(VaporErrorCodes.X_VAPOR_BIND_NO_EXPRESSION, loc),
         )
         return
       }
@@ -394,7 +467,7 @@ function transformProp(
     case 'on': {
       if (!exp && !modifiers.length) {
         ctx.options.onError!(
-          createCompilerError(ErrorCodes.VAPOR_ON_NO_EXPRESSION, loc),
+          createCompilerError(VaporErrorCodes.X_VAPOR_ON_NO_EXPRESSION, loc),
         )
         return
       }
@@ -441,10 +514,6 @@ function transformProp(
       })
       break
     }
-    case 'once': {
-      ctx.once = true
-      break
-    }
     case 'cloak': {
       // do nothing
       break
diff --git a/packages/compiler-vapor/src/transforms/vOnce.ts b/packages/compiler-vapor/src/transforms/vOnce.ts
new file mode 100644 (file)
index 0000000..a22ee46
--- /dev/null
@@ -0,0 +1,14 @@
+import { NodeTypes, findDir } from '@vue/compiler-dom'
+import { NodeTransform } from '../transform'
+
+const seen = new WeakSet()
+
+export const transformOnce: NodeTransform = (node, context) => {
+  if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) {
+    if (seen.has(node) || context.inVOnce /* || context.inSSR */) {
+      return
+    }
+    seen.add(node)
+    context.inVOnce = true
+  }
+}
index fb76f62662e331b95a01de32c59547884bca729c..6f5a88d2a3f8f0a659de8a808db3e40f31805272 100644 (file)
@@ -13,7 +13,7 @@ export default defineConfig({
   plugins: [
     Vue({
       template: {
-        compiler: CompilerVapor
+        compiler: CompilerVapor as any
       },
       compiler: CompilerSFC
     }),