--- /dev/null
+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
+ })
+ })
+})
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)
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)
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)
"dist"
],
"types": "dist/compiler-core.d.ts",
- "sideEffects": false,
+ "buildOptions": {
+ "formats": ["cjs"]
+ },
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/vue.git"
"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"
+ }
}
}
export interface Position {
- offset: number // from start of file (in SFCs)
+ offset: number // from start of file
line: number
column: number
}
-// 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) {}
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) => {
function maybeCreateExpression(
alias: AliasExpression | undefined,
- node: Node
+ node: ExpressionNode
): ExpressionNode | undefined {
if (alias) {
return {
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'
--- /dev/null
+// 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.
import { ErrorCodes, CompilerError, createCompilerError } from './errors'
-import { assert, advancePositionWithMutation } from './utils'
+import {
+ assert,
+ advancePositionWithMutation,
+ advancePositionWithClone
+} from './utils'
import {
Namespace,
Namespaces,
function advanceBy(context: ParserContext, numberOfCharacters: number): void {
const { source } = context
+ __DEV__ && assert(numberOfCharacters <= source.length)
advancePositionWithMutation(context, source, numberOfCharacters)
context.source = source.slice(numberOfCharacters)
}
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(
onError?: (error: CompilerError) => void
}
-export interface TransformContext extends Required<TransformOptions> {
+interface TransformContext extends Required<TransformOptions> {
parent: ParentNode
ancestors: ParentNode[]
childIndex: number
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
source: string,
numberOfCharacters: number
): Position {
- __DEV__ && assert(numberOfCharacters <= source.length)
-
let linesCount = 0
let lastNewLinePos = -1
for (let i = 0; i < numberOfCharacters; i++) {
"sideEffects": false,
"buildOptions": {
"name": "VueDOMCompiler",
- "formats": ["esm", "cjs", "global", "esm-browser"]
+ "formats": ["cjs", "global", "esm-browser"]
},
"repository": {
"type": "git",
compilerOptions: {
declaration: shouldEmitDeclarations,
declarationMap: shouldEmitDeclarations
- }
+ },
+ exclude: ['**/__tests__']
}
})
// we only need to check TS and generate declarations once for each build.
"removeComments": false,
"jsx": "react",
"lib": ["esnext", "dom"],
+ "types": ["jest", "node"],
"rootDir": ".",
"paths": {
"@vue/shared": ["packages/shared/src"],
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"