feat(compiler): basic codegen with source map support

This commit is contained in:
Evan You 2019-09-19 23:05:51 -04:00
parent 98571ab496
commit 9b1a548c6b
15 changed files with 235 additions and 41 deletions

View File

@ -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
})
})
})

View File

@ -1,14 +1,14 @@
import { Position } from '../src/ast' 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 { function p(line: number, column: number, offset: number): Position {
return { column, line, offset } return { column, line, offset }
} }
describe('advancePositionBy', () => { describe('advancePositionWithClone', () => {
test('same line', () => { test('same line', () => {
const pos = p(1, 1, 0) 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.column).toBe(3)
expect(newPos.line).toBe(1) expect(newPos.line).toBe(1)
@ -17,7 +17,7 @@ describe('advancePositionBy', () => {
test('same line', () => { test('same line', () => {
const pos = p(1, 1, 0) 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.column).toBe(1)
expect(newPos.line).toBe(2) expect(newPos.line).toBe(2)
@ -26,7 +26,7 @@ describe('advancePositionBy', () => {
test('multiple lines', () => { test('multiple lines', () => {
const pos = p(1, 1, 0) 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.column).toBe(2)
expect(newPos.line).toBe(3) expect(newPos.line).toBe(3)

View File

@ -9,7 +9,9 @@
"dist" "dist"
], ],
"types": "dist/compiler-core.d.ts", "types": "dist/compiler-core.d.ts",
"sideEffects": false, "buildOptions": {
"formats": ["cjs"]
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/vuejs/vue.git" "url": "git+https://github.com/vuejs/vue.git"
@ -22,5 +24,8 @@
"bugs": { "bugs": {
"url": "https://github.com/vuejs/vue/issues" "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"
}
} }

View File

@ -109,7 +109,7 @@ export interface ForNode extends Node {
} }
export interface Position { export interface Position {
offset: number // from start of file (in SFCs) offset: number // from start of file
line: number line: number
column: number column: number
} }

View 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) {}

View File

@ -1,11 +1,12 @@
import { createDirectiveTransform } from '../transform' import { createDirectiveTransform } from '../transform'
import { NodeTypes, ExpressionNode, Node } from '../ast' import { NodeTypes, ExpressionNode } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { getInnerRange } from '../utils' import { getInnerRange } from '../utils'
const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/ const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g const stripParensRE = /^\(|\)$/g
export const transformFor = createDirectiveTransform( export const transformFor = createDirectiveTransform(
'for', 'for',
(node, dir, context) => { (node, dir, context) => {
@ -114,7 +115,7 @@ function parseAliasExpressions(source: string): AliasExpressions | null {
function maybeCreateExpression( function maybeCreateExpression(
alias: AliasExpression | undefined, alias: AliasExpression | undefined,
node: Node node: ExpressionNode
): ExpressionNode | undefined { ): ExpressionNode | undefined {
if (alias) { if (alias) {
return { return {

View File

@ -1,6 +1,5 @@
export { parse, ParserOptions, TextModes } from './parse' 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 { ErrorCodes } from './errors'
export * from './ast' export * from './ast'
export { transformIf } from './directives/vIf'

View File

@ -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.

View File

@ -1,5 +1,9 @@
import { ErrorCodes, CompilerError, createCompilerError } from './errors' import { ErrorCodes, CompilerError, createCompilerError } from './errors'
import { assert, advancePositionWithMutation } from './utils' import {
assert,
advancePositionWithMutation,
advancePositionWithClone
} from './utils'
import { import {
Namespace, Namespace,
Namespaces, Namespaces,
@ -799,6 +803,7 @@ function startsWith(source: string, searchString: string): boolean {
function advanceBy(context: ParserContext, numberOfCharacters: number): void { function advanceBy(context: ParserContext, numberOfCharacters: number): void {
const { source } = context const { source } = context
__DEV__ && assert(numberOfCharacters <= source.length)
advancePositionWithMutation(context, source, numberOfCharacters) advancePositionWithMutation(context, source, numberOfCharacters)
context.source = source.slice(numberOfCharacters) context.source = source.slice(numberOfCharacters)
} }
@ -815,24 +820,11 @@ function getNewPosition(
start: Position, start: Position,
numberOfCharacters: number numberOfCharacters: number
): Position { ): Position {
const { originalSource } = context return advancePositionWithClone(
const str = originalSource.slice(start.offset, numberOfCharacters) start,
const lines = str.split(/\r?\n/) context.originalSource.slice(start.offset, numberOfCharacters),
numberOfCharacters
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
} }
function emitError( function emitError(

View File

@ -22,7 +22,7 @@ export interface TransformOptions {
onError?: (error: CompilerError) => void onError?: (error: CompilerError) => void
} }
export interface TransformContext extends Required<TransformOptions> { interface TransformContext extends Required<TransformOptions> {
parent: ParentNode parent: ParentNode
ancestors: ParentNode[] ancestors: ParentNode[]
childIndex: number childIndex: number

View File

@ -5,21 +5,27 @@ export function getInnerRange(
offset: number, offset: number,
length?: number length?: number
): SourceLocation { ): SourceLocation {
__DEV__ && assert(offset <= loc.source.length)
const source = loc.source.substr(offset, length) const source = loc.source.substr(offset, length)
const newLoc: SourceLocation = { const newLoc: SourceLocation = {
source, source,
start: advancePositionBy(loc.start, loc.source, offset), start: advancePositionWithClone(loc.start, loc.source, offset),
end: loc.end end: loc.end
} }
if (length != null) { 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 return newLoc
} }
export function advancePositionBy( export function advancePositionWithClone(
pos: Position, pos: Position,
source: string, source: string,
numberOfCharacters: number numberOfCharacters: number
@ -34,8 +40,6 @@ export function advancePositionWithMutation(
source: string, source: string,
numberOfCharacters: number numberOfCharacters: number
): Position { ): Position {
__DEV__ && assert(numberOfCharacters <= source.length)
let linesCount = 0 let linesCount = 0
let lastNewLinePos = -1 let lastNewLinePos = -1
for (let i = 0; i < numberOfCharacters; i++) { for (let i = 0; i < numberOfCharacters; i++) {

View File

@ -13,7 +13,7 @@
"sideEffects": false, "sideEffects": false,
"buildOptions": { "buildOptions": {
"name": "VueDOMCompiler", "name": "VueDOMCompiler",
"formats": ["esm", "cjs", "global", "esm-browser"] "formats": ["cjs", "global", "esm-browser"]
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -93,7 +93,8 @@ function createConfig(output, plugins = []) {
compilerOptions: { compilerOptions: {
declaration: shouldEmitDeclarations, declaration: shouldEmitDeclarations,
declarationMap: shouldEmitDeclarations declarationMap: shouldEmitDeclarations
} },
exclude: ['**/__tests__']
} }
}) })
// we only need to check TS and generate declarations once for each build. // we only need to check TS and generate declarations once for each build.

View File

@ -16,6 +16,7 @@
"removeComments": false, "removeComments": false,
"jsx": "react", "jsx": "react",
"lib": ["esnext", "dom"], "lib": ["esnext", "dom"],
"types": ["jest", "node"],
"rootDir": ".", "rootDir": ".",
"paths": { "paths": {
"@vue/shared": ["packages/shared/src"], "@vue/shared": ["packages/shared/src"],

View File

@ -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" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 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: sourcemap-codec@^1.4.4:
version "1.4.6" version "1.4.6"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9"