diff --git a/packages/compiler-core/__tests__/transforms/expression.spec.ts b/packages/compiler-core/__tests__/transforms/expression.spec.ts new file mode 100644 index 00000000..2153fd08 --- /dev/null +++ b/packages/compiler-core/__tests__/transforms/expression.spec.ts @@ -0,0 +1,19 @@ +import { SourceMapConsumer } from 'source-map' +import { compile } from '../../src' + +test(`should work`, async () => { + const { code, map } = compile( + `
{{ ({ a }, b) => a + b + c }}
`, + { + useWith: false + } + ) + console.log(code) + console.log(map) + const consumer = await new SourceMapConsumer(map!) + const pos = consumer.originalPositionFor({ + line: 4, + column: 31 + }) + console.log(pos) +}) diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index db71ca54..bfefbbae 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -10,7 +10,9 @@ ], "types": "dist/compiler-core.d.ts", "buildOptions": { - "formats": ["cjs"] + "formats": [ + "cjs" + ] }, "repository": { "type": "git", @@ -26,6 +28,8 @@ }, "homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-core#readme", "dependencies": { + "estree-walker": "^0.8.1", + "meriyah": "^1.7.2", "source-map": "^0.7.3" } } diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 897eaafa..1c5e87a5 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -16,6 +16,7 @@ export const enum NodeTypes { ATTRIBUTE, DIRECTIVE, // containers + COMPOUND_EXPRESSION, IF, IF_BRANCH, FOR, @@ -109,6 +110,7 @@ export interface ExpressionNode extends Node { type: NodeTypes.EXPRESSION content: string isStatic: boolean + children?: (ExpressionNode | string)[] } export interface IfNode extends Node { diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 3b2d1f32..820b1d97 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -119,7 +119,7 @@ export function generate( options: CodegenOptions = {} ): CodegenResult { const context = createCodegenContext(ast, options) - const { mode, push, useWith, indent, deindent } = context + const { mode, push, useWith, indent, deindent, newline } = context const imports = ast.imports.join(', ') if (mode === 'function') { // generate const declarations for helpers @@ -135,13 +135,19 @@ export function generate( push(`export default `) } push(`function render() {`) - // generate asset resolution statements - ast.statements.forEach(s => push(s + `\n`)) - if (useWith) { - indent() - push(`with (this) {`) - } indent() + // generate asset resolution statements + if (ast.statements.length) { + ast.statements.forEach(s => { + push(s) + newline() + }) + newline() + } + if (useWith) { + push(`with (this) {`) + indent() + } push(`return `) genChildren(ast.children, context) if (useWith) { @@ -236,6 +242,10 @@ function genNode(node: CodegenNode, context: CodegenContext) { break case NodeTypes.JS_ARRAY_EXPRESSION: genArrayExpression(node, context) + break + default: + __DEV__ && + assert(false, `unhandled codegen node type: ${(node as any).type}`) } } @@ -254,9 +264,9 @@ function genText(node: TextNode | ExpressionNode, context: CodegenContext) { } function genExpression(node: ExpressionNode, context: CodegenContext) { - // if (node.codegenNode) { - // TODO handle transformed expression - // } + if (node.children) { + return genCompoundExpression(node, context) + } const text = node.isStatic ? JSON.stringify(node.content) : node.content context.push(text, node) } @@ -265,9 +275,9 @@ function genExpressionAsPropertyKey( node: ExpressionNode, context: CodegenContext ) { - // if (node.codegenNode) { - // TODO handle transformed expression - // } + if (node.children) { + return genCompoundExpression(node, context) + } if (node.isStatic) { // only quote keys if necessary const text = /^\d|[^\w]/.test(node.content) @@ -279,6 +289,17 @@ function genExpressionAsPropertyKey( } } +function genCompoundExpression(node: ExpressionNode, context: CodegenContext) { + for (let i = 0; i < node.children!.length; i++) { + const child = node.children![i] + if (isString(child)) { + context.push(child) + } else { + genExpression(child, context) + } + } +} + function genComment(node: CommentNode, context: CodegenContext) { context.push(``, node) } diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 2409c920..5af9df19 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -8,6 +8,7 @@ import { transformFor } from './transforms/vFor' import { prepareElementForCodegen } from './transforms/element' import { transformOn } from './transforms/vOn' import { transformBind } from './transforms/vBind' +import { rewriteExpression } from './transforms/expression' export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions @@ -22,6 +23,7 @@ export function compile( nodeTransforms: [ transformIf, transformFor, + ...(!__BROWSER__ && options.useWith === false ? [rewriteExpression] : []), prepareElementForCodegen, ...(options.nodeTransforms || []) // user transforms ], @@ -31,7 +33,6 @@ export function compile( ...(options.directiveTransforms || {}) // user transforms } }) - return generate(ast, options) } diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 3cf26ecd..d3b442b3 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -79,7 +79,7 @@ function createTransformContext( removeNode(node) { const list = context.parent.children const removalIndex = node - ? list.indexOf(node) + ? list.indexOf(node as any) : context.currentNode ? context.childIndex : -1 @@ -124,12 +124,15 @@ export function traverseChildren( i-- } for (; i < parent.children.length; i++) { + const child = parent.children[i] + if (isString(child)) continue + context.currentNode = child context.parent = parent context.ancestors = ancestors context.childIndex = i context.onNodeRemoved = nodeRemoved context.identifiers = identifiers - traverseNode((context.currentNode = parent.children[i]), context) + traverseNode(child, context) } } diff --git a/packages/compiler-core/src/transforms/element.ts b/packages/compiler-core/src/transforms/element.ts index 3844f4e0..115d9a02 100644 --- a/packages/compiler-core/src/transforms/element.ts +++ b/packages/compiler-core/src/transforms/element.ts @@ -192,9 +192,7 @@ function createDirectiveArgs( // inject statement for resolving directive const dirIdentifier = `_directive_${toValidId(dir.name)}` context.statements.push( - `const ${dirIdentifier} = _${RESOLVE_DIRECTIVE}(${JSON.stringify( - dir.name - )})` + `const ${dirIdentifier} = ${RESOLVE_DIRECTIVE}(${JSON.stringify(dir.name)})` ) const dirArgs: ArrayExpression['elements'] = [dirIdentifier] const { loc } = dir diff --git a/packages/compiler-core/src/transforms/expression.ts b/packages/compiler-core/src/transforms/expression.ts index ba13f11f..943cb5ac 100644 --- a/packages/compiler-core/src/transforms/expression.ts +++ b/packages/compiler-core/src/transforms/expression.ts @@ -1,5 +1,5 @@ -// - Parse expressions in templates into more detailed JavaScript ASTs so that -// source-maps are more accurate +// - Parse expressions in templates into compound expressions so that each +// identifier gets more accurate source-map locations. // // - Prefix identifiers with `_ctx.` so that they are accessed from the render // context @@ -7,3 +7,146 @@ // - 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) { ... }`. + +import { parseScript } from 'meriyah' +import { walk } from 'estree-walker' +import { NodeTransform, TransformContext } from '../transform' +import { NodeTypes, createExpression, ExpressionNode } from '../ast' +import { Node, Function, Identifier } from 'estree' +import { advancePositionWithClone } from '../utils' + +export const rewriteExpression: NodeTransform = (node, context) => { + if (node.type === NodeTypes.EXPRESSION && !node.isStatic) { + context.replaceNode(convertExpression(node, context)) + } else if (node.type === NodeTypes.ELEMENT) { + // handle directives on element + for (let i = 0; i < node.props.length; i++) { + const prop = node.props[i] + if (prop.type === NodeTypes.DIRECTIVE) { + if (prop.exp) { + prop.exp = convertExpression(prop.exp, context) + } + if (prop.arg && !prop.arg.isStatic) { + prop.arg = convertExpression(prop.arg, context) + } + } + } + } else if (node.type === NodeTypes.IF) { + for (let i = 0; i < node.branches.length; i++) {} + } else if (node.type === NodeTypes.FOR) { + } +} + +function convertExpression( + node: ExpressionNode, + context: TransformContext +): ExpressionNode { + const ast = parseScript(`(${node.content})`, { ranges: true }) as any + const ids: Node[] = [] + const knownIds = Object.create(context.identifiers) + + walk(ast, { + enter(node, parent) { + if (node.type === 'Identifier') { + if (ids.indexOf(node) === -1) { + ids.push(node) + if (!knownIds[node.name] && shouldPrependContext(node, parent)) { + node.name = `_ctx.${node.name}` + } + } + } else if (isFunction(node)) { + node.params.forEach(p => + walk(p, { + enter(child) { + if (child.type === 'Identifier') { + knownIds[child.name] = true + ;( + (node as any)._scopeIds || + ((node as any)._scopeIds = new Set()) + ).add(child.name) + } + } + }) + ) + } + }, + leave(node: any) { + if (node._scopeIds) { + node._scopeIds.forEach((id: string) => { + delete knownIds[id] + }) + } + } + }) + + const full = node.content + const children: ExpressionNode['children'] = [] + ids.sort((a: any, b: any) => a.start - b.start) + ids.forEach((id: any, i) => { + const last = ids[i - 1] as any + const text = full.slice(last ? last.end - 1 : 0, id.start - 1) + if (text.length) { + children.push(text) + } + const source = full.slice(id.start, id.end) + children.push( + createExpression(id.name, false, { + source, + start: advancePositionWithClone(node.loc.start, source, id.start), + end: advancePositionWithClone(node.loc.start, source, id.end) + }) + ) + if (i === ids.length - 1 && id.end < full.length - 1) { + children.push(full.slice(id.end)) + } + }) + + return { + type: NodeTypes.EXPRESSION, + content: '', + isStatic: false, + loc: node.loc, + children + } +} + +const globals = new Set( + ( + 'Infinity,undefined,NaN,isFinite,isNaN,' + + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + + 'require,' + // for webpack + 'arguments,' + ) // parsed as identifier but is a special keyword... + .split(',') +) + +const isFunction = (node: Node): node is Function => + /Function(Expression|Declaration)$/.test(node.type) + +function shouldPrependContext(identifier: Identifier, parent: Node) { + if ( + // not id of a FunctionDeclaration + !(parent.type === 'FunctionDeclaration' && parent.id === identifier) && + // not a params of a function + !(isFunction(parent) && parent.params.indexOf(identifier) > -1) && + // not a key of Property + !( + parent.type === 'Property' && + parent.key === identifier && + !parent.computed + ) && + // not a property of a MemberExpression + !( + parent.type === 'MemberExpression' && + parent.property === identifier && + !parent.computed + ) && + // not in an Array destructure pattern + !(parent.type === 'ArrayPattern') && + // skip globals + commonly used shorthands + !globals.has(identifier.name) + ) { + return true + } +} diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts index 69b5db1e..bcb0203d 100644 --- a/packages/compiler-core/src/transforms/vIf.ts +++ b/packages/compiler-core/src/transforms/vIf.ts @@ -24,7 +24,7 @@ export const transformIf = createStructuralDirectiveTransform( // locate the adjacent v-if const siblings = context.parent.children const comments = [] - let i = siblings.indexOf(node) + let i = siblings.indexOf(node as any) while (i-- >= -1) { const sibling = siblings[i] if (__DEV__ && sibling && sibling.type === NodeTypes.COMMENT) { diff --git a/yarn.lock b/yarn.lock index 0cfeed06..6e3e1b3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2627,6 +2627,11 @@ estree-walker@^0.6.1: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== +estree-walker@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.8.1.tgz#6230ce2ec9a5cb03888afcaf295f97d90aa52b79" + integrity sha512-H6cJORkqvrNziu0KX2hqOMAlA2CiuAxHeGJXSIoKA/KLv229Dw806J3II6mKTm5xiDX1At1EXCfsOQPB+tMB+g== + esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -4785,6 +4790,11 @@ merge2@^1.2.3: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.4.tgz#c9269589e6885a60cf80605d9522d4b67ca646e3" integrity sha512-FYE8xI+6pjFOhokZu0We3S5NKCirLbCzSh2Usf3qEyr4X8U+0jNg9P8RZ4qz+V2UoECLVwSyzU3LxXBaLGtD3A== +meriyah@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/meriyah/-/meriyah-1.7.2.tgz#c47d07d8f1284577658827cd134b180e80ae4bef" + integrity sha512-bBXN6hJ9RHA0mEae5O2Ocr6giK0S87nsz/W7tnBRm4kpW04LEEpXSOfwaID9GZgPRVcn3rAHzHHDDD68DLQgWw== + micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"