]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler): basic codegen with source map support
authorEvan You <yyx990803@gmail.com>
Fri, 20 Sep 2019 03:05:51 +0000 (23:05 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 20 Sep 2019 03:05:51 +0000 (23:05 -0400)
15 files changed:
packages/compiler-core/__tests__/codegen.spec.ts [new file with mode: 0644]
packages/compiler-core/__tests__/utils.spec.ts
packages/compiler-core/package.json
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/directives/vFor.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/optimizations/mergeExpressions.ts [new file with mode: 0644]
packages/compiler-core/src/parse.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/utils.ts
packages/compiler-dom/package.json
rollup.config.js
tsconfig.json
yarn.lock

diff --git a/packages/compiler-core/__tests__/codegen.spec.ts b/packages/compiler-core/__tests__/codegen.spec.ts
new file mode 100644 (file)
index 0000000..03a00b6
--- /dev/null
@@ -0,0 +1,20 @@
+import { parse, generate } from '../src'
+import { SourceMapConsumer, RawSourceMap } from 'source-map'
+
+describe('compiler: codegen', () => {
+  test('basic source map support', async () => {
+    const ast = parse(`hello {{ world }}`)
+    const { code, map } = generate(ast, { module: false })
+    expect(code).toBe(`["hello ", world]`)
+
+    const consumer = await new SourceMapConsumer(map as RawSourceMap)
+    const pos = consumer.originalPositionFor({
+      line: 1,
+      column: 11
+    })
+    expect(pos).toMatchObject({
+      line: 1,
+      column: 6
+    })
+  })
+})
index dffa6be8af086928868562660c2ce7fe9433c4f0..c83ce2261f86083c153f8a153180186971385640 100644 (file)
@@ -1,14 +1,14 @@
 import { Position } from '../src/ast'
-import { getInnerRange, advancePositionBy } from '../src/utils'
+import { getInnerRange, advancePositionWithClone } from '../src/utils'
 
 function p(line: number, column: number, offset: number): Position {
   return { column, line, offset }
 }
 
-describe('advancePositionBy', () => {
+describe('advancePositionWithClone', () => {
   test('same line', () => {
     const pos = p(1, 1, 0)
-    const newPos = advancePositionBy(pos, 'foo\nbar', 2)
+    const newPos = advancePositionWithClone(pos, 'foo\nbar', 2)
 
     expect(newPos.column).toBe(3)
     expect(newPos.line).toBe(1)
@@ -17,7 +17,7 @@ describe('advancePositionBy', () => {
 
   test('same line', () => {
     const pos = p(1, 1, 0)
-    const newPos = advancePositionBy(pos, 'foo\nbar', 4)
+    const newPos = advancePositionWithClone(pos, 'foo\nbar', 4)
 
     expect(newPos.column).toBe(1)
     expect(newPos.line).toBe(2)
@@ -26,7 +26,7 @@ describe('advancePositionBy', () => {
 
   test('multiple lines', () => {
     const pos = p(1, 1, 0)
-    const newPos = advancePositionBy(pos, 'foo\nbar\nbaz', 10)
+    const newPos = advancePositionWithClone(pos, 'foo\nbar\nbaz', 10)
 
     expect(newPos.column).toBe(2)
     expect(newPos.line).toBe(3)
index ea0246ddf37e21b49c7057fbe4824593635bc7df..db71ca548a878a61787bb15f5fd08d3efc1ad990 100644 (file)
@@ -9,7 +9,9 @@
     "dist"
   ],
   "types": "dist/compiler-core.d.ts",
-  "sideEffects": false,
+  "buildOptions": {
+    "formats": ["cjs"]
+  },
   "repository": {
     "type": "git",
     "url": "git+https://github.com/vuejs/vue.git"
@@ -22,5 +24,8 @@
   "bugs": {
     "url": "https://github.com/vuejs/vue/issues"
   },
-  "homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-core#readme"
+  "homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-core#readme",
+  "dependencies": {
+    "source-map": "^0.7.3"
+  }
 }
index 1d14158f1a7cccb71c789e502be0c4f6edc029c4..853a95304ebf29c4bb0167efea8a0176d3170d85 100644 (file)
@@ -109,7 +109,7 @@ export interface ForNode extends Node {
 }
 
 export interface Position {
-  offset: number // from start of file (in SFCs)
+  offset: number // from start of file
   line: number
   column: number
 }
index 70b786d12ed055a08b57f5cf47f717bf6a266301..06753c36a9b7d37c6ec2374ec4bc67e4c234256d 100644 (file)
@@ -1 +1,165 @@
-// TODO
+import {
+  RootNode,
+  ChildNode,
+  ElementNode,
+  IfNode,
+  ForNode,
+  TextNode,
+  CommentNode,
+  ExpressionNode,
+  NodeTypes
+} from './ast'
+import { SourceMapGenerator, RawSourceMap } from 'source-map'
+import { advancePositionWithMutation } from './utils'
+
+export interface CodegenOptions {
+  // Assume ES module environment. If true, will generate import statements for
+  // runtime helpers; otherwise will grab the helpers from global `Vue`.
+  module?: boolean
+  // Filename for source map generation.
+  filename?: string
+}
+
+export interface CodegenResult {
+  code: string
+  map?: RawSourceMap
+}
+
+interface CodegenContext extends Required<CodegenOptions> {
+  source: string
+  code: string
+  line: number
+  column: number
+  offset: number
+  indent: number
+  identifiers: Set<string>
+  map?: SourceMapGenerator
+  push(generatedCode: string, astNode?: ChildNode): void
+}
+
+export function generate(
+  ast: RootNode,
+  options: CodegenOptions = {}
+): CodegenResult {
+  const context = createCodegenContext(ast, options)
+  if (context.module) {
+    // TODO inject import statements on RootNode
+    context.push(`export function render() {\n`)
+    context.indent++
+    context.push(`  return `)
+  }
+  if (ast.children.length === 1) {
+    genNode(ast.children[0], context)
+  } else {
+    genChildren(ast.children, context)
+  }
+  if (context.module) {
+    context.indent--
+    context.push(`\n}`)
+  }
+  return {
+    code: context.code,
+    map: context.map ? context.map.toJSON() : undefined
+  }
+}
+
+function createCodegenContext(
+  ast: RootNode,
+  options: CodegenOptions
+): CodegenContext {
+  const context: CodegenContext = {
+    module: true,
+    filename: `template.vue.html`,
+    ...options,
+    source: ast.loc.source,
+    code: ``,
+    column: 1,
+    line: 1,
+    offset: 0,
+    indent: 0,
+    identifiers: new Set(),
+    // lazy require source-map implementation, only in non-browser builds!
+    map: __BROWSER__
+      ? undefined
+      : new (require('source-map')).SourceMapGenerator(),
+    push(generatedCode, node) {
+      // TODO handle indent
+      context.code += generatedCode
+      if (context.map) {
+        if (node) {
+          context.map.addMapping({
+            source: context.filename,
+            original: {
+              line: node.loc.start.line,
+              column: node.loc.start.column - 1 // source-map column is 0 based
+            },
+            generated: {
+              line: context.line,
+              column: context.column - 1
+            }
+          })
+        }
+        advancePositionWithMutation(
+          context,
+          generatedCode,
+          generatedCode.length
+        )
+      }
+    }
+  }
+  return context
+}
+
+function genChildren(children: ChildNode[], context: CodegenContext) {
+  context.push(`[`)
+  for (let i = 0; i < children.length; i++) {
+    genNode(children[i], context)
+    if (i < children.length - 1) context.push(', ')
+  }
+  context.push(`]`)
+}
+
+function genNode(node: ChildNode, context: CodegenContext) {
+  switch (node.type) {
+    case NodeTypes.ELEMENT:
+      genElement(node, context)
+      break
+    case NodeTypes.TEXT:
+      genText(node, context)
+      break
+    case NodeTypes.EXPRESSION:
+      genExpression(node, context)
+      break
+    case NodeTypes.COMMENT:
+      genComment(node, context)
+      break
+    case NodeTypes.IF:
+      genIf(node, context)
+      break
+    case NodeTypes.FOR:
+      genFor(node, context)
+      break
+  }
+}
+
+function genElement(el: ElementNode, context: CodegenContext) {}
+
+function genText(node: TextNode | ExpressionNode, context: CodegenContext) {
+  context.push(JSON.stringify(node.content), node)
+}
+
+function genExpression(node: ExpressionNode, context: CodegenContext) {
+  if (!__BROWSER__) {
+    // TODO parse expression content and rewrite identifiers
+  }
+  context.push(node.content, node)
+}
+
+function genComment(node: CommentNode, context: CodegenContext) {
+  context.push(`<!--${node.content}-->`, node)
+}
+
+// control flow
+function genIf(node: IfNode, context: CodegenContext) {}
+
+function genFor(node: ForNode, context: CodegenContext) {}
index bd961f396e44f26b75a960726614af554fc82062..095d201ceeecdbd5cd709f18446885a6d0f88610 100644 (file)
@@ -1,11 +1,12 @@
 import { createDirectiveTransform } from '../transform'
-import { NodeTypes, ExpressionNode, Node } from '../ast'
+import { NodeTypes, ExpressionNode } from '../ast'
 import { createCompilerError, ErrorCodes } from '../errors'
 import { getInnerRange } from '../utils'
 
 const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/
 const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
 const stripParensRE = /^\(|\)$/g
+
 export const transformFor = createDirectiveTransform(
   'for',
   (node, dir, context) => {
@@ -114,7 +115,7 @@ function parseAliasExpressions(source: string): AliasExpressions | null {
 
 function maybeCreateExpression(
   alias: AliasExpression | undefined,
-  node: Node
+  node: ExpressionNode
 ): ExpressionNode | undefined {
   if (alias) {
     return {
index d2f3542f0cf871604536a491a40b853c75d03be5..9fcb1534f3af69491f2f73876190df985724c14f 100644 (file)
@@ -1,6 +1,5 @@
 export { parse, ParserOptions, TextModes } from './parse'
-export { transform, Transform, TransformContext } from './transform'
+export { transform, TransformOptions, Transform } from './transform'
+export { generate, CodegenOptions, CodegenResult } from './codegen'
 export { ErrorCodes } from './errors'
 export * from './ast'
-
-export { transformIf } from './directives/vIf'
diff --git a/packages/compiler-core/src/optimizations/mergeExpressions.ts b/packages/compiler-core/src/optimizations/mergeExpressions.ts
new file mode 100644 (file)
index 0000000..13cfc4b
--- /dev/null
@@ -0,0 +1,2 @@
+// TODO merge adjacent text nodes and expressions into a single expression
+// e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child.
index 54959c4009ecbd116e9399d1be523342fcd93a3e..0012e379c064551c9ffd70bf317246fe31322094 100644 (file)
@@ -1,5 +1,9 @@
 import { ErrorCodes, CompilerError, createCompilerError } from './errors'
-import { assert, advancePositionWithMutation } from './utils'
+import {
+  assert,
+  advancePositionWithMutation,
+  advancePositionWithClone
+} from './utils'
 import {
   Namespace,
   Namespaces,
@@ -799,6 +803,7 @@ function startsWith(source: string, searchString: string): boolean {
 
 function advanceBy(context: ParserContext, numberOfCharacters: number): void {
   const { source } = context
+  __DEV__ && assert(numberOfCharacters <= source.length)
   advancePositionWithMutation(context, source, numberOfCharacters)
   context.source = source.slice(numberOfCharacters)
 }
@@ -815,24 +820,11 @@ function getNewPosition(
   start: Position,
   numberOfCharacters: number
 ): Position {
-  const { originalSource } = context
-  const str = originalSource.slice(start.offset, numberOfCharacters)
-  const lines = str.split(/\r?\n/)
-
-  const newPosition = {
-    column: start.column,
-    line: start.line,
-    offset: start.offset
-  }
-
-  newPosition.offset += numberOfCharacters
-  newPosition.line += lines.length - 1
-  newPosition.column =
-    lines.length === 1
-      ? start.column + numberOfCharacters
-      : Math.max(1, lines.pop()!.length)
-
-  return newPosition
+  return advancePositionWithClone(
+    start,
+    context.originalSource.slice(start.offset, numberOfCharacters),
+    numberOfCharacters
+  )
 }
 
 function emitError(
index 7db56d3b12c215e6e3ab47683351a9275aa60145..7db018a322be29c58c9310325652be8b2e985ea8 100644 (file)
@@ -22,7 +22,7 @@ export interface TransformOptions {
   onError?: (error: CompilerError) => void
 }
 
-export interface TransformContext extends Required<TransformOptions> {
+interface TransformContext extends Required<TransformOptions> {
   parent: ParentNode
   ancestors: ParentNode[]
   childIndex: number
index 354b8ec1b264f16201cefdddb797b21a17f2ec0f..49609ac1198796c165ef1f5cd2fd61723bc432cc 100644 (file)
@@ -5,21 +5,27 @@ export function getInnerRange(
   offset: number,
   length?: number
 ): SourceLocation {
+  __DEV__ && assert(offset <= loc.source.length)
   const source = loc.source.substr(offset, length)
   const newLoc: SourceLocation = {
     source,
-    start: advancePositionBy(loc.start, loc.source, offset),
+    start: advancePositionWithClone(loc.start, loc.source, offset),
     end: loc.end
   }
 
   if (length != null) {
-    newLoc.end = advancePositionBy(loc.start, loc.source, offset + length)
+    __DEV__ && assert(offset + length <= loc.source.length)
+    newLoc.end = advancePositionWithClone(
+      loc.start,
+      loc.source,
+      offset + length
+    )
   }
 
   return newLoc
 }
 
-export function advancePositionBy(
+export function advancePositionWithClone(
   pos: Position,
   source: string,
   numberOfCharacters: number
@@ -34,8 +40,6 @@ export function advancePositionWithMutation(
   source: string,
   numberOfCharacters: number
 ): Position {
-  __DEV__ && assert(numberOfCharacters <= source.length)
-
   let linesCount = 0
   let lastNewLinePos = -1
   for (let i = 0; i < numberOfCharacters; i++) {
index 1c916b6f3d75a729d2449942ca9009897b3dc6c7..f5b34967956a90200194ab8f4854d2ebf3bcaecf 100644 (file)
@@ -13,7 +13,7 @@
   "sideEffects": false,
   "buildOptions": {
     "name": "VueDOMCompiler",
-    "formats": ["esm", "cjs", "global", "esm-browser"]
+    "formats": ["cjs", "global", "esm-browser"]
   },
   "repository": {
     "type": "git",
index 360bdf9b2ca2d7f61fc4ebd4231dc2090d4d5106..9f7487bf17dd2d1974a2d962eb53a2b6af74541a 100644 (file)
@@ -93,7 +93,8 @@ function createConfig(output, plugins = []) {
       compilerOptions: {
         declaration: shouldEmitDeclarations,
         declarationMap: shouldEmitDeclarations
-      }
+      },
+      exclude: ['**/__tests__']
     }
   })
   // we only need to check TS and generate declarations once for each build.
index ce959b3764b00f03774ca6c46c063b604aa4e8cf..8c0d5784fe485f9fd0b3d8a1a062617008f34748 100644 (file)
@@ -16,6 +16,7 @@
     "removeComments": false,
     "jsx": "react",
     "lib": ["esnext", "dom"],
+    "types": ["jest", "node"],
     "rootDir": ".",
     "paths": {
       "@vue/shared": ["packages/shared/src"],
index 94f38888f4e7d690589ac5ce9b3605f5c1e14886..13f9c64210995c2015827a6acc9dbfd79e31782b 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -6475,6 +6475,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
   integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
 
+source-map@^0.7.3:
+  version "0.7.3"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
+  integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
+
 sourcemap-codec@^1.4.4:
   version "1.4.6"
   resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9"