diff --git a/packages/compiler-core/__tests__/codegen.spec.ts b/packages/compiler-core/__tests__/codegen.spec.ts index 73e4a5a5..d5f17f17 100644 --- a/packages/compiler-core/__tests__/codegen.spec.ts +++ b/packages/compiler-core/__tests__/codegen.spec.ts @@ -8,15 +8,24 @@ describe('compiler: codegen', () => { const { code, map } = generate(ast, { filename: `foo.vue` }) - expect(code).toBe(`["hello ", world]`) + expect(code).toBe( + `return function render() { + with (this) { + return [ + "hello ", + world + ] + } +}` + ) expect(map!.sources).toEqual([`foo.vue`]) expect(map!.sourcesContent).toEqual([source]) const consumer = await new SourceMapConsumer(map as RawSourceMap) const pos = consumer.originalPositionFor({ - line: 1, - column: 11 + line: 5, + column: 6 }) expect(pos).toMatchObject({ line: 1, diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index d97b976a..75d07527 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -20,10 +20,10 @@ export const enum NodeTypes { IF_BRANCH, FOR, // codegen - CALL_EXPRESSION, - OBJECT_EXPRESSION, - PROPERTY, - ARRAY_EXPRESSION + JS_CALL_EXPRESSION, + JS_OBJECT_EXPRESSION, + JS_PROPERTY, + JS_ARRAY_EXPRESSION } export const enum ElementTypes { @@ -129,36 +129,35 @@ export interface ForNode extends Node { children: ChildNode[] } -// We also include a subset of JavaScript AST for code generation -// purposes. The AST is intentioanlly minimal just to meet the exact needs of +// We also include a number of JavaScript AST nodes for code generation. +// The AST is an intentioanlly minimal subset just to meet the exact needs of // Vue render function generation. -type CodegenNode = - | string +export type JSChildNode = | CallExpression | ObjectExpression | ArrayExpression | ExpressionNode export interface CallExpression extends Node { - type: NodeTypes.CALL_EXPRESSION + type: NodeTypes.JS_CALL_EXPRESSION callee: string // can only be imported runtime helpers, so no source location - arguments: Array + arguments: Array } export interface ObjectExpression extends Node { - type: NodeTypes.OBJECT_EXPRESSION + type: NodeTypes.JS_OBJECT_EXPRESSION properties: Array } export interface Property extends Node { - type: NodeTypes.PROPERTY + type: NodeTypes.JS_PROPERTY key: ExpressionNode value: ExpressionNode } export interface ArrayExpression extends Node { - type: NodeTypes.ARRAY_EXPRESSION - elements: Array + type: NodeTypes.JS_ARRAY_EXPRESSION + elements: Array } export function createArrayExpression( @@ -166,7 +165,7 @@ export function createArrayExpression( loc: SourceLocation ): ArrayExpression { return { - type: NodeTypes.ARRAY_EXPRESSION, + type: NodeTypes.JS_ARRAY_EXPRESSION, loc, elements } @@ -177,7 +176,7 @@ export function createObjectExpression( loc: SourceLocation ): ObjectExpression { return { - type: NodeTypes.OBJECT_EXPRESSION, + type: NodeTypes.JS_OBJECT_EXPRESSION, loc, properties } @@ -189,7 +188,7 @@ export function createObjectProperty( loc: SourceLocation ): Property { return { - type: NodeTypes.PROPERTY, + type: NodeTypes.JS_PROPERTY, loc, key, value @@ -215,7 +214,7 @@ export function createCallExpression( loc: SourceLocation ): CallExpression { return { - type: NodeTypes.CALL_EXPRESSION, + type: NodeTypes.JS_CALL_EXPRESSION, loc, callee, arguments: args diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index fadef39b..ed2b6f05 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -7,16 +7,26 @@ import { TextNode, CommentNode, ExpressionNode, - NodeTypes + NodeTypes, + JSChildNode, + CallExpression, + ArrayExpression, + ObjectExpression, + IfBranchNode } from './ast' import { SourceMapGenerator, RawSourceMap } from 'source-map' -import { advancePositionWithMutation } from './utils' +import { advancePositionWithMutation, assert } from './utils' +import { isString, isArray } from '@vue/shared' +import { RENDER_LIST_HELPER } from './transforms/vFor' + +type CodegenNode = ChildNode | JSChildNode export interface CodegenOptions { - // Assume ES module environment. If true, will generate import statements for + // will generate import statements for // runtime helpers; otherwise will grab the helpers from global `Vue`. // default: false - module?: boolean + mode?: 'module' | 'function' + useWith?: boolean // Filename for source map generation. filename?: string } @@ -32,52 +42,34 @@ export interface CodegenContext extends Required { line: number column: number offset: number - indent: number + indentLevel: number imports: Set knownIdentifiers: Set 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 - } + push(code: string, node?: CodegenNode): void + indent(): void + deindent(): void + newline(): void } function createCodegenContext( ast: RootNode, - { module = false, filename = `template.vue.html` }: CodegenOptions + { + mode = 'function', + useWith = true, + filename = `template.vue.html` + }: CodegenOptions ): CodegenContext { const context: CodegenContext = { - module, + mode, + useWith, filename, source: ast.loc.source, code: ``, column: 1, line: 1, offset: 0, - indent: 0, + indentLevel: 0, imports: new Set(), knownIdentifiers: new Set(), @@ -86,9 +78,8 @@ function createCodegenContext( ? undefined : new (require('source-map')).SourceMapGenerator(), - push(generatedCode, node) { - // TODO handle indent - context.code += generatedCode + push(code, node?: CodegenNode) { + context.code += code if (context.map) { if (node) { context.map.addMapping({ @@ -103,30 +94,113 @@ function createCodegenContext( } }) } - advancePositionWithMutation( - context, - generatedCode, - generatedCode.length - ) + advancePositionWithMutation(context, code, code.length) } + }, + indent() { + newline(++context.indentLevel) + }, + deindent() { + newline(--context.indentLevel) + }, + newline() { + newline(context.indentLevel) } } + const newline = (n: number) => context.push('\n' + ` `.repeat(n)) if (!__BROWSER__) { context.map!.setSourceContent(filename, context.source) } 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(', ') +export function generate( + ast: RootNode, + options: CodegenOptions = {} +): CodegenResult { + const context = createCodegenContext(ast, options) + // TODO handle different output for module mode and IIFE mode + const { mode, push, useWith, indent, deindent } = context + if (mode === 'function') { + // TODO generate const declarations for helpers + push(`return `) + } else { + // TODO generate import statements for helpers + push(`export default `) } + push(`function render() {`) + if (useWith) { + indent() + push(`with (this) {`) + } + indent() + push(`return `) + genChildren(ast.children, context) + if (useWith) { + deindent() + push(`}`) + } + deindent() + push(`}`) + return { + code: context.code, + map: context.map ? context.map.toJSON() : undefined + } +} + +// This will generate a single vnode call if the list has length === 1. +function genChildren(children: ChildNode[], context: CodegenContext) { + if (children.length === 1) { + genNode(children[0], context) + } else { + genNodeListAsArray(children, context) + } +} + +function genNodeListAsArray( + nodes: (string | CodegenNode | ChildNode[])[], + context: CodegenContext +) { + const multilines = nodes.length > 1 + context.push(`[`) + multilines && context.indent() + genNodeList(nodes, context, multilines) + multilines && context.deindent() context.push(`]`) } -function genNode(node: ChildNode, context: CodegenContext) { +function genNodeList( + nodes: (string | CodegenNode | ChildNode[])[], + context: CodegenContext, + multilines: boolean = false +) { + const { push, newline } = context + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i] + if (isString(node)) { + // plain code string + // note not adding quotes here because this can be any code, + // not just plain strings. + push(node) + } else if (isArray(node)) { + // child VNodes in a h() call + // not using genChildren here because we want them to always be an array + genNodeListAsArray(node, context) + } else { + genNode(node, context) + } + if (i < nodes.length - 1) { + if (multilines) { + push(',') + newline() + } else { + push(', ') + } + } + } +} + +function genNode(node: CodegenNode, context: CodegenContext) { switch (node.type) { case NodeTypes.ELEMENT: genElement(node, context) @@ -146,20 +220,55 @@ function genNode(node: ChildNode, context: CodegenContext) { case NodeTypes.FOR: genFor(node, context) break + case NodeTypes.JS_CALL_EXPRESSION: + genCallExpression(node, context) + break + case NodeTypes.JS_OBJECT_EXPRESSION: + genObjectExpression(node, context) + break + case NodeTypes.JS_ARRAY_EXPRESSION: + genArrayExpression(node, context) } } -function genElement(node: ElementNode, context: CodegenContext) {} +function genElement(node: ElementNode, context: CodegenContext) { + __DEV__ && + assert( + node.codegenNode != null, + `AST is not transformed for codegen. ` + + `Apply appropriate transforms first.` + ) + genCallExpression(node.codegenNode!, context, false) +} 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 + // if (node.codegenNode) { + // TODO handle transformed expression + // } + const text = node.isStatic ? JSON.stringify(node.content) : node.content + context.push(text, node) +} + +function genExpressionAsPropertyKey( + node: ExpressionNode, + context: CodegenContext +) { + // if (node.codegenNode) { + // TODO handle transformed expression + // } + if (node.isStatic) { + // only quote keys if necessary + const text = /^\d|[^\w]/.test(node.content) + ? JSON.stringify(node.content) + : node.content + context.push(text, node) + } else { + context.push(`[${node.content}]`, node) } - context.push(node.content, node) } function genComment(node: CommentNode, context: CodegenContext) { @@ -167,6 +276,107 @@ function genComment(node: CommentNode, context: CodegenContext) { } // control flow -function genIf(node: IfNode, context: CodegenContext) {} +function genIf(node: IfNode, context: CodegenContext) { + genIfBranch(node.branches[0], node.branches, 1, context) +} -function genFor(node: ForNode, context: CodegenContext) {} +function genIfBranch( + { condition, children }: IfBranchNode, + branches: IfBranchNode[], + nextIndex: number, + context: CodegenContext +) { + if (condition) { + // v-if or v-else-if + context.push(`(${condition.content})`, condition) + context.push(`?`) + genChildren(children, context) + context.push(`:`) + if (nextIndex < branches.length) { + genIfBranch(branches[nextIndex], branches, nextIndex + 1, context) + } else { + context.push(`null`) + } + } else { + // v-else + __DEV__ && assert(nextIndex === branches.length) + genChildren(children, context) + } +} + +function genFor(node: ForNode, context: CodegenContext) { + const { push } = context + const { source, keyAlias, valueAlias, objectIndexAlias, children } = node + push(`${RENDER_LIST_HELPER}(`, node) + genExpression(source, context) + context.push(`(`) + if (valueAlias) { + // not using genExpression here because these aliases can only be code + // that is valid in the function argument position, so the parse rule can + // be off and they don't need identifier prefixing anyway. + push(valueAlias.content, valueAlias) + push(`, `) + } + if (keyAlias) { + if (!valueAlias) { + push(`_, `) + } + push(keyAlias.content, keyAlias) + push(`, `) + } + if (objectIndexAlias) { + if (!keyAlias) { + if (!valueAlias) { + push(`_, `) + } + push(`_, `) + } + push(objectIndexAlias.content, objectIndexAlias) + } + context.push(`) => `) + genChildren(children, context) + context.push(`)`) +} + +// JavaScript +function genCallExpression( + node: CallExpression, + context: CodegenContext, + multilines = node.arguments.length > 1 +) { + context.push(node.callee + `(`, node) + multilines && context.indent() + genNodeList(node.arguments, context, multilines) + multilines && context.deindent() + context.push(`)`) +} + +function genObjectExpression(node: ObjectExpression, context: CodegenContext) { + const { push, indent, deindent, newline } = context + const { properties } = node + const multilines = properties.length > 1 + push(`{`, node) + multilines && indent() + for (let i = 0; i < properties.length; i++) { + const { key, value } = properties[i] + // key + genExpressionAsPropertyKey(key, context) + push(`: `) + // value + genExpression(value, context) + if (i < properties.length - 1) { + if (multilines) { + push(`,`) + newline() + } else { + push(`, `) + } + } + } + multilines && deindent() + push(`}`) +} + +function genArrayExpression(node: ArrayExpression, context: CodegenContext) { + genNodeListAsArray(node.elements, context) +} diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 62fc7317..fb91b332 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -3,6 +3,9 @@ import { transform, TransformOptions } from './transform' import { generate, CodegenOptions, CodegenResult } from './codegen' import { RootNode } from './ast' import { isString } from '@vue/shared' +import { transformIf } from './transforms/vIf' +import { transformFor } from './transforms/vFor' +import { prepareElementForCodegen } from './transforms/element' export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions @@ -15,7 +18,9 @@ export function compile( transform(ast, { ...options, nodeTransforms: [ - // TODO include built-in core transforms + transformIf, + transformFor, + prepareElementForCodegen, ...(options.nodeTransforms || []) // user transforms ], directiveTransforms: { diff --git a/packages/compiler-core/src/transforms/element.ts b/packages/compiler-core/src/transforms/element.ts index 780b7729..066874c3 100644 --- a/packages/compiler-core/src/transforms/element.ts +++ b/packages/compiler-core/src/transforms/element.ts @@ -24,11 +24,16 @@ export const prepareElementForCodegen: NodeTransform = (node, context) => { node.tagType === ElementTypes.ELEMENT || node.tagType === ElementTypes.COMPONENT ) { - const isComponent = node.tagType === ElementTypes.ELEMENT + const isComponent = node.tagType === ElementTypes.COMPONENT const hasProps = node.props.length > 0 const hasChildren = node.children.length > 0 let runtimeDirectives: DirectiveNode[] | undefined + if (isComponent) { + // TODO inject import for `resolveComponent` + // TODO inject statement for resolving component + } + const args: CallExpression['arguments'] = [ // TODO inject resolveComponent dep to root isComponent ? node.tag : `"${node.tag}"` @@ -49,9 +54,11 @@ export const prepareElementForCodegen: NodeTransform = (node, context) => { } const { loc } = node + // TODO inject import for `h` const vnode = createCallExpression(`h`, args, loc) - if (runtimeDirectives) { + if (runtimeDirectives && runtimeDirectives.length) { + // TODO inject import for `applyDirectives` node.codegenNode = createCallExpression( `applyDirectives`, [ @@ -170,7 +177,8 @@ function createDirectiveArgs( dir: DirectiveNode, context: TransformContext ): ArrayExpression { - // TODO inject resolveDirective dep to root + // TODO inject import for `resolveDirective` + // TODO inject statement for resolving directive const dirArgs: ArrayExpression['elements'] = [dir.name] const { loc } = dir if (dir.exp) dirArgs.push(dir.exp) diff --git a/packages/compiler-core/src/transforms/expression.ts b/packages/compiler-core/src/transforms/expression.ts new file mode 100644 index 00000000..ba13f11f --- /dev/null +++ b/packages/compiler-core/src/transforms/expression.ts @@ -0,0 +1,9 @@ +// - Parse expressions in templates into more detailed JavaScript ASTs so that +// source-maps are more accurate +// +// - Prefix identifiers with `_ctx.` so that they are accessed from the render +// context +// +// - This transform is only applied in non-browser builds because it relies on +// an additional JavaScript parser. In the browser, there is no source-map +// support and the code is wrapped in `with (this) { ... }`. diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts index 8b8e79e2..8e25ab09 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -7,13 +7,17 @@ const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/ const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const stripParensRE = /^\(|\)$/g +export const RENDER_LIST_HELPER = `renderList` + export const transformFor = createStructuralDirectiveTransform( 'for', (node, dir, context) => { if (dir.exp) { + // TODO inject helper import const aliases = parseAliasExpressions(dir.exp.content) if (aliases) { + // TODO inject identifiers to context context.replaceNode({ type: NodeTypes.FOR, loc: node.loc, diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index 49609ac1..2a93612b 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -61,6 +61,6 @@ export function advancePositionWithMutation( export function assert(condition: boolean, msg?: string) { if (!condition) { - throw new Error(msg || `unexpected parser condition`) + throw new Error(msg || `unexpected compiler condition`) } } diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts index 6c09e0bc..3ead2be7 100644 --- a/packages/compiler-dom/src/index.ts +++ b/packages/compiler-dom/src/index.ts @@ -6,8 +6,6 @@ import { import { parserOptionsMinimal } from './parserOptionsMinimal' import { parserOptionsStandard } from './parserOptionsStandard' -export * from '@vue/compiler-core' - export function compile( template: string, options: CompilerOptions = {} @@ -21,3 +19,5 @@ export function compile( } }) } + +export * from '@vue/compiler-core' diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 60c2033a..f9728088 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -1,7 +1,6 @@ import { ComponentInternalInstance, Data, - currentInstance, Component, SetupContext } from './component' @@ -11,9 +10,7 @@ import { isString, isObject, isArray, - EMPTY_OBJ, - capitalize, - camelize + EMPTY_OBJ } from '@vue/shared' import { computed } from './apiReactivity' import { watch, WatchOptions, CleanupRegistrator } from './apiWatch' @@ -29,12 +26,10 @@ import { onUnmounted } from './apiLifecycle' import { DebuggerEvent, reactive } from '@vue/reactivity' -import { warn } from './warning' import { ComponentPropsOptions, ExtractPropTypes } from './componentProps' import { Directive } from './directives' import { VNodeChild } from './vnode' import { ComponentPublicInstance } from './componentPublicInstanceProxy' -import { currentRenderingInstance } from './componentRenderUtils' interface ComponentOptionsBase< Props, @@ -387,32 +382,3 @@ function applyMixins( applyOptions(instance, mixins[i], true) } } - -export function resolveComponent(name: string): Component | undefined { - return resolveAsset('components', name) as any -} - -export function resolveDirective(name: string): Directive | undefined { - return resolveAsset('directives', name) as any -} - -function resolveAsset(type: 'components' | 'directives', name: string) { - const instance = currentRenderingInstance || currentInstance - if (instance) { - let camelized - const registry = instance[type] - const res = - registry[name] || - registry[(camelized = camelize(name))] || - registry[capitalize(camelized)] - if (__DEV__ && !res) { - warn(`Failed to resolve ${type.slice(0, -1)}: ${name}`) - } - return res - } else if (__DEV__) { - warn( - `resolve${capitalize(type.slice(0, -1))} ` + - `can only be used in render() or setup().` - ) - } -} diff --git a/packages/runtime-core/src/componentPublicInstanceProxy.ts b/packages/runtime-core/src/componentPublicInstanceProxy.ts index 6494028e..e0555e5d 100644 --- a/packages/runtime-core/src/componentPublicInstanceProxy.ts +++ b/packages/runtime-core/src/componentPublicInstanceProxy.ts @@ -40,6 +40,7 @@ export const PublicInstanceProxyHandlers = { // return the value from propsProxy for ref unwrapping and readonly return (propsProxy as any)[key] } else { + // TODO simplify this? switch (key) { case '$data': return data @@ -79,6 +80,7 @@ export const PublicInstanceProxyHandlers = { }, has(target: ComponentInternalInstance, key: string): boolean { const { renderContext, data, props } = target + // TODO handle $xxx properties return ( (data !== EMPTY_OBJ && hasOwn(data, key)) || hasOwn(renderContext, key) || diff --git a/packages/runtime-core/src/helpers/renderList.ts b/packages/runtime-core/src/helpers/renderList.ts new file mode 100644 index 00000000..7a715be2 --- /dev/null +++ b/packages/runtime-core/src/helpers/renderList.ts @@ -0,0 +1,2 @@ +// TODO +export function renderList() {} diff --git a/packages/runtime-core/src/helpers/resolveAssets.ts b/packages/runtime-core/src/helpers/resolveAssets.ts new file mode 100644 index 00000000..fd6fdd5d --- /dev/null +++ b/packages/runtime-core/src/helpers/resolveAssets.ts @@ -0,0 +1,34 @@ +import { currentRenderingInstance } from '../componentRenderUtils' +import { currentInstance, Component } from '../component' +import { Directive } from '../directives' +import { camelize, capitalize } from '@vue/shared' +import { warn } from '../warning' + +export function resolveComponent(name: string): Component | undefined { + return resolveAsset('components', name) as any +} + +export function resolveDirective(name: string): Directive | undefined { + return resolveAsset('directives', name) as any +} + +function resolveAsset(type: 'components' | 'directives', name: string) { + const instance = currentRenderingInstance || currentInstance + if (instance) { + let camelized + const registry = instance[type] + const res = + registry[name] || + registry[(camelized = camelize(name))] || + registry[capitalize(camelized)] + if (__DEV__ && !res) { + warn(`Failed to resolve ${type.slice(0, -1)}: ${name}`) + } + return res + } else if (__DEV__) { + warn( + `resolve${capitalize(type.slice(0, -1))} ` + + `can only be used in render() or setup().` + ) + } +} diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 006ecf64..15c66f85 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -37,7 +37,8 @@ export { // Internal, for compiler generated code export { applyDirectives } from './directives' -export { resolveComponent, resolveDirective } from './componentOptions' +export { resolveComponent, resolveDirective } from './helpers/resolveAssets' +export { renderList } from './helpers/renderList' // Internal, for integration with runtime compiler export { registerRuntimeCompiler } from './component' diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 1c89caba..e2baaaf6 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -8,7 +8,7 @@ function compileToFunction( options?: CompilerOptions ): RenderFunction { const { code } = compile(template, options) - return new Function(`with(this){return ${code}}`) as RenderFunction + return new Function(code)() as RenderFunction } registerRuntimeCompiler(compileToFunction)