feat(compiler): basic codegen with source map support
This commit is contained in:
parent
98571ab496
commit
9b1a548c6b
20
packages/compiler-core/__tests__/codegen.spec.ts
Normal file
20
packages/compiler-core/__tests__/codegen.spec.ts
Normal 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
|
||||
})
|
||||
})
|
||||
})
|
@ -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)
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {}
|
||||
|
@ -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 {
|
||||
|
@ -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'
|
||||
|
@ -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.
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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++) {
|
||||
|
@ -13,7 +13,7 @@
|
||||
"sideEffects": false,
|
||||
"buildOptions": {
|
||||
"name": "VueDOMCompiler",
|
||||
"formats": ["esm", "cjs", "global", "esm-browser"]
|
||||
"formats": ["cjs", "global", "esm-browser"]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -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.
|
||||
|
@ -16,6 +16,7 @@
|
||||
"removeComments": false,
|
||||
"jsx": "react",
|
||||
"lib": ["esnext", "dom"],
|
||||
"types": ["jest", "node"],
|
||||
"rootDir": ".",
|
||||
"paths": {
|
||||
"@vue/shared": ["packages/shared/src"],
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user