diff --git a/package.json b/package.json index a57d6cdedf..361cee6b23 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "eslint": "^4.3.0", "eslint-plugin-html": "^3.0.0", "eslint-plugin-import": "^2.2.0", - "estree-walker": "^0.5.0", + "estree-walker": "^0.5.1", "glob": "^7.1.1", "jsdom": "^11.1.0", "locate-character": "^2.0.0", diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 8f2dad24e4..c57cee27c3 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -1,17 +1,16 @@ import MagicString, { Bundle } from 'magic-string'; -import { walk } from 'estree-walker'; +import { walk, childKeys } from 'estree-walker'; import { getLocator } from 'locate-character'; import deindent from '../utils/deindent'; import CodeBuilder from '../utils/CodeBuilder'; import getCodeFrame from '../utils/getCodeFrame'; import isReference from '../utils/isReference'; import flattenReference from '../utils/flattenReference'; -import globalWhitelist from '../utils/globalWhitelist'; import reservedNames from '../utils/reservedNames'; import namespaces from '../utils/namespaces'; import { removeNode, removeObjectKey } from '../utils/removeNode'; import wrapModule from './shared/utils/wrapModule'; -import annotateWithScopes from '../utils/annotateWithScopes'; +import annotateWithScopes, { Scope } from '../utils/annotateWithScopes'; import getName from '../utils/getName'; import clone from '../utils/clone'; import DomBlock from './dom/Block'; @@ -71,6 +70,19 @@ function removeIndentation( } } +// We need to tell estree-walker that it should always +// look for an `else` block, otherwise it might get +// the wrong idea about the shape of each/if blocks +childKeys.EachBlock = [ + 'children', + 'else' +]; + +childKeys.IfBlock = [ + 'children', + 'else' +]; + export default class Generator { ast: Parsed; parsed: Parsed; @@ -156,7 +168,12 @@ export default class Generator { this.aliases = new Map(); this.usedNames = new Set(); - this.parseJs(dom); + this.computations = []; + this.templateProperties = {}; + + this.walkJs(dom); + this.walkTemplate(); + this.name = this.alias(name); if (options.customElement === true) { @@ -196,12 +213,10 @@ export default class Generator { context: string, isEventHandler: boolean ): { - dependencies: string[], contexts: Set, - indexes: Set, - snippet: string + indexes: Set } { - this.addSourcemapLocations(expression); + // this.addSourcemapLocations(expression); const usedContexts: Set = new Set(); const usedIndexes: Set = new Set(); @@ -209,7 +224,7 @@ export default class Generator { const { code, helpers } = this; const { contexts, indexes } = block; - let scope = annotateWithScopes(expression); // TODO this already happens in findDependencies + let scope: Scope; let lexicalDepth = 0; const self = this; @@ -231,7 +246,7 @@ export default class Generator { }); } else if (isReference(node, parent)) { const { name } = flattenReference(node); - if (scope.has(name)) return; + if (scope && scope.has(name)) return; if (name === 'event' && isEventHandler) { // noop @@ -267,16 +282,7 @@ export default class Generator { } } - if (globalWhitelist.has(name)) { - code.prependRight(node.start, `('${name}' in state ? state.`); - code.appendLeft( - node.object ? node.object.end : node.end, - ` : ${name})` - ); - } else { - code.prependRight(node.start, `state.`); - } - + code.prependRight(node.start, `state.`); usedContexts.add('state'); } @@ -290,73 +296,12 @@ export default class Generator { }, }); - const dependencies: Set = new Set(expression._dependencies || []); - - if (expression._dependencies) { - expression._dependencies.forEach((prop: string) => { - if (this.indirectDependencies.has(prop)) { - this.indirectDependencies.get(prop).forEach(dependency => { - dependencies.add(dependency); - }); - } - }); - } - return { - dependencies: Array.from(dependencies), contexts: usedContexts, - indexes: usedIndexes, - snippet: `[✂${expression.start}-${expression.end}✂]`, + indexes: usedIndexes }; } - findDependencies( - contextDependencies: Map, - indexes: Map, - expression: Node - ) { - if (expression._dependencies) return expression._dependencies; - - let scope = annotateWithScopes(expression); - const dependencies: string[] = []; - - const generator = this; // can't use arrow functions, because of this.skip() - - walk(expression, { - enter(node: Node, parent: Node) { - if (node._scope) { - scope = node._scope; - return; - } - - if (isReference(node, parent)) { - const { name } = flattenReference(node); - if (scope.has(name) || generator.helpers.has(name)) return; - - if (contextDependencies.has(name)) { - dependencies.push(...contextDependencies.get(name)); - } else if (!indexes.has(name)) { - dependencies.push(name); - } - - this.skip(); - } - }, - - leave(node: Node) { - if (node._scope) scope = scope.parent; - }, - }); - - dependencies.forEach(name => { - if (!globalWhitelist.has(name)) { - this.expectedProperties.add(name); - } - }); - - return (expression._dependencies = dependencies); - } - generate(result: string, options: CompileOptions, { banner = '', sharedPath, helpers, name, format }: GenerateOptions ) { const pattern = /\[✂(\d+)-(\d+)$/; @@ -454,17 +399,19 @@ export default class Generator { }; } - parseJs(dom: boolean) { - const { code, source } = this; + walkJs(dom: boolean) { + const { + code, + source, + computations, + templateProperties, + imports + } = this; + const { js } = this.parsed; - const imports = this.imports; - const computations: Computation[] = []; - const templateProperties: Record = {}; const componentDefinition = new CodeBuilder(); - let namespace = null; - if (js) { this.addSourcemapLocations(js.content); @@ -637,7 +584,7 @@ export default class Generator { if (templateProperties.namespace) { const ns = templateProperties.namespace.value.value; - namespace = namespaces[ns] || ns; + this.namespace = namespaces[ns] || ns; } if (templateProperties.onrender) templateProperties.oncreate = templateProperties.onrender; // remove after v2 @@ -693,9 +640,132 @@ export default class Generator { this.javascript = a === b ? null : `[✂${a}-${b}✂]`; } } + } + + walkTemplate() { + const { + code, + expectedProperties, + helpers + } = this; + const { html } = this.parsed; + + const contextualise = (node: Node, contextDependencies: Map, indexes: Set) => { + this.addSourcemapLocations(node); // TODO this involves an additional walk — can we roll it in somewhere else? + let scope = annotateWithScopes(node); + + const dependencies: Set = new Set(); + + walk(node, { + enter(node: Node, parent: Node) { + code.addSourcemapLocation(node.start); + code.addSourcemapLocation(node.end); + + if (node._scope) { + scope = node._scope; + return; + } + + if (isReference(node, parent)) { + const { name } = flattenReference(node); + if (scope && scope.has(name) || helpers.has(name)) return; + + if (contextDependencies.has(name)) { + contextDependencies.get(name).forEach(dependency => { + dependencies.add(dependency); + }); + } else if (!indexes.has(name)) { + dependencies.add(name); + } + + this.skip(); + } + }, + + leave(node: Node, parent: Node) { + if (node._scope) scope = scope.parent; + } + }); + + dependencies.forEach(dependency => { + expectedProperties.add(dependency); + }); + + return { + snippet: `[✂${node.start}-${node.end}✂]`, + dependencies: Array.from(dependencies) + }; + } + + const contextStack = []; + const indexStack = []; + const dependenciesStack = []; + + let contextDependencies = new Map(); + const contextDependenciesStack: Map[] = [contextDependencies]; + + let indexes = new Set(); + const indexesStack: Set[] = [indexes]; + + walk(html, { + enter(node: Node, parent: Node) { + if (node.type === 'EachBlock') { + node.metadata = contextualise(node.expression, contextDependencies, indexes); + + contextDependencies = new Map(contextDependencies); + contextDependencies.set(node.context, node.metadata.dependencies); + + if (node.destructuredContexts) { + for (let i = 0; i < node.destructuredContexts.length; i += 1) { + const name = node.destructuredContexts[i]; + const value = `${node.context}[${i}]`; + + contextDependencies.set(name, node.metadata.dependencies); + } + } + + contextDependenciesStack.push(contextDependencies); + + if (node.index) { + indexes = new Set(indexes); + indexes.add(node.index); + indexesStack.push(indexes); + } + } + + if (node.type === 'IfBlock') { + node.metadata = contextualise(node.expression, contextDependencies, indexes); + } + + if (node.type === 'MustacheTag' || node.type === 'RawMustacheTag' || node.type === 'AttributeShorthand') { + node.metadata = contextualise(node.expression, contextDependencies, indexes); + this.skip(); + } + + if (node.type === 'Binding') { + node.metadata = contextualise(node.value, contextDependencies, indexes); + this.skip(); + } + + if (node.type === 'EventHandler' && node.expression) { + node.expression.arguments.forEach((arg: Node) => { + arg.metadata = contextualise(arg, contextDependencies, indexes); + }); + this.skip(); + } + }, + + leave(node: Node, parent: Node) { + if (node.type === 'EachBlock') { + contextDependenciesStack.pop(); + contextDependencies = contextDependenciesStack[contextDependenciesStack.length - 1]; - this.computations = computations; - this.namespace = namespace; - this.templateProperties = templateProperties; + if (node.index) { + indexesStack.pop(); + indexes = indexesStack[indexesStack.length - 1]; + } + } + } + }); } } diff --git a/src/generators/dom/Block.ts b/src/generators/dom/Block.ts index 53ae519465..e2d3ed7a55 100644 --- a/src/generators/dom/Block.ts +++ b/src/generators/dom/Block.ts @@ -10,12 +10,12 @@ export interface BlockOptions { generator?: DomGenerator; expression?: Node; context?: string; + destructuredContexts?: string[]; comment?: string; key?: string; contexts?: Map; indexes?: Map; changeableIndexes?: Map; - contextDependencies?: Map; params?: string[]; indexNames?: Map; listNames?: Map; @@ -38,7 +38,6 @@ export default class Block { contexts: Map; indexes: Map; changeableIndexes: Map; - contextDependencies: Map; dependencies: Set; params: string[]; indexNames: Map; @@ -86,7 +85,6 @@ export default class Block { this.contexts = options.contexts; this.indexes = options.indexes; this.changeableIndexes = options.changeableIndexes; - this.contextDependencies = options.contextDependencies; this.dependencies = new Set(); this.params = options.params; @@ -176,14 +174,6 @@ export default class Block { ); } - findDependencies(expression: Node) { - return this.generator.findDependencies( - this.contextDependencies, - this.indexes, - expression - ); - } - mount(name: string, parentNode: string) { if (parentNode) { this.builders.mount.addLine(`@appendNode(${name}, ${parentNode});`); diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index 7c7c4a45dc..1cbf6132cf 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -6,6 +6,7 @@ import { walk } from 'estree-walker'; import deindent from '../../utils/deindent'; import { stringify, escape } from '../../utils/stringify'; import CodeBuilder from '../../utils/CodeBuilder'; +import globalWhitelist from '../../utils/globalWhitelist'; import reservedNames from '../../utils/reservedNames'; import visit from './visit'; import shared from './shared'; @@ -184,13 +185,28 @@ export default function dom( const debugName = `<${generator.customElement ? generator.tag : name}>`; + // generate initial state object + const globals = Array.from(generator.expectedProperties).filter(prop => globalWhitelist.has(prop)); + const initialState = []; + if (globals.length > 0) { + initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`); + } + + if (templateProperties.data) { + initialState.push(`%data()`); + } else if (globals.length === 0) { + initialState.push('{}'); + } + + initialState.push(`options.data`); + const constructorBody = deindent` ${options.dev && `this._debugName = '${debugName}';`} ${options.dev && !generator.customElement && `if (!options || (!options.target && !options._root)) throw new Error("'target' is a required option");`} @init(this, options); ${generator.usesRefs && `this.refs = {};`} - this._state = @assign(${templateProperties.data ? '%data()' : '{}'}, options.data); + this._state = @assign(${initialState.join(', ')}); ${generator.metaBindings} ${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`} ${options.dev && diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts index 33a305146f..ef5147c920 100644 --- a/src/generators/dom/preprocess.ts +++ b/src/generators/dom/preprocess.ts @@ -74,9 +74,7 @@ const preprocessors = { ) => { cannotUseInnerHTML(node); node.var = block.getUniqueName('text'); - - const dependencies = block.findDependencies(node.expression); - block.addDependencies(dependencies); + block.addDependencies(node.metadata.dependencies); }, RawMustacheTag: ( @@ -90,9 +88,7 @@ const preprocessors = { ) => { cannotUseInnerHTML(node); node.var = block.getUniqueName('raw'); - - const dependencies = block.findDependencies(node.expression); - block.addDependencies(dependencies); + block.addDependencies(node.metadata.dependencies); }, Text: ( @@ -133,8 +129,7 @@ const preprocessors = { function attachBlocks(node: Node) { node.var = block.getUniqueName(`if_block`); - const dependencies = block.findDependencies(node.expression); - block.addDependencies(dependencies); + block.addDependencies(node.metadata.dependencies); node._block = block.child({ comment: createDebuggingComment(node, generator), @@ -209,7 +204,7 @@ const preprocessors = { cannotUseInnerHTML(node); node.var = block.getUniqueName(`each`); - const dependencies = block.findDependencies(node.expression); + const { dependencies } = node.metadata; block.addDependencies(dependencies); const indexNames = new Map(block.indexNames); @@ -235,24 +230,18 @@ const preprocessors = { const changeableIndexes = new Map(block.changeableIndexes); if (node.index) changeableIndexes.set(node.index, node.key); - const contextDependencies = new Map(block.contextDependencies); - contextDependencies.set(node.context, dependencies); - if (node.destructuredContexts) { - for (const i = 0; i < node.destructuredContexts.length; i++) { + for (let i = 0; i < node.destructuredContexts.length; i += 1) { contexts.set(node.destructuredContexts[i], `${context}[${i}]`); - contextDependencies.set(node.destructuredContexts[i], dependencies); } } node._block = block.child({ comment: createDebuggingComment(node, generator), name: generator.getUniqueName('create_each_block'), - expression: node.expression, context: node.context, key: node.key, - contextDependencies, contexts, indexes, changeableIndexes, @@ -319,7 +308,7 @@ const preprocessors = { if (chunk.type !== 'Text') { if (node.parent) cannotUseInnerHTML(node.parent); - const dependencies = block.findDependencies(chunk.expression); + const dependencies = chunk.metadata.dependencies; block.addDependencies(dependencies); // special case —