diff --git a/.gitignore b/.gitignore index 39e5add714..ed21efb721 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,16 @@ .DS_Store -node_modules -compiler -ssr -shared.js -scratch -!test/compiler -!test/ssr .nyc_output -coverage -coverage.lcov -test/sourcemaps/samples/*/output.js -test/sourcemaps/samples/*/output.js.map -_actual.* -_actual-v2.* -_actual-bundle.* -src/generators/dom/shared.ts -package-lock.json -.idea/ -*.iml -store.umd.js -yarn-error.log \ No newline at end of file +node_modules +/compiler/ +/ssr/ +/shared.js +/scratch/ +/coverage/ +/coverage.lcov/ +/test/sourcemaps/samples/*/output.js +/test/sourcemaps/samples/*/output.js.map +/src/compile/shared.ts +/package-lock.json +/store.umd.js +/yarn-error.log +_actual*.* \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js index 1f10cb6a0c..5ad189694b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -34,7 +34,7 @@ export default [ /* ssr/register.js */ { - input: 'src/server-side-rendering/register.js', + input: 'src/ssr/register.js', plugins: [ resolve(), commonjs(), diff --git a/src/Stats.ts b/src/Stats.ts index c1a4b9b642..c9e62aef21 100644 --- a/src/Stats.ts +++ b/src/Stats.ts @@ -1,5 +1,5 @@ import { Node, Warning } from './interfaces'; -import Generator from './generators/Generator'; +import Compiler from './compile/Compiler'; const now = (typeof process !== 'undefined' && process.hrtime) ? () => { @@ -73,12 +73,12 @@ export default class Stats { this.currentChildren = this.currentTiming ? this.currentTiming.children : this.timings; } - render(generator: Generator) { + render(compiler: Compiler) { const timings = Object.assign({ total: now() - this.startTime }, collapseTimings(this.timings)); - const imports = generator.imports.map(node => { + const imports = compiler.imports.map(node => { return { source: node.source.value, specifiers: node.specifiers.map(specifier => { @@ -95,8 +95,8 @@ export default class Stats { }); const hooks: Record = {}; - if (generator.templateProperties.oncreate) hooks.oncreate = true; - if (generator.templateProperties.ondestroy) hooks.ondestroy = true; + if (compiler.templateProperties.oncreate) hooks.oncreate = true; + if (compiler.templateProperties.ondestroy) hooks.ondestroy = true; return { timings, diff --git a/src/generators/Generator.ts b/src/compile/Compiler.ts similarity index 62% rename from src/generators/Generator.ts rename to src/compile/Compiler.ts index b3cdd96402..da5207de39 100644 --- a/src/generators/Generator.ts +++ b/src/compile/Compiler.ts @@ -1,3 +1,4 @@ +import { parseExpressionAt } from 'acorn'; import MagicString, { Bundle } from 'magic-string'; import isReference from 'is-reference'; import { walk, childKeys } from 'estree-walker'; @@ -13,11 +14,13 @@ import nodeToString from '../utils/nodeToString'; import wrapModule from './wrapModule'; import annotateWithScopes, { Scope } from '../utils/annotateWithScopes'; import getName from '../utils/getName'; -import clone from '../utils/clone'; import Stylesheet from '../css/Stylesheet'; import { test } from '../config'; -import nodes from './nodes/index'; -import { Node, GenerateOptions, ShorthandImport, Parsed, CompileOptions, CustomElementOptions } from '../interfaces'; +import Fragment from './nodes/Fragment'; +import shared from './shared'; +import { DomTarget } from './dom/index'; +import { SsrTarget } from './ssr/index'; +import { Node, GenerateOptions, ShorthandImport, Ast, CompileOptions, CustomElementOptions } from '../interfaces'; interface Computation { key: string; @@ -75,14 +78,15 @@ function removeIndentation( childKeys.EachBlock = childKeys.IfBlock = ['children', 'else']; childKeys.Attribute = ['value']; -export default class Generator { +export default class Compiler { stats: Stats; - ast: Parsed; - parsed: Parsed; + ast: Ast; source: string; name: string; options: CompileOptions; + fragment: Fragment; + target: DomTarget | SsrTarget; customElement: CustomElementOptions; tag: string; @@ -122,22 +126,22 @@ export default class Generator { usedNames: Set; constructor( - parsed: Parsed, + ast: Ast, source: string, name: string, stylesheet: Stylesheet, options: CompileOptions, stats: Stats, - dom: boolean + dom: boolean, + target: DomTarget | SsrTarget ) { stats.start('compile'); this.stats = stats; - this.ast = clone(parsed); - - this.parsed = parsed; + this.ast = ast; this.source = source; this.options = options; + this.target = target; this.imports = []; this.shorthandImports = []; @@ -191,8 +195,11 @@ export default class Generator { throw new Error(`No tag name specified`); // TODO better error } - this.walkTemplate(); + this.fragment = new Fragment(this, ast.html); + // this.walkTemplate(); if (!this.customElement) this.stylesheet.reify(); + + stylesheet.warnOnUnusedSelectors(options.onwarn); } addSourcemapLocations(node: Node) { @@ -212,112 +219,108 @@ export default class Generator { return this.aliases.get(name); } - contextualise( - contexts: Map, - indexes: Map, - expression: Node, - context: string, - isEventHandler: boolean - ): { - contexts: Set, - indexes: Set - } { - // this.addSourcemapLocations(expression); + generate(result: string, options: CompileOptions, { banner = '', name, format }: GenerateOptions ) { + const pattern = /\[✂(\d+)-(\d+)$/; - const usedContexts: Set = new Set(); - const usedIndexes: Set = new Set(); + const helpers = new Set(); - const { code, helpers } = this; + // TODO use same regex for both + result = result.replace(options.generate === 'ssr' ? /(@+|#+|%+)(\w*(?:-\w*)?)/g : /(%+|@+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => { + if (sigil === '@') { + if (name in shared) { + if (options.dev && `${name}Dev` in shared) name = `${name}Dev`; + helpers.add(name); + } - let scope: Scope; - let lexicalDepth = 0; + return this.alias(name); + } - const self = this; + if (sigil === '%') { + return this.templateVars.get(name); + } - walk(expression, { - enter(node: Node, parent: Node, key: string) { - if (/^Function/.test(node.type)) lexicalDepth += 1; + return sigil.slice(1) + name; + }); - if (node._scope) { - scope = node._scope; - return; - } + let importedHelpers; - if (node.type === 'ThisExpression') { - if (lexicalDepth === 0 && context) - code.overwrite(node.start, node.end, context, { - storeName: true, - contentOnly: false, - }); - } else if (isReference(node, parent)) { - const { name } = flattenReference(node); - if (scope && scope.has(name)) return; - - if (name === 'event' && isEventHandler) { - // noop - } else if (contexts.has(name)) { - const contextName = contexts.get(name); - if (contextName !== name) { - // this is true for 'reserved' names like `state` and `component`, - // also destructured contexts - code.overwrite( - node.start, - node.start + name.length, - contextName, - { storeName: true, contentOnly: false } - ); - - const destructuredName = contextName.replace(/\[\d+\]/, ''); - if (destructuredName !== contextName) { - // so that hoisting the context works correctly - usedContexts.add(destructuredName); - } - } + if (options.shared) { + if (format !== 'es' && format !== 'cjs') { + throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``); + } - usedContexts.add(name); - } else if (helpers.has(name)) { - let object = node; - while (object.type === 'MemberExpression') object = object.object; - - const alias = self.templateVars.get(`helpers-${name}`); - if (alias !== name) code.overwrite(object.start, object.end, alias); - } else if (indexes.has(name)) { - const context = indexes.get(name); - usedContexts.add(context); // TODO is this right? - usedIndexes.add(name); - } else { - // handle shorthand properties - if (parent && parent.type === 'Property' && parent.shorthand) { - if (key === 'key') { - code.appendLeft(node.start, `${name}: `); - return; + importedHelpers = Array.from(helpers).sort().map(name => { + const alias = this.alias(name); + return { name, alias }; + }); + } else { + let inlineHelpers = ''; + + const compiler = this; + + importedHelpers = []; + + helpers.forEach(name => { + const str = shared[name]; + const code = new MagicString(str); + const expression = parseExpressionAt(str, 0); + + let { scope } = annotateWithScopes(expression); + + walk(expression, { + enter(node: Node, parent: Node) { + if (node._scope) scope = node._scope; + + if ( + node.type === 'Identifier' && + isReference(node, parent) && + !scope.has(node.name) + ) { + if (node.name in shared) { + // this helper function depends on another one + const dependency = node.name; + helpers.add(dependency); + + const alias = compiler.alias(dependency); + if (alias !== node.name) { + code.overwrite(node.start, node.end, alias); + } } } + }, + + leave(node: Node) { + if (node._scope) scope = scope.parent; + }, + }); + + if (name === 'transitionManager') { + // special case + const global = `_svelteTransitionManager`; - code.prependRight(node.start, `state.`); - usedContexts.add('state'); + inlineHelpers += `\n\nvar ${this.alias('transitionManager')} = window.${global} || (window.${global} = ${code});\n\n`; + } else if (name === 'escaped' || name === 'missingComponent') { + // vars are an awkward special case... would be nice to avoid this + const alias = this.alias(name); + inlineHelpers += `\n\nconst ${alias} = ${code};` + } else { + const alias = this.alias(expression.id.name); + if (alias !== expression.id.name) { + code.overwrite(expression.id.start, expression.id.end, alias); } - this.skip(); + inlineHelpers += `\n\n${code}`; } - }, - - leave(node: Node) { - if (/^Function/.test(node.type)) lexicalDepth -= 1; - if (node._scope) scope = scope.parent; - }, - }); + }); - return { - contexts: usedContexts, - indexes: usedIndexes - }; - } + result += inlineHelpers; + } - generate(result: string, options: CompileOptions, { banner = '', sharedPath, helpers, name, format }: GenerateOptions ) { - const pattern = /\[✂(\d+)-(\d+)$/; + const sharedPath = options.shared === true + ? 'svelte/shared.js' + : options.shared || ''; - const module = wrapModule(result, format, name, options, banner, sharedPath, helpers, this.imports, this.shorthandImports, this.source); + const module = wrapModule(result, format, name, options, banner, sharedPath, importedHelpers, this.imports, this.shorthandImports, this.source); const parts = module.split('✂]'); const finalChunk = parts.pop(); @@ -393,7 +396,7 @@ export default class Generator { return alias; } - getUniqueNameMaker(names: string[]) { + getUniqueNameMaker() { const localUsedNames = new Set(); function add(name: string) { @@ -402,7 +405,6 @@ export default class Generator { reservedNames.forEach(add); this.userVars.forEach(add); - names.forEach(add); return (name: string) => { if (test) name = `${name}$`; @@ -428,7 +430,7 @@ export default class Generator { imports } = this; - const { js } = this.parsed; + const { js } = this.ast; const componentDefinition = new CodeBuilder(); @@ -703,213 +705,4 @@ export default class Generator { } } } - - walkTemplate() { - const generator = this; - const { - code, - expectedProperties, - helpers - } = this; - const { html } = this.parsed; - - const contextualise = ( - node: Node, contextDependencies: Map, - indexes: Set, - isEventHandler: boolean - ) => { - 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) || (name === 'event' && isEventHandler)) 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]; - - function parentIsHead(node) { - if (!node) return false; - if (node.type === 'Component' || node.type === 'Element') return false; - if (node.type === 'Head') return true; - - return parentIsHead(node.parent); - } - - walk(html, { - enter(node: Node, parent: Node, key: string) { - // TODO this is hacky as hell - if (key === 'parent') return this.skip(); - node.parent = parent; - - node.generator = generator; - - if (node.type === 'Element' && (node.name === 'svelte:component' || node.name === 'svelte:self' || generator.components.has(node.name))) { - node.type = 'Component'; - Object.setPrototypeOf(node, nodes.Component.prototype); - } else if (node.type === 'Element' && node.name === 'title' && parentIsHead(parent)) { // TODO do this in parse? - node.type = 'Title'; - Object.setPrototypeOf(node, nodes.Title.prototype); - } else if (node.type === 'Element' && node.name === 'slot' && !generator.customElement) { - node.type = 'Slot'; - Object.setPrototypeOf(node, nodes.Slot.prototype); - } else if (node.type in nodes) { - Object.setPrototypeOf(node, nodes[node.type].prototype); - } - - if (node.type === 'Element') { - generator.stylesheet.apply(node); - } - - if (node.type === 'EachBlock') { - node.metadata = contextualise(node.expression, contextDependencies, indexes, false); - - contextDependencies = new Map(contextDependencies); - contextDependencies.set(node.context, node.metadata.dependencies); - - if (node.destructuredContexts) { - node.destructuredContexts.forEach((name: string) => { - 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 === 'AwaitBlock') { - node.metadata = contextualise(node.expression, contextDependencies, indexes, false); - - contextDependencies = new Map(contextDependencies); - contextDependencies.set(node.value, node.metadata.dependencies); - contextDependencies.set(node.error, node.metadata.dependencies); - - contextDependenciesStack.push(contextDependencies); - } - - if (node.type === 'IfBlock') { - node.metadata = contextualise(node.expression, contextDependencies, indexes, false); - } - - if (node.type === 'MustacheTag' || node.type === 'RawMustacheTag' || node.type === 'AttributeShorthand') { - node.metadata = contextualise(node.expression, contextDependencies, indexes, false); - this.skip(); - } - - if (node.type === 'Binding') { - node.metadata = contextualise(node.value, contextDependencies, indexes, false); - this.skip(); - } - - if (node.type === 'EventHandler' && node.expression) { - node.expression.arguments.forEach((arg: Node) => { - arg.metadata = contextualise(arg, contextDependencies, indexes, true); - }); - this.skip(); - } - - if (node.type === 'Transition' && node.expression) { - node.metadata = contextualise(node.expression, contextDependencies, indexes, false); - this.skip(); - } - - if (node.type === 'Action' && node.expression) { - node.metadata = contextualise(node.expression, contextDependencies, indexes, false); - if (node.expression.type === 'CallExpression') { - node.expression.arguments.forEach((arg: Node) => { - arg.metadata = contextualise(arg, contextDependencies, indexes, true); - }); - } - this.skip(); - } - - if (node.type === 'Component' && node.name === 'svelte:component') { - node.metadata = contextualise(node.expression, contextDependencies, indexes, false); - } - - if (node.type === 'Spread') { - node.metadata = contextualise(node.expression, contextDependencies, indexes, false); - } - }, - - leave(node: Node, parent: Node) { - if (node.type === 'EachBlock') { - contextDependenciesStack.pop(); - contextDependencies = contextDependenciesStack[contextDependenciesStack.length - 1]; - - if (node.index) { - indexesStack.pop(); - indexes = indexesStack[indexesStack.length - 1]; - } - } - - if (node.type === 'Element' && node.name === 'option') { - // Special case — treat these the same way: - // - // - const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value'); - - if (!valueAttribute) { - node.attributes.push(new nodes.Attribute({ - generator, - name: 'value', - value: node.children, - parent: node - })); - } - } - } - }); - } } diff --git a/src/generators/dom/Block.ts b/src/compile/dom/Block.ts similarity index 77% rename from src/generators/dom/Block.ts rename to src/compile/dom/Block.ts index edf505c7b3..f41892ccc9 100644 --- a/src/generators/dom/Block.ts +++ b/src/compile/dom/Block.ts @@ -1,42 +1,27 @@ import CodeBuilder from '../../utils/CodeBuilder'; import deindent from '../../utils/deindent'; import { escape } from '../../utils/stringify'; -import { DomGenerator } from './index'; +import Compiler from '../Compiler'; import { Node } from '../../interfaces'; -import shared from './shared'; export interface BlockOptions { name: string; - generator?: DomGenerator; - expression?: Node; - context?: string; - destructuredContexts?: string[]; + compiler?: Compiler; comment?: string; key?: string; - contexts?: Map; - contextTypes?: Map; - indexes?: Map; - changeableIndexes?: Map; indexNames?: Map; listNames?: Map; dependencies?: Set; } export default class Block { - generator: DomGenerator; + compiler: Compiler; name: string; - expression: Node; - context: string; - destructuredContexts?: string[]; comment?: string; key: string; first: string; - contexts: Map; - contextTypes: Map; - indexes: Map; - changeableIndexes: Map; dependencies: Set; indexNames: Map; listNames: Map; @@ -67,21 +52,14 @@ export default class Block { autofocus: string; constructor(options: BlockOptions) { - this.generator = options.generator; + this.compiler = options.compiler; this.name = options.name; - this.expression = options.expression; - this.context = options.context; - this.destructuredContexts = options.destructuredContexts; this.comment = options.comment; // for keyed each blocks this.key = options.key; this.first = null; - this.contexts = options.contexts; - this.contextTypes = options.contextTypes; - this.indexes = options.indexes; - this.changeableIndexes = options.changeableIndexes; this.dependencies = new Set(); this.indexNames = options.indexNames; @@ -105,18 +83,18 @@ export default class Block { this.hasOutroMethod = false; this.outros = 0; - this.getUniqueName = this.generator.getUniqueNameMaker([...this.contexts.values()]); + this.getUniqueName = this.compiler.getUniqueNameMaker(); this.variables = new Map(); this.aliases = new Map() .set('component', this.getUniqueName('component')) - .set('state', this.getUniqueName('state')); + .set('ctx', this.getUniqueName('ctx')); if (this.key) this.aliases.set('key', this.getUniqueName('key')); this.hasUpdateMethod = false; // determined later } - addDependencies(dependencies: string[]) { + addDependencies(dependencies: Set) { dependencies.forEach(dependency => { this.dependencies.add(dependency); }); @@ -163,10 +141,6 @@ export default class Block { return new Block(Object.assign({}, this, { key: null }, options, { parent: this })); } - contextualise(expression: Node, context?: string, isEventHandler?: boolean) { - return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler); - } - toString() { let introing; const hasIntros = !this.builders.intro.isEmpty(); @@ -186,23 +160,6 @@ export default class Block { this.builders.mount.addLine(`${this.autofocus}.focus();`); } - // TODO `this.contexts` is possibly redundant post-#1122 - const initializers = []; - - this.contexts.forEach((name, context) => { - // TODO only the ones that are actually used in this block... - const listName = this.listNames.get(context); - const indexName = this.indexNames.get(context); - - initializers.push( - `${name} = state.${context}`, - `${listName} = state.${listName}`, - `${indexName} = state.${indexName}` - ); - - this.hasUpdateMethod = true; - }); - // minor hack – we need to ensure that any {{{triples}}} are detached first this.builders.unmount.addBlockAtStart(this.builders.detachRaw.toString()); @@ -230,7 +187,7 @@ export default class Block { `); } - if (this.generator.hydratable) { + if (this.compiler.options.hydratable) { if (this.builders.claim.isEmpty() && this.builders.hydrate.isEmpty()) { properties.addBlock(`l: @noop,`); } else { @@ -261,13 +218,13 @@ export default class Block { `); } - if (this.hasUpdateMethod) { - if (this.builders.update.isEmpty() && initializers.length === 0) { + if (this.hasUpdateMethod || this.maintainContext) { + if (this.builders.update.isEmpty() && !this.maintainContext) { properties.addBlock(`p: @noop,`); } else { properties.addBlock(deindent` - p: function update(changed, state) { - ${initializers.map(str => `${str};`)} + p: function update(changed, ${this.maintainContext ? '_ctx' : 'ctx'}) { + ${this.maintainContext && `ctx = _ctx;`} ${this.builders.update} }, `); @@ -338,9 +295,7 @@ export default class Block { return deindent` ${this.comment && `// ${escape(this.comment)}`} - function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, state) { - ${initializers.length > 0 && - `var ${initializers.join(', ')};`} + function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, ctx) { ${this.variables.size > 0 && `var ${Array.from(this.variables.keys()) .map(key => { diff --git a/src/generators/dom/index.ts b/src/compile/dom/index.ts similarity index 58% rename from src/generators/dom/index.ts rename to src/compile/dom/index.ts index 337d23d2ed..3db1158449 100644 --- a/src/generators/dom/index.ts +++ b/src/compile/dom/index.ts @@ -8,52 +8,33 @@ import { stringify, escape } from '../../utils/stringify'; import CodeBuilder from '../../utils/CodeBuilder'; import globalWhitelist from '../../utils/globalWhitelist'; import reservedNames from '../../utils/reservedNames'; -import shared from './shared'; -import Generator from '../Generator'; +import Compiler from '../Compiler'; import Stylesheet from '../../css/Stylesheet'; import Stats from '../../Stats'; import Block from './Block'; import { test } from '../../config'; -import { Parsed, CompileOptions, Node } from '../../interfaces'; +import { Ast, CompileOptions, Node } from '../../interfaces'; -export class DomGenerator extends Generator { +export class DomTarget { blocks: (Block|string)[]; readonly: Set; metaBindings: string[]; - hydratable: boolean; - legacy: boolean; - hasIntroTransitions: boolean; hasOutroTransitions: boolean; hasComplexBindings: boolean; - needsEncapsulateHelper: boolean; - - constructor( - parsed: Parsed, - source: string, - name: string, - stylesheet: Stylesheet, - options: CompileOptions, - stats: Stats - ) { - super(parsed, source, name, stylesheet, options, stats, true); + constructor() { this.blocks = []; - this.readonly = new Set(); - this.hydratable = options.hydratable; - this.legacy = options.legacy; - this.needsEncapsulateHelper = false; - // initial values for e.g. window.innerWidth, if there's a meta tag this.metaBindings = []; } } export default function dom( - parsed: Parsed, + ast: Ast, source: string, stylesheet: Stylesheet, options: CompileOptions, @@ -61,23 +42,22 @@ export default function dom( ) { const format = options.format || 'es'; - const generator = new DomGenerator(parsed, source, options.name || 'SvelteComponent', stylesheet, options, stats); + const target = new DomTarget(); + const compiler = new Compiler(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, true, target); const { computations, name, templateProperties, namespace, - } = generator; + } = compiler; - parsed.html.build(); - const { block } = parsed.html; + compiler.fragment.build(); + const { block } = compiler.fragment; // prevent fragment being created twice (#1063) if (options.customElement) block.builders.create.addLine(`this.c = @noop;`); - generator.stylesheet.warnOnUnusedSelectors(options.onwarn); - const builder = new CodeBuilder(); const computationBuilder = new CodeBuilder(); const computationDeps = new Set(); @@ -88,14 +68,14 @@ export default function dom( computationDeps.add(dep); }); - if (generator.readonly.has(key)) { + if (target.readonly.has(key)) { // bindings throw new Error( `Cannot have a computed value '${key}' that clashes with a read-only property` ); } - generator.readonly.add(key); + target.readonly.add(key); const condition = `${deps.map(dep => `changed.${dep}`).join(' || ')}`; @@ -105,27 +85,27 @@ export default function dom( }); } - if (generator.javascript) { - builder.addBlock(generator.javascript); + if (compiler.javascript) { + builder.addBlock(compiler.javascript); } - const css = generator.stylesheet.render(options.filename, !generator.customElement); - const styles = generator.stylesheet.hasStyles && stringify(options.dev ? + const css = compiler.stylesheet.render(options.filename, !compiler.customElement); + const styles = compiler.stylesheet.hasStyles && stringify(options.dev ? `${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` : css.code, { onlyEscapeAtSymbol: true }); - if (styles && generator.options.css !== false && !generator.customElement) { + if (styles && compiler.options.css !== false && !compiler.customElement) { builder.addBlock(deindent` function @add_css() { var style = @createElement("style"); - style.id = '${generator.stylesheet.id}-style'; + style.id = '${compiler.stylesheet.id}-style'; style.textContent = ${styles}; @appendNode(style, document.head); } `); } - generator.blocks.forEach(block => { + target.blocks.forEach(block => { builder.addBlock(block.toString()); }); @@ -144,10 +124,10 @@ export default function dom( .join(',\n')} }`; - const debugName = `<${generator.customElement ? generator.tag : name}>`; + const debugName = `<${compiler.customElement ? compiler.tag : name}>`; // generate initial state object - const expectedProperties = Array.from(generator.expectedProperties); + const expectedProperties = Array.from(compiler.expectedProperties); const globals = expectedProperties.filter(prop => globalWhitelist.has(prop)); const storeProps = expectedProperties.filter(prop => prop[0] === '$'); const initialState = []; @@ -172,31 +152,31 @@ export default function dom( const constructorBody = deindent` ${options.dev && `this._debugName = '${debugName}';`} - ${options.dev && !generator.customElement && + ${options.dev && !compiler.customElement && `if (!options || (!options.target && !options.root)) throw new Error("'target' is a required option");`} @init(this, options); ${templateProperties.store && `this.store = %store();`} - ${generator.usesRefs && `this.refs = {};`} + ${compiler.usesRefs && `this.refs = {};`} this._state = ${initialState.reduce((state, piece) => `@assign(${state}, ${piece})`)}; ${storeProps.length > 0 && `this.store._add(this, [${storeProps.map(prop => `"${prop.slice(1)}"`)}]);`} - ${generator.metaBindings} + ${target.metaBindings} ${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`} ${options.dev && - Array.from(generator.expectedProperties).map(prop => { + Array.from(compiler.expectedProperties).map(prop => { if (globalWhitelist.has(prop)) return; if (computations.find(c => c.key === prop)) return; - const message = generator.components.has(prop) ? + const message = compiler.components.has(prop) ? `${debugName} expected to find '${prop}' in \`data\`, but found it in \`components\` instead` : `${debugName} was created without expected data property '${prop}'`; const conditions = [`!('${prop}' in this._state)`]; - if (generator.customElement) conditions.push(`!('${prop}' in this.attributes)`); + if (compiler.customElement) conditions.push(`!('${prop}' in this.attributes)`); return `if (${conditions.join(' && ')}) console.warn("${message}");` })} - ${generator.bindingGroups.length && - `this._bindingGroups = [${Array(generator.bindingGroups.length).fill('[]').join(', ')}];`} + ${compiler.bindingGroups.length && + `this._bindingGroups = [${Array(compiler.bindingGroups.length).fill('[]').join(', ')}];`} ${templateProperties.onstate && `this._handlers.state = [%onstate];`} ${templateProperties.onupdate && `this._handlers.update = [%onupdate];`} @@ -207,26 +187,26 @@ export default function dom( }];` )} - ${generator.slots.size && `this._slotted = options.slots || {};`} + ${compiler.slots.size && `this._slotted = options.slots || {};`} - ${generator.customElement ? + ${compiler.customElement ? deindent` this.attachShadow({ mode: 'open' }); ${css.code && `this.shadowRoot.innerHTML = \`\`;`} ` : - (generator.stylesheet.hasStyles && options.css !== false && - `if (!document.getElementById("${generator.stylesheet.id}-style")) @add_css();`) + (compiler.stylesheet.hasStyles && options.css !== false && + `if (!document.getElementById("${compiler.stylesheet.id}-style")) @add_css();`) } - ${(hasInitHooks || generator.hasComponents || generator.hasComplexBindings || generator.hasIntroTransitions) && deindent` + ${(hasInitHooks || compiler.hasComponents || target.hasComplexBindings || target.hasIntroTransitions) && deindent` if (!options.root) { this._oncreate = []; - ${(generator.hasComponents || generator.hasComplexBindings) && `this._beforecreate = [];`} - ${(generator.hasComponents || generator.hasIntroTransitions) && `this._aftercreate = [];`} + ${(compiler.hasComponents || target.hasComplexBindings) && `this._beforecreate = [];`} + ${(compiler.hasComponents || target.hasIntroTransitions) && `this._aftercreate = [];`} } `} - ${generator.slots.size && `this.slots = {};`} + ${compiler.slots.size && `this.slots = {};`} this._fragment = @create_main_fragment(this, this._state); @@ -238,14 +218,14 @@ export default function dom( }); `} - ${generator.customElement ? deindent` + ${compiler.customElement ? deindent` this._fragment.c(); this._fragment.${block.hasIntroMethod ? 'i' : 'm'}(this.shadowRoot, null); if (options.target) this._mount(options.target, options.anchor); ` : deindent` if (options.target) { - ${generator.hydratable + ${compiler.options.hydratable ? deindent` var nodes = @children(options.target); options.hydrate ? this._fragment.l(nodes) : this._fragment.c(); @@ -257,19 +237,19 @@ export default function dom( `} this._mount(options.target, options.anchor); - ${(generator.hasComponents || generator.hasComplexBindings || hasInitHooks || generator.hasIntroTransitions) && deindent` - ${generator.hasComponents && `this._lock = true;`} - ${(generator.hasComponents || generator.hasComplexBindings) && `@callAll(this._beforecreate);`} - ${(generator.hasComponents || hasInitHooks) && `@callAll(this._oncreate);`} - ${(generator.hasComponents || generator.hasIntroTransitions) && `@callAll(this._aftercreate);`} - ${generator.hasComponents && `this._lock = false;`} + ${(compiler.hasComponents || target.hasComplexBindings || hasInitHooks || target.hasIntroTransitions) && deindent` + ${compiler.hasComponents && `this._lock = true;`} + ${(compiler.hasComponents || target.hasComplexBindings) && `@callAll(this._beforecreate);`} + ${(compiler.hasComponents || hasInitHooks) && `@callAll(this._oncreate);`} + ${(compiler.hasComponents || target.hasIntroTransitions) && `@callAll(this._aftercreate);`} + ${compiler.hasComponents && `this._lock = false;`} `} } `} `; - if (generator.customElement) { - const props = generator.props || Array.from(generator.expectedProperties); + if (compiler.customElement) { + const props = compiler.props || Array.from(compiler.expectedProperties); builder.addBlock(deindent` class ${name} extends HTMLElement { @@ -292,7 +272,7 @@ export default function dom( } `).join('\n\n')} - ${generator.slots.size && deindent` + ${compiler.slots.size && deindent` connectedCallback() { Object.keys(this._slotted).forEach(key => { this.appendChild(this._slotted[key]); @@ -303,18 +283,18 @@ export default function dom( this.set({ [attr]: newValue }); } - ${(generator.hasComponents || generator.hasComplexBindings || templateProperties.oncreate || generator.hasIntroTransitions) && deindent` + ${(compiler.hasComponents || target.hasComplexBindings || templateProperties.oncreate || target.hasIntroTransitions) && deindent` connectedCallback() { - ${generator.hasComponents && `this._lock = true;`} - ${(generator.hasComponents || generator.hasComplexBindings) && `@callAll(this._beforecreate);`} - ${(generator.hasComponents || templateProperties.oncreate) && `@callAll(this._oncreate);`} - ${(generator.hasComponents || generator.hasIntroTransitions) && `@callAll(this._aftercreate);`} - ${generator.hasComponents && `this._lock = false;`} + ${compiler.hasComponents && `this._lock = true;`} + ${(compiler.hasComponents || target.hasComplexBindings) && `@callAll(this._beforecreate);`} + ${(compiler.hasComponents || templateProperties.oncreate) && `@callAll(this._oncreate);`} + ${(compiler.hasComponents || target.hasIntroTransitions) && `@callAll(this._aftercreate);`} + ${compiler.hasComponents && `this._lock = false;`} } `} } - customElements.define("${generator.tag}", ${name}); + customElements.define("${compiler.tag}", ${name}); @assign(@assign(${prototypeBase}, ${proto}), { _mount(target, anchor) { target.insertBefore(this, anchor); @@ -341,7 +321,7 @@ export default function dom( builder.addBlock(deindent` ${options.dev && deindent` ${name}.prototype._checkReadOnly = function _checkReadOnly(newState) { - ${Array.from(generator.readonly).map( + ${Array.from(target.readonly).map( prop => `if ('${prop}' in newState && !this._updatingReadonlyProperty) throw new Error("${debugName}: Cannot set read-only property '${prop}'");` )} @@ -361,99 +341,15 @@ export default function dom( ${immutable && `${name}.prototype._differs = @_differsImmutable;`} `); - const usedHelpers = new Set(); - - let result = builder - .toString() - .replace(/(%+|@+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => { - if (sigil === '@') { - if (name in shared) { - if (options.dev && `${name}Dev` in shared) name = `${name}Dev`; - usedHelpers.add(name); - } - - return generator.alias(name); - } - - if (sigil === '%') { - return generator.templateVars.get(name); - } - - return sigil.slice(1) + name; - }); - - let helpers; - - if (sharedPath) { - if (format !== 'es' && format !== 'cjs') { - throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``); - } - const used = Array.from(usedHelpers).sort(); - helpers = used.map(name => { - const alias = generator.alias(name); - return { name, alias }; - }); - } else { - let inlineHelpers = ''; - - usedHelpers.forEach(key => { - const str = shared[key]; - const code = new MagicString(str); - const expression = parseExpressionAt(str, 0); - - let { scope } = annotateWithScopes(expression); - - walk(expression, { - enter(node: Node, parent: Node) { - if (node._scope) scope = node._scope; - - if ( - node.type === 'Identifier' && - isReference(node, parent) && - !scope.has(node.name) - ) { - if (node.name in shared) { - // this helper function depends on another one - const dependency = node.name; - usedHelpers.add(dependency); - - const alias = generator.alias(dependency); - if (alias !== node.name) - code.overwrite(node.start, node.end, alias); - } - } - }, - - leave(node: Node) { - if (node._scope) scope = scope.parent; - }, - }); - - if (key === 'transitionManager') { - // special case - const global = `_svelteTransitionManager`; - - inlineHelpers += `\n\nvar ${generator.alias('transitionManager')} = window.${global} || (window.${global} = ${code});\n\n`; - } else { - const alias = generator.alias(expression.id.name); - if (alias !== expression.id.name) - code.overwrite(expression.id.start, expression.id.end, alias); - - inlineHelpers += `\n\n${code}`; - } - }); - - result += inlineHelpers; - } + let result = builder.toString(); const filename = options.filename && ( typeof process !== 'undefined' ? options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : options.filename ); - return generator.generate(result, options, { + return compiler.generate(result, options, { banner: `/* ${filename ? `${filename} ` : ``}generated by Svelte v${"__VERSION__"} */`, sharedPath, - helpers, name, format, }); diff --git a/src/compile/nodes/Action.ts b/src/compile/nodes/Action.ts new file mode 100644 index 0000000000..1e8f4311e7 --- /dev/null +++ b/src/compile/nodes/Action.ts @@ -0,0 +1,18 @@ +import Node from './shared/Node'; +import Expression from './shared/Expression'; + +export default class Action extends Node { + type: 'Action'; + name: string; + expression: Expression; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + + this.name = info.name; + + this.expression = info.expression + ? new Expression(compiler, this, scope, info.expression) + : null; + } +} \ No newline at end of file diff --git a/src/generators/nodes/Attribute.ts b/src/compile/nodes/Attribute.ts similarity index 77% rename from src/generators/nodes/Attribute.ts rename to src/compile/nodes/Attribute.ts index 904c971c1d..37de5068a9 100644 --- a/src/generators/nodes/Attribute.ts +++ b/src/compile/nodes/Attribute.ts @@ -1,45 +1,105 @@ import deindent from '../../utils/deindent'; -import { stringify } from '../../utils/stringify'; +import { escape, escapeTemplate, stringify } from '../../utils/stringify'; import fixAttributeCasing from '../../utils/fixAttributeCasing'; -import getExpressionPrecedence from '../../utils/getExpressionPrecedence'; -import { DomGenerator } from '../dom/index'; +import addToSet from '../../utils/addToSet'; +import Compiler from '../Compiler'; import Node from './shared/Node'; import Element from './Element'; +import Text from './Text'; import Block from '../dom/Block'; +import Expression from './shared/Expression'; export interface StyleProp { key: string; value: Node[]; } -export default class Attribute { +export default class Attribute extends Node { type: 'Attribute'; start: number; end: number; - generator: DomGenerator; + compiler: Compiler; parent: Element; name: string; - value: true | Node[] - expression: Node; - - constructor({ - generator, - name, - value, - parent - }: { - generator: DomGenerator, - name: string, - value: Node[], - parent: Element - }) { - this.type = 'Attribute'; - this.generator = generator; - this.parent = parent; - - this.name = name; - this.value = value; + isSpread: boolean; + isTrue: boolean; + isDynamic: boolean; + isSynthetic: boolean; + shouldCache: boolean; + expression?: Expression; + chunks: (Text | Expression)[]; + dependencies: Set; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + + if (info.type === 'Spread') { + this.name = null; + this.isSpread = true; + this.isTrue = false; + this.isSynthetic = false; + + this.expression = new Expression(compiler, this, scope, info.expression); + this.dependencies = this.expression.dependencies; + this.chunks = null; + + this.isDynamic = true; // TODO not necessarily + this.shouldCache = false; // TODO does this mean anything here? + } + + else { + this.name = info.name; + this.isTrue = info.value === true; + this.isSynthetic = info.synthetic; + + this.dependencies = new Set(); + + this.chunks = this.isTrue + ? [] + : info.value.map(node => { + if (node.type === 'Text') return node; + + const expression = new Expression(compiler, this, scope, node.expression); + + addToSet(this.dependencies, expression.dependencies); + return expression; + }); + + // TODO this would be better, but it breaks some stuff + // this.isDynamic = this.dependencies.size > 0; + this.isDynamic = this.chunks.length === 1 + ? this.chunks[0].type !== 'Text' + : this.chunks.length > 1; + + this.shouldCache = this.isDynamic + ? this.chunks.length === 1 + ? this.chunks[0].node.type !== 'Identifier' || scope.names.has(this.chunks[0].node.name) + : true + : false; + } + } + + getValue() { + if (this.isTrue) return true; + if (this.chunks.length === 0) return `''`; + + if (this.chunks.length === 1) { + return this.chunks[0].type === 'Text' + ? stringify(this.chunks[0].data) + : this.chunks[0].snippet; + } + + return (this.chunks[0].type === 'Text' ? '' : `"" + `) + + this.chunks + .map(chunk => { + if (chunk.type === 'Text') { + return stringify(chunk.data); + } else { + return chunk.getPrecedence() <= 13 ? `(${chunk.snippet})` : chunk.snippet; + } + }) + .join(' + '); } render(block: Block) { @@ -47,7 +107,7 @@ export default class Attribute { const name = fixAttributeCasing(this.name); if (name === 'style') { - const styleProps = optimizeStyle(this.value); + const styleProps = optimizeStyle(this.chunks); if (styleProps) { this.renderStyle(block, styleProps); return; @@ -62,9 +122,9 @@ export default class Attribute { name === 'value' && (node.name === 'option' || // TODO check it's actually bound (node.name === 'input' && - node.attributes.find( - (attribute: Attribute) => - attribute.type === 'Binding' && /checked|group/.test(attribute.name) + node.bindings.find( + (binding: Binding) => + /checked|group/.test(binding.name) ))); const propertyName = isIndirectlyBoundValue @@ -78,77 +138,48 @@ export default class Attribute { ? '@setXlinkAttribute' : '@setAttribute'; - const isDynamic = this.isDynamic(); - const isLegacyInputType = this.generator.legacy && name === 'type' && this.parent.name === 'input'; + const isLegacyInputType = this.compiler.options.legacy && name === 'type' && this.parent.name === 'input'; - const isDataSet = /^data-/.test(name) && !this.generator.legacy && !node.namespace; + const isDataSet = /^data-/.test(name) && !this.compiler.options.legacy && !node.namespace; const camelCaseName = isDataSet ? name.replace('data-', '').replace(/(-\w)/g, function (m) { return m[1].toUpperCase(); }) : name; - if (isDynamic) { + if (this.isDynamic) { let value; - const allDependencies = new Set(); - let shouldCache; - let hasChangeableIndex; - // TODO some of this code is repeated in Tag.ts — would be good to // DRY it out if that's possible without introducing crazy indirection - if (this.value.length === 1) { - // single {{tag}} — may be a non-string - const { expression } = this.value[0]; - const { indexes } = block.contextualise(expression); - const { dependencies, snippet } = this.value[0].metadata; - - value = snippet; - dependencies.forEach(d => { - allDependencies.add(d); - }); - - hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index)); - - shouldCache = ( - expression.type !== 'Identifier' || - block.contexts.has(expression.name) || - hasChangeableIndex - ); + if (this.chunks.length === 1) { + // single {tag} — may be a non-string + value = this.chunks[0].snippet; } else { - // '{{foo}} {{bar}}' — treat as string concatenation + // '{foo} {bar}' — treat as string concatenation value = - (this.value[0].type === 'Text' ? '' : `"" + `) + - this.value + (this.chunks[0].type === 'Text' ? '' : `"" + `) + + this.chunks .map((chunk: Node) => { if (chunk.type === 'Text') { return stringify(chunk.data); } else { - const { indexes } = block.contextualise(chunk.expression); - const { dependencies, snippet } = chunk.metadata; - - if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) { - hasChangeableIndex = true; - } - - dependencies.forEach(d => { - allDependencies.add(d); - }); - - return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet; + return chunk.getPrecedence() <= 13 + ? `(${chunk.snippet})` + : chunk.snippet; } }) .join(' + '); - - shouldCache = true; } const isSelectValueAttribute = name === 'value' && node.name === 'select'; - const last = (shouldCache || isSelectValueAttribute) && block.getUniqueName( + const shouldCache = this.shouldCache || isSelectValueAttribute; + + const last = shouldCache && block.getUniqueName( `${node.var}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value` ); - if (shouldCache || isSelectValueAttribute) block.addVariable(last); + if (shouldCache) block.addVariable(last); let updater; const init = shouldCache ? `${last} = ${value}` : value; @@ -185,8 +216,6 @@ export default class Attribute { ${last} = ${value}; ${updater} `); - - block.builders.update.addLine(`${last} = ${value};`); } else if (propertyName) { block.builders.hydrate.addLine( `${node.var}.${propertyName} = ${init};` @@ -204,8 +233,8 @@ export default class Attribute { updater = `${method}(${node.var}, "${name}", ${shouldCache ? last : value});`; } - if (allDependencies.size || hasChangeableIndex || isSelectValueAttribute) { - const dependencies = Array.from(allDependencies); + if (this.dependencies.size || isSelectValueAttribute) { + const dependencies = Array.from(this.dependencies); const changedCheck = ( ( block.hasOutroMethod ? `#outroing || ` : '' ) + dependencies.map(dependency => `changed.${dependency}`).join(' || ') @@ -223,9 +252,9 @@ export default class Attribute { ); } } else { - const value = this.value === true + const value = this.isTrue ? 'true' - : this.value.length === 0 ? `""` : stringify(this.value[0].data); + : this.chunks.length === 0 ? `""` : stringify(this.chunks[0].data); const statement = ( isLegacyInputType @@ -240,7 +269,7 @@ export default class Attribute { block.builders.hydrate.addLine(statement); // special case – autofocus. has to be handled in a bit of a weird way - if (this.value === true && name === 'autofocus') { + if (this.isTrue && name === 'autofocus') { block.autofocus = node.var; } } @@ -249,7 +278,7 @@ export default class Attribute { const updateValue = `${node.var}.value = ${node.var}.__value;`; block.builders.hydrate.addLine(updateValue); - if (isDynamic) block.builders.update.addLine(updateValue); + if (this.isDynamic) block.builders.update.addLine(updateValue); } } @@ -261,9 +290,8 @@ export default class Attribute { let value; if (isDynamic(prop.value)) { - const allDependencies = new Set(); + const propDependencies = new Set(); let shouldCache; - let hasChangeableIndex; value = ((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) + @@ -272,26 +300,21 @@ export default class Attribute { if (chunk.type === 'Text') { return stringify(chunk.data); } else { - const { indexes } = block.contextualise(chunk.expression); - const { dependencies, snippet } = chunk.metadata; - - if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) { - hasChangeableIndex = true; - } + const { dependencies, snippet } = chunk; dependencies.forEach(d => { - allDependencies.add(d); + propDependencies.add(d); }); - return getExpressionPrecedence(chunk.expression) <= 13 ? `( ${snippet} )` : snippet; + return chunk.getPrecedence() <= 13 ? `(${snippet})` : snippet; } }) .join(' + '); - if (allDependencies.size || hasChangeableIndex) { - const dependencies = Array.from(allDependencies); + if (propDependencies.size) { + const dependencies = Array.from(propDependencies); const condition = ( - ( block.hasOutroMethod ? `#outroing || ` : '' ) + + (block.hasOutroMethod ? `#outroing || ` : '') + dependencies.map(dependency => `changed.${dependency}`).join(' || ') ); @@ -310,10 +333,16 @@ export default class Attribute { }); } - isDynamic() { - if (this.value === true || this.value.length === 0) return false; - if (this.value.length > 1) return true; - return this.value[0].type !== 'Text'; + stringifyForSsr() { + return this.chunks + .map((chunk: Node) => { + if (chunk.type === 'Text') { + return escapeTemplate(escape(chunk.data).replace(/"/g, '"')); + } + + return '${@escape(' + chunk.snippet + ')}'; + }) + .join(''); } } diff --git a/src/generators/nodes/AwaitBlock.ts b/src/compile/nodes/AwaitBlock.ts similarity index 65% rename from src/generators/nodes/AwaitBlock.ts rename to src/compile/nodes/AwaitBlock.ts index 02847d93d6..87e1f7a850 100644 --- a/src/generators/nodes/AwaitBlock.ts +++ b/src/compile/nodes/AwaitBlock.ts @@ -1,21 +1,36 @@ import deindent from '../../utils/deindent'; import Node from './shared/Node'; -import { DomGenerator } from '../dom/index'; import Block from '../dom/Block'; import PendingBlock from './PendingBlock'; import ThenBlock from './ThenBlock'; import CatchBlock from './CatchBlock'; import createDebuggingComment from '../../utils/createDebuggingComment'; +import Expression from './shared/Expression'; +import { SsrTarget } from '../ssr'; export default class AwaitBlock extends Node { + expression: Expression; value: string; error: string; - expression: Node; pending: PendingBlock; then: ThenBlock; catch: CatchBlock; + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + + this.expression = new Expression(compiler, this, scope, info.expression); + const deps = this.expression.dependencies; + + this.value = info.value; + this.error = info.error; + + this.pending = new PendingBlock(compiler, this, scope, info.pending); + this.then = new ThenBlock(compiler, this, scope.add(this.value, deps), info.then); + this.catch = new CatchBlock(compiler, this, scope.add(this.error, deps), info.catch); + } + init( block: Block, stripWhitespace: boolean, @@ -24,42 +39,30 @@ export default class AwaitBlock extends Node { this.cannotUseInnerHTML(); this.var = block.getUniqueName('await_block'); - block.addDependencies(this.metadata.dependencies); + block.addDependencies(this.expression.dependencies); - let dynamic = false; + let isDynamic = false; - [ - ['pending', null], - ['then', this.value], - ['catch', this.error] - ].forEach(([status, arg]) => { + ['pending', 'then', 'catch'].forEach(status => { const child = this[status]; child.block = block.child({ - comment: createDebuggingComment(child, this.generator), - name: this.generator.getUniqueName(`create_${status}_block`), - contexts: new Map(block.contexts), - contextTypes: new Map(block.contextTypes) + comment: createDebuggingComment(child, this.compiler), + name: this.compiler.getUniqueName(`create_${status}_block`) }); - if (arg) { - child.block.context = arg; - child.block.contexts.set(arg, arg); // TODO should be using getUniqueName - child.block.contextTypes.set(arg, status); - } - child.initChildren(child.block, stripWhitespace, nextSibling); - this.generator.blocks.push(child.block); + this.compiler.target.blocks.push(child.block); if (child.block.dependencies.size > 0) { - dynamic = true; + isDynamic = true; block.addDependencies(child.block.dependencies); } }); - this.pending.block.hasUpdateMethod = dynamic; - this.then.block.hasUpdateMethod = dynamic; - this.catch.block.hasUpdateMethod = dynamic; + this.pending.block.hasUpdateMethod = isDynamic; + this.then.block.hasUpdateMethod = isDynamic; + this.catch.block.hasUpdateMethod = isDynamic; } build( @@ -72,8 +75,7 @@ export default class AwaitBlock extends Node { const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes); const updateMountNode = this.getUpdateMountNode(anchor); - block.contextualise(this.expression); - const { snippet } = this.metadata; + const { snippet } = this.expression; const promise = block.getUniqueName(`promise`); const resolved = block.getUniqueName(`resolved`); @@ -101,11 +103,11 @@ export default class AwaitBlock extends Node { // but it's probably not worth it block.builders.init.addBlock(deindent` - function ${replace_await_block}(${token}, type, state) { + function ${replace_await_block}(${token}, type, ctx) { if (${token} !== ${await_token}) return; var ${old_block} = ${await_block}; - ${await_block} = type && (${await_block_type} = type)(#component, state); + ${await_block} = type && (${await_block_type} = type)(#component, ctx); if (${old_block}) { ${old_block}.u(); @@ -117,23 +119,23 @@ export default class AwaitBlock extends Node { } } - function ${handle_promise}(${promise}, state) { + function ${handle_promise}(${promise}, ctx) { var ${token} = ${await_token} = {}; if (@isPromise(${promise})) { ${promise}.then(function(${value}) { - ${this.then.block.context ? deindent` - var state = #component.get(); - ${resolved} = { ${this.then.block.context}: ${value} }; - ${replace_await_block}(${token}, ${create_then_block}, @assign(@assign({}, state), ${resolved})); + ${this.value ? deindent` + var ctx = #component.get(); + ${resolved} = { ${this.value}: ${value} }; + ${replace_await_block}(${token}, ${create_then_block}, @assign(@assign({}, ctx), ${resolved})); ` : deindent` ${replace_await_block}(${token}, null, null); `} }, function (${error}) { - ${this.catch.block.context ? deindent` - var state = #component.get(); - ${resolved} = { ${this.catch.block.context}: ${error} }; - ${replace_await_block}(${token}, ${create_catch_block}, @assign(@assign({}, state), ${resolved})); + ${this.error ? deindent` + var ctx = #component.get(); + ${resolved} = { ${this.error}: ${error} }; + ${replace_await_block}(${token}, ${create_catch_block}, @assign(@assign({}, ctx), ${resolved})); ` : deindent` ${replace_await_block}(${token}, null, null); `} @@ -141,19 +143,19 @@ export default class AwaitBlock extends Node { // if we previously had a then/catch block, destroy it if (${await_block_type} !== ${create_pending_block}) { - ${replace_await_block}(${token}, ${create_pending_block}, state); + ${replace_await_block}(${token}, ${create_pending_block}, ctx); return true; } } else { - ${resolved} = { ${this.then.block.context}: ${promise} }; + ${resolved} = { ${this.value}: ${promise} }; if (${await_block_type} !== ${create_then_block}) { - ${replace_await_block}(${token}, ${create_then_block}, @assign(@assign({}, state), ${resolved})); + ${replace_await_block}(${token}, ${create_then_block}, @assign(@assign({}, ctx), ${resolved})); return true; } } } - ${handle_promise}(${promise} = ${snippet}, state); + ${handle_promise}(${promise} = ${snippet}, ctx); `); block.builders.create.addBlock(deindent` @@ -174,15 +176,15 @@ export default class AwaitBlock extends Node { `); const conditions = []; - if (this.metadata.dependencies) { + if (this.expression.dependencies.size > 0) { conditions.push( - `(${this.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})` + `(${[...this.expression.dependencies].map(dep => `'${dep}' in changed`).join(' || ')})` ); } conditions.push( `${promise} !== (${promise} = ${snippet})`, - `${handle_promise}(${promise}, state)` + `${handle_promise}(${promise}, ctx)` ); if (this.pending.block.hasUpdateMethod) { @@ -190,7 +192,7 @@ export default class AwaitBlock extends Node { if (${conditions.join(' && ')}) { // nothing } else { - ${await_block}.p(changed, @assign(@assign({}, state), ${resolved})); + ${await_block}.p(changed, @assign(@assign({}, ctx), ${resolved})); } `); } else { @@ -217,4 +219,23 @@ export default class AwaitBlock extends Node { }); }); } + + ssr() { + const target: SsrTarget = this.compiler.target; + const { snippet } = this.expression; + + target.append('${(function(__value) { if(@isPromise(__value)) return `'); + + this.pending.children.forEach((child: Node) => { + child.ssr(); + }); + + target.append('`; return function(ctx) { return `'); + + this.then.children.forEach((child: Node) => { + child.ssr(); + }); + + target.append(`\`;}(Object.assign({}, ctx, { ${this.value}: __value }));}(${snippet})) }`); + } } diff --git a/src/generators/nodes/Binding.ts b/src/compile/nodes/Binding.ts similarity index 68% rename from src/generators/nodes/Binding.ts rename to src/compile/nodes/Binding.ts index 1ac182102b..3c56da0f43 100644 --- a/src/generators/nodes/Binding.ts +++ b/src/compile/nodes/Binding.ts @@ -3,8 +3,9 @@ import Element from './Element'; import getObject from '../../utils/getObject'; import getTailSnippet from '../../utils/getTailSnippet'; import flattenReference from '../../utils/flattenReference'; -import { DomGenerator } from '../dom/index'; +import Compiler from '../Compiler'; import Block from '../dom/Block'; +import Expression from './shared/Expression'; const readOnlyMediaAttributes = new Set([ 'duration', @@ -15,12 +16,43 @@ const readOnlyMediaAttributes = new Set([ export default class Binding extends Node { name: string; - value: Node; - expression: Node; + value: Expression; + isContextual: boolean; + usesContext: boolean; + obj: string; + prop: string; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + + this.name = info.name; + this.value = new Expression(compiler, this, scope, info.value); + + let obj; + let prop; + + const { name } = getObject(this.value.node); + this.isContextual = scope.names.has(name); + + if (this.value.node.type === 'MemberExpression') { + prop = `[✂${this.value.node.property.start}-${this.value.node.property.end}✂]`; + if (!this.value.node.computed) prop = `'${prop}'`; + obj = `[✂${this.value.node.object.start}-${this.value.node.object.end}✂]`; + + this.usesContext = true; + } else { + obj = 'ctx'; + prop = `'${name}'`; + + this.usesContext = scope.names.has(name); + } + + this.obj = obj; + this.prop = prop; + } munge( - block: Block, - allUsedContexts: Set + block: Block ) { const node: Element = this.parent; @@ -29,32 +61,27 @@ export default class Binding extends Node { let updateCondition: string; - const { name } = getObject(this.value); - const { contexts } = block.contextualise(this.value); - const { snippet } = this.metadata; + const { name } = getObject(this.value.node); + const { snippet } = this.value; // special case: if you have e.g. `` // and `selected` is an object chosen with a if (binding.name === 'group') { - const bindingGroup = getBindingGroup(generator, binding.value); + const bindingGroup = getBindingGroup(compiler, binding.value.node); if (type === 'checkbox') { return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`; } diff --git a/src/compile/nodes/CatchBlock.ts b/src/compile/nodes/CatchBlock.ts new file mode 100644 index 0000000000..db15f7ceb1 --- /dev/null +++ b/src/compile/nodes/CatchBlock.ts @@ -0,0 +1,13 @@ +import Node from './shared/Node'; +import Block from '../dom/Block'; +import mapChildren from './shared/mapChildren'; + +export default class CatchBlock extends Node { + block: Block; + children: Node[]; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + this.children = mapChildren(compiler, parent, scope, info.children); + } +} \ No newline at end of file diff --git a/src/compile/nodes/Comment.ts b/src/compile/nodes/Comment.ts new file mode 100644 index 0000000000..ba20f5750a --- /dev/null +++ b/src/compile/nodes/Comment.ts @@ -0,0 +1,18 @@ +import Node from './shared/Node'; + +export default class Comment extends Node { + type: 'Comment'; + data: string; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + this.data = info.data; + } + + ssr() { + // Allow option to preserve comments, otherwise ignore + if (this.compiler.options.preserveComments) { + this.compiler.target.append(``); + } + } +} \ No newline at end of file diff --git a/src/compile/nodes/Component.ts b/src/compile/nodes/Component.ts new file mode 100644 index 0000000000..d40466a052 --- /dev/null +++ b/src/compile/nodes/Component.ts @@ -0,0 +1,616 @@ +import deindent from '../../utils/deindent'; +import flattenReference from '../../utils/flattenReference'; +import validCalleeObjects from '../../utils/validCalleeObjects'; +import stringifyProps from '../../utils/stringifyProps'; +import CodeBuilder from '../../utils/CodeBuilder'; +import getTailSnippet from '../../utils/getTailSnippet'; +import getObject from '../../utils/getObject'; +import quoteIfNecessary from '../../utils/quoteIfNecessary'; +import { escape, escapeTemplate, stringify } from '../../utils/stringify'; +import Node from './shared/Node'; +import Block from '../dom/Block'; +import Attribute from './Attribute'; +import usesThisOrArguments from '../../validate/js/utils/usesThisOrArguments'; +import mapChildren from './shared/mapChildren'; +import Binding from './Binding'; +import EventHandler from './EventHandler'; +import Expression from './shared/Expression'; +import { AppendTarget } from '../../interfaces'; + +export default class Component extends Node { + type: 'Component'; + name: string; + expression: Expression; + attributes: Attribute[]; + bindings: Binding[]; + handlers: EventHandler[]; + children: Node[]; + ref: string; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + + compiler.hasComponents = true; + + this.name = info.name; + + this.expression = this.name === 'svelte:component' + ? new Expression(compiler, this, scope, info.expression) + : null; + + this.attributes = []; + this.bindings = []; + this.handlers = []; + + info.attributes.forEach(node => { + switch (node.type) { + case 'Attribute': + case 'Spread': + this.attributes.push(new Attribute(compiler, this, scope, node)); + break; + + case 'Binding': + this.bindings.push(new Binding(compiler, this, scope, node)); + break; + + case 'EventHandler': + this.handlers.push(new EventHandler(compiler, this, scope, node)); + break; + + case 'Ref': + // TODO catch this in validation + if (this.ref) throw new Error(`Duplicate refs`); + + compiler.usesRefs = true + this.ref = node.name; + break; + + default: + throw new Error(`Not implemented: ${node.type}`); + } + }); + + this.children = mapChildren(compiler, this, scope, info.children); + } + + init( + block: Block, + stripWhitespace: boolean, + nextSibling: Node + ) { + this.cannotUseInnerHTML(); + + this.attributes.forEach(attr => { + block.addDependencies(attr.dependencies); + }); + + this.bindings.forEach(binding => { + block.addDependencies(binding.value.dependencies); + }); + + this.handlers.forEach(handler => { + block.addDependencies(handler.dependencies); + }); + + this.var = block.getUniqueName( + ( + this.name === 'svelte:self' ? this.compiler.name : + this.name === 'svelte:component' ? 'switch_instance' : + this.name + ).toLowerCase() + ); + + if (this.children.length) { + this._slots = new Set(['default']); + + this.children.forEach(child => { + child.init(block, stripWhitespace, nextSibling); + }); + } + } + + build( + block: Block, + parentNode: string, + parentNodes: string + ) { + const { compiler } = this; + + const name = this.var; + + const componentInitProperties = [`root: #component.root`]; + + if (this.children.length > 0) { + const slots = Array.from(this._slots).map(name => `${quoteIfNecessary(name)}: @createFragment()`); + componentInitProperties.push(`slots: { ${slots.join(', ')} }`); + + this.children.forEach((child: Node) => { + child.build(block, `${this.var}._slotted.default`, 'nodes'); + }); + } + + const statements: string[] = []; + + const name_initial_data = block.getUniqueName(`${name}_initial_data`); + const name_changes = block.getUniqueName(`${name}_changes`); + let name_updating: string; + let beforecreate: string = null; + + const updates: string[] = []; + + const usesSpread = !!this.attributes.find(a => a.isSpread); + + const attributeObject = usesSpread + ? '{}' + : stringifyProps( + this.attributes.map(attr => `${attr.name}: ${attr.getValue()}`) + ); + + if (this.attributes.length || this.bindings.length) { + componentInitProperties.push(`data: ${name_initial_data}`); + } + + if ((!usesSpread && this.attributes.filter(a => a.isDynamic).length) || this.bindings.length) { + updates.push(`var ${name_changes} = {};`); + } + + if (this.attributes.length) { + if (usesSpread) { + const levels = block.getUniqueName(`${this.var}_spread_levels`); + + const initialProps = []; + const changes = []; + + this.attributes.forEach(attr => { + const { name, dependencies } = attr; + + const condition = dependencies.size > 0 + ? [...dependencies].map(d => `changed.${d}`).join(' || ') + : null; + + if (attr.isSpread) { + const value = attr.expression.snippet; + initialProps.push(value); + + changes.push(condition ? `${condition} && ${value}` : value); + } else { + const obj = `{ ${quoteIfNecessary(name)}: ${attr.getValue()} }`; + initialProps.push(obj); + + changes.push(condition ? `${condition} && ${obj}` : obj); + } + }); + + block.addVariable(levels); + + statements.push(deindent` + ${levels} = [ + ${initialProps.join(',\n')} + ]; + + for (var #i = 0; #i < ${levels}.length; #i += 1) { + ${name_initial_data} = @assign(${name_initial_data}, ${levels}[#i]); + } + `); + + updates.push(deindent` + var ${name_changes} = @getSpreadUpdate(${levels}, [ + ${changes.join(',\n')} + ]); + `); + } else { + this.attributes + .filter((attribute: Attribute) => attribute.isDynamic) + .forEach((attribute: Attribute) => { + if (attribute.dependencies.size > 0) { + updates.push(deindent` + if (${[...attribute.dependencies] + .map(dependency => `changed.${dependency}`) + .join(' || ')}) ${name_changes}.${attribute.name} = ${attribute.getValue()}; + `); + } + + else { + // TODO this is an odd situation to encounter – I *think* it should only happen with + // each block indices, in which case it may be possible to optimise this + updates.push(`${name_changes}.${attribute.name} = ${attribute.getValue()};`); + } + }); + } + } + + if (this.bindings.length) { + compiler.target.hasComplexBindings = true; + + name_updating = block.alias(`${name}_updating`); + block.addVariable(name_updating, '{}'); + + let hasLocalBindings = false; + let hasStoreBindings = false; + + const builder = new CodeBuilder(); + + this.bindings.forEach((binding: Binding) => { + let { name: key } = getObject(binding.value.node); + + let setFromChild; + + if (binding.isContextual) { + const computed = isComputed(binding.value.node); + const tail = binding.value.node.type === 'MemberExpression' ? getTailSnippet(binding.value.node) : ''; + + const list = block.listNames.get(key); + const index = block.indexNames.get(key); + + const lhs = binding.value.node.type === 'MemberExpression' + ? binding.value.snippet + : `ctx.${list}[ctx.${index}]${tail} = childState.${binding.name}`; + + setFromChild = deindent` + ${lhs} = childState.${binding.name}; + + ${[...binding.value.dependencies] + .map((name: string) => { + const isStoreProp = name[0] === '$'; + const prop = isStoreProp ? name.slice(1) : name; + const newState = isStoreProp ? 'newStoreState' : 'newState'; + + if (isStoreProp) hasStoreBindings = true; + else hasLocalBindings = true; + + return `${newState}.${prop} = ctx.${name};`; + })} + `; + } + + else { + const isStoreProp = key[0] === '$'; + const prop = isStoreProp ? key.slice(1) : key; + const newState = isStoreProp ? 'newStoreState' : 'newState'; + + if (isStoreProp) hasStoreBindings = true; + else hasLocalBindings = true; + + if (binding.value.node.type === 'MemberExpression') { + setFromChild = deindent` + ${binding.value.snippet} = childState.${binding.name}; + ${newState}.${prop} = ctx.${key}; + `; + } + + else { + setFromChild = `${newState}.${prop} = childState.${binding.name};`; + } + } + + statements.push(deindent` + if (${binding.prop} in ${binding.obj}) { + ${name_initial_data}.${binding.name} = ${binding.value.snippet}; + ${name_updating}.${binding.name} = true; + }` + ); + + builder.addConditional( + `!${name_updating}.${binding.name} && changed.${binding.name}`, + setFromChild + ); + + updates.push(deindent` + if (!${name_updating}.${binding.name} && ${[...binding.value.dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')}) { + ${name_changes}.${binding.name} = ${binding.value.snippet}; + ${name_updating}.${binding.name} = true; + } + `); + }); + + block.maintainContext = true; // TODO put this somewhere more logical + + const initialisers = [ + hasLocalBindings && 'newState = {}', + hasStoreBindings && 'newStoreState = {}', + ].filter(Boolean).join(', '); + + // TODO use component.on('state', ...) instead of _bind + componentInitProperties.push(deindent` + _bind: function(changed, childState) { + var ${initialisers}; + ${builder} + ${hasStoreBindings && `#component.store.set(newStoreState);`} + ${hasLocalBindings && `#component._set(newState);`} + ${name_updating} = {}; + } + `); + + beforecreate = deindent` + #component.root._beforecreate.push(function() { + ${name}._bind({ ${this.bindings.map(b => `${b.name}: 1`).join(', ')} }, ${name}.get()); + }); + `; + } + + this.handlers.forEach(handler => { + handler.var = block.getUniqueName(`${this.var}_${handler.name}`); // TODO this is hacky + handler.render(compiler, block, false); // TODO hoist when possible + if (handler.usesContext) block.maintainContext = true; // TODO is there a better place to put this? + }); + + if (this.name === 'svelte:component') { + const switch_value = block.getUniqueName('switch_value'); + const switch_props = block.getUniqueName('switch_props'); + + const { dependencies, snippet } = this.expression; + + const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes); + + block.builders.init.addBlock(deindent` + var ${switch_value} = ${snippet}; + + function ${switch_props}(ctx) { + ${(this.attributes.length || this.bindings.length) && deindent` + var ${name_initial_data} = ${attributeObject};`} + ${statements} + return { + ${componentInitProperties.join(',\n')} + }; + } + + if (${switch_value}) { + var ${name} = new ${switch_value}(${switch_props}(ctx)); + + ${beforecreate} + } + + ${this.handlers.map(handler => deindent` + function ${handler.var}(event) { + ${handler.snippet} + } + + if (${name}) ${name}.on("${handler.name}", ${handler.var}); + `)} + `); + + block.builders.create.addLine( + `if (${name}) ${name}._fragment.c();` + ); + + if (parentNodes) { + block.builders.claim.addLine( + `if (${name}) ${name}._fragment.l(${parentNodes});` + ); + } + + block.builders.mount.addBlock(deindent` + if (${name}) { + ${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'}); + ${this.ref && `#component.refs.${this.ref} = ${name};`} + } + `); + + const updateMountNode = this.getUpdateMountNode(anchor); + + block.builders.update.addBlock(deindent` + if (${switch_value} !== (${switch_value} = ${snippet})) { + if (${name}) ${name}.destroy(); + + if (${switch_value}) { + ${name} = new ${switch_value}(${switch_props}(ctx)); + ${name}._fragment.c(); + + ${this.children.map(child => child.remount(name))} + ${name}._mount(${updateMountNode}, ${anchor}); + + ${this.handlers.map(handler => deindent` + ${name}.on("${handler.name}", ${handler.var}); + `)} + + ${this.ref && `#component.refs.${this.ref} = ${name};`} + } + + ${this.ref && deindent` + else if (#component.refs.${this.ref} === ${name}) { + #component.refs.${this.ref} = null; + }`} + } + `); + + if (updates.length) { + block.builders.update.addBlock(deindent` + else { + ${updates} + ${name}._set(${name_changes}); + ${this.bindings.length && `${name_updating} = {};`} + } + `); + } + + if (!parentNode) block.builders.unmount.addLine(`if (${name}) ${name}._unmount();`); + + block.builders.destroy.addLine(`if (${name}) ${name}.destroy(false);`); + } else { + const expression = this.name === 'svelte:self' + ? compiler.name + : `%components-${this.name}`; + + block.builders.init.addBlock(deindent` + ${(this.attributes.length || this.bindings.length) && deindent` + var ${name_initial_data} = ${attributeObject};`} + ${statements} + var ${name} = new ${expression}({ + ${componentInitProperties.join(',\n')} + }); + + ${beforecreate} + + ${this.handlers.map(handler => deindent` + ${name}.on("${handler.name}", function(event) { + ${handler.snippet || `#component.fire("${handler.name}", event);`} + }); + `)} + + ${this.ref && `#component.refs.${this.ref} = ${name};`} + `); + + block.builders.create.addLine(`${name}._fragment.c();`); + + if (parentNodes) { + block.builders.claim.addLine( + `${name}._fragment.l(${parentNodes});` + ); + } + + block.builders.mount.addLine( + `${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});` + ); + + if (updates.length) { + block.builders.update.addBlock(deindent` + ${updates} + ${name}._set(${name_changes}); + ${this.bindings.length && `${name_updating} = {};`} + `); + } + + if (!parentNode) block.builders.unmount.addLine(`${name}._unmount();`); + + block.builders.destroy.addLine(deindent` + ${name}.destroy(false); + ${this.ref && `if (#component.refs.${this.ref} === ${name}) #component.refs.${this.ref} = null;`} + `); + } + } + + remount(name: string) { + return `${this.var}._mount(${name}._slotted.default, null);`; + } + + ssr() { + function stringifyAttribute(chunk: Node) { + if (chunk.type === 'Text') { + return escapeTemplate(escape(chunk.data)); + } + + return '${@escape( ' + chunk.snippet + ')}'; + } + + const bindingProps = this.bindings.map(binding => { + const { name } = getObject(binding.value.node); + const tail = binding.value.node.type === 'MemberExpression' + ? getTailSnippet(binding.value.node) + : ''; + + return `${binding.name}: ctx.${name}${tail}`; + }); + + function getAttributeValue(attribute) { + if (attribute.isTrue) return `true`; + if (attribute.chunks.length === 0) return `''`; + + if (attribute.chunks.length === 1) { + const chunk = attribute.chunks[0]; + if (chunk.type === 'Text') { + return stringify(chunk.data); + } + + return chunk.snippet; + } + + return '`' + attribute.chunks.map(stringifyAttribute).join('') + '`'; + } + + const usesSpread = this.attributes.find(attr => attr.isSpread); + + const props = usesSpread + ? `Object.assign(${ + this.attributes + .map(attribute => { + if (attribute.isSpread) { + return attribute.expression.snippet; + } else { + return `{ ${attribute.name}: ${getAttributeValue(attribute)} }`; + } + }) + .concat(bindingProps.map(p => `{ ${p} }`)) + .join(', ') + })` + : `{ ${this.attributes + .map(attribute => `${attribute.name}: ${getAttributeValue(attribute)}`) + .concat(bindingProps) + .join(', ')} }`; + + const isDynamicComponent = this.name === 'svelte:component'; + + const expression = ( + this.name === 'svelte:self' ? this.compiler.name : + isDynamicComponent ? `((${this.expression.snippet}) || @missingComponent)` : + `%components-${this.name}` + ); + + this.bindings.forEach(binding => { + const conditions = []; + + let node = this; + while (node = node.parent) { + if (node.type === 'IfBlock') { + // TODO handle contextual bindings... + conditions.push(`(${node.expression.snippet})`); + } + } + + conditions.push(`!('${binding.name}' in ctx)`); + + const { name } = getObject(binding.value.node); + + this.compiler.target.bindings.push(deindent` + if (${conditions.reverse().join('&&')}) { + tmp = ${expression}.data(); + if ('${name}' in tmp) { + ctx.${binding.name} = tmp.${name}; + settled = false; + } + } + `); + }); + + let open = `\${${expression}._render(__result, ${props}`; + + const options = []; + options.push(`store: options.store`); + + if (this.children.length) { + const appendTarget: AppendTarget = { + slots: { default: '' }, + slotStack: ['default'] + }; + + this.compiler.target.appendTargets.push(appendTarget); + + this.children.forEach((child: Node) => { + child.ssr(); + }); + + const slotted = Object.keys(appendTarget.slots) + .map(name => `${name}: () => \`${appendTarget.slots[name]}\``) + .join(', '); + + options.push(`slotted: { ${slotted} }`); + + this.compiler.target.appendTargets.pop(); + } + + if (options.length) { + open += `, { ${options.join(', ')} }`; + } + + this.compiler.target.append(open); + this.compiler.target.append(')}'); + } +} + +function isComputed(node: Node) { + while (node.type === 'MemberExpression') { + if (node.computed) return true; + node = node.object; + } + + return false; +} diff --git a/src/generators/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts similarity index 77% rename from src/generators/nodes/EachBlock.ts rename to src/compile/nodes/EachBlock.ts index e74b8a01c2..cb58b31411 100644 --- a/src/generators/nodes/EachBlock.ts +++ b/src/compile/nodes/EachBlock.ts @@ -3,22 +3,57 @@ import Node from './shared/Node'; import ElseBlock from './ElseBlock'; import Block from '../dom/Block'; import createDebuggingComment from '../../utils/createDebuggingComment'; +import Expression from './shared/Expression'; +import mapChildren from './shared/mapChildren'; +import TemplateScope from './shared/TemplateScope'; export default class EachBlock extends Node { type: 'EachBlock'; block: Block; - expression: Node; + expression: Expression; iterations: string; index: string; context: string; key: string; + scope: TemplateScope; destructuredContexts: string[]; children: Node[]; else?: ElseBlock; + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + + this.expression = new Expression(compiler, this, scope, info.expression); + this.context = info.context; + this.index = info.index; + this.key = info.key; + + this.scope = scope.child(); + + this.scope.add(this.context, this.expression.dependencies); + + if (this.index) { + // index can only change if this is a keyed each block + const dependencies = this.key ? this.expression.dependencies : []; + this.scope.add(this.index, dependencies); + } + + // TODO more general approach to destructuring + this.destructuredContexts = info.destructuredContexts || []; + this.destructuredContexts.forEach(name => { + this.scope.add(name, this.expression.dependencies); + }); + + this.children = mapChildren(compiler, this, this.scope, info.children); + + this.else = info.else + ? new ElseBlock(compiler, this, this.scope, info.else) + : null; + } + init( block: Block, stripWhitespace: boolean, @@ -30,45 +65,26 @@ export default class EachBlock extends Node { this.iterations = block.getUniqueName(`${this.var}_blocks`); this.each_context = block.getUniqueName(`${this.var}_context`); - const { dependencies } = this.metadata; + const { dependencies } = this.expression; block.addDependencies(dependencies); this.block = block.child({ - comment: createDebuggingComment(this, this.generator), - name: this.generator.getUniqueName('create_each_block'), - context: this.context, + comment: createDebuggingComment(this, this.compiler), + name: this.compiler.getUniqueName('create_each_block'), key: this.key, - contexts: new Map(block.contexts), - contextTypes: new Map(block.contextTypes), - indexes: new Map(block.indexes), - changeableIndexes: new Map(block.changeableIndexes), - indexNames: new Map(block.indexNames), listNames: new Map(block.listNames) }); - const listName = this.generator.getUniqueName('each_value'); - const indexName = this.index || this.generator.getUniqueName(`${this.context}_index`); + const listName = this.compiler.getUniqueName('each_value'); + const indexName = this.index || this.compiler.getUniqueName(`${this.context}_index`); - this.block.contextTypes.set(this.context, 'each'); this.block.indexNames.set(this.context, indexName); this.block.listNames.set(this.context, listName); if (this.index) { this.block.getUniqueName(this.index); // this prevents name collisions (#1254) - this.block.indexes.set(this.index, this.context); - this.block.changeableIndexes.set(this.index, this.key); // TODO is this right? - } - - const context = this.block.getUniqueName(this.context); - this.block.contexts.set(this.context, context); // TODO this is now redundant? - - if (this.destructuredContexts) { - for (let i = 0; i < this.destructuredContexts.length; i += 1) { - const context = this.block.getUniqueName(this.destructuredContexts[i]); - this.block.contexts.set(this.destructuredContexts[i], context); - } } this.contextProps = [ @@ -83,18 +99,18 @@ export default class EachBlock extends Node { } } - this.generator.blocks.push(this.block); + this.compiler.target.blocks.push(this.block); this.initChildren(this.block, stripWhitespace, nextSibling); block.addDependencies(this.block.dependencies); this.block.hasUpdateMethod = this.block.dependencies.size > 0; if (this.else) { this.else.block = block.child({ - comment: createDebuggingComment(this.else, this.generator), - name: this.generator.getUniqueName(`${this.block.name}_else`), + comment: createDebuggingComment(this.else, this.compiler), + name: this.compiler.getUniqueName(`${this.block.name}_else`), }); - this.generator.blocks.push(this.else.block); + this.compiler.target.blocks.push(this.else.block); this.else.initChildren( this.else.block, stripWhitespace, @@ -111,7 +127,7 @@ export default class EachBlock extends Node { ) { if (this.children.length === 0) return; - const { generator } = this; + const { compiler } = this; const each = this.var; @@ -127,8 +143,8 @@ export default class EachBlock extends Node { // hack the sourcemap, so that if data is missing the bug // is easy to find let c = this.start + 2; - while (generator.source[c] !== 'e') c += 1; - generator.code.overwrite(c, c + 4, 'length'); + while (compiler.source[c] !== 'e') c += 1; + compiler.code.overwrite(c, c + 4, 'length'); const length = `[✂${c}-${c+4}✂]`; const mountOrIntro = this.block.hasIntroMethod ? 'i' : 'm'; @@ -142,8 +158,7 @@ export default class EachBlock extends Node { mountOrIntro, }; - block.contextualise(this.expression); - const { snippet } = this.metadata; + const { snippet } = this.expression; block.builders.init.addLine(`var ${each_block_value} = ${snippet};`); @@ -163,14 +178,14 @@ export default class EachBlock extends Node { } if (this.else) { - const each_block_else = generator.getUniqueName(`${each}_else`); + const each_block_else = compiler.getUniqueName(`${each}_else`); block.builders.init.addLine(`var ${each_block_else} = null;`); // TODO neaten this up... will end up with an empty line in the block block.builders.init.addBlock(deindent` if (!${each_block_value}.${length}) { - ${each_block_else} = ${this.else.block.name}(#component, state); + ${each_block_else} = ${this.else.block.name}(#component, ctx); ${each_block_else}.c(); } `); @@ -186,9 +201,9 @@ export default class EachBlock extends Node { if (this.else.block.hasUpdateMethod) { block.builders.update.addBlock(deindent` if (!${each_block_value}.${length} && ${each_block_else}) { - ${each_block_else}.p(changed, state); + ${each_block_else}.p(changed, ctx); } else if (!${each_block_value}.${length}) { - ${each_block_else} = ${this.else.block.name}(#component, state); + ${each_block_else} = ${this.else.block.name}(#component, ctx); ${each_block_else}.c(); ${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor}); } else if (${each_block_else}) { @@ -206,7 +221,7 @@ export default class EachBlock extends Node { ${each_block_else} = null; } } else if (!${each_block_else}) { - ${each_block_else} = ${this.else.block.name}(#component, state); + ${each_block_else} = ${this.else.block.name}(#component, ctx); ${each_block_else}.c(); ${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor}); } @@ -269,7 +284,7 @@ export default class EachBlock extends Node { block.builders.init.addBlock(deindent` for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) { var ${key} = ${each_block_value}[#i].${this.key}; - ${blocks}[#i] = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, @assign(@assign({}, state), { + ${blocks}[#i] = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, @assign(@assign({}, ctx), { ${this.contextProps.join(',\n')} })); } @@ -299,7 +314,7 @@ export default class EachBlock extends Node { var ${each_block_value} = ${snippet}; ${blocks} = @updateKeyedEach(${blocks}, #component, changed, "${this.key}", ${dynamic ? '1' : '0'}, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, function(#i) { - return @assign(@assign({}, state), { + return @assign(@assign({}, ctx), { ${this.contextProps.join(',\n')} }); }); @@ -334,7 +349,7 @@ export default class EachBlock extends Node { var ${iterations} = []; for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) { - ${iterations}[#i] = ${create_each_block}(#component, @assign(@assign({}, state), { + ${iterations}[#i] = ${create_each_block}(#component, @assign(@assign({}, ctx), { ${this.contextProps.join(',\n')} })); } @@ -365,7 +380,7 @@ export default class EachBlock extends Node { `); const allDependencies = new Set(this.block.dependencies); - const { dependencies } = this.metadata; + const { dependencies } = this.expression; dependencies.forEach((dependency: string) => { allDependencies.add(dependency); }); @@ -432,7 +447,7 @@ export default class EachBlock extends Node { if (${condition}) { for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) { - var ${this.each_context} = @assign(@assign({}, state), { + var ${this.each_context} = @assign(@assign({}, ctx), { ${this.contextProps.join(',\n')} }); @@ -457,4 +472,36 @@ export default class EachBlock extends Node { // TODO consider keyed blocks return `for (var #i = 0; #i < ${this.iterations}.length; #i += 1) ${this.iterations}[#i].m(${name}._slotted.default, null);`; } + + ssr() { + const { compiler } = this; + const { snippet } = this.expression; + + const props = [`${this.context}: item`] + .concat(this.destructuredContexts.map((name, i) => `${name}: item[${i}]`)); + + const getContext = this.index + ? `(item, i) => Object.assign({}, ctx, { ${props.join(', ')}, ${this.index}: i })` + : `item => Object.assign({}, ctx, { ${props.join(', ')} })`; + + const open = `\${ ${this.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${getContext}, ctx => \``; + compiler.target.append(open); + + this.children.forEach((child: Node) => { + child.ssr(); + }); + + const close = `\`)`; + compiler.target.append(close); + + if (this.else) { + compiler.target.append(` : \``); + this.else.children.forEach((child: Node) => { + child.ssr(); + }); + compiler.target.append(`\``); + } + + compiler.target.append('}'); + } } diff --git a/src/generators/nodes/Element.ts b/src/compile/nodes/Element.ts similarity index 58% rename from src/generators/nodes/Element.ts rename to src/compile/nodes/Element.ts index a44c949816..a025eb7b05 100644 --- a/src/generators/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -6,24 +6,132 @@ import validCalleeObjects from '../../utils/validCalleeObjects'; import reservedNames from '../../utils/reservedNames'; import fixAttributeCasing from '../../utils/fixAttributeCasing'; import quoteIfNecessary from '../../utils/quoteIfNecessary'; -import mungeAttribute from './shared/mungeAttribute'; +import Compiler from '../Compiler'; import Node from './shared/Node'; import Block from '../dom/Block'; import Attribute from './Attribute'; import Binding from './Binding'; import EventHandler from './EventHandler'; -import Ref from './Ref'; import Transition from './Transition'; import Action from './Action'; import Text from './Text'; import * as namespaces from '../../utils/namespaces'; +import mapChildren from './shared/mapChildren'; + +// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7 +const booleanAttributes = new Set('async autocomplete autofocus autoplay border challenge checked compact contenteditable controls default defer disabled formnovalidate frameborder hidden indeterminate ismap loop multiple muted nohref noresize noshade novalidate nowrap open readonly required reversed scoped scrolling seamless selected sortable spellcheck translate'.split(' ')); export default class Element extends Node { type: 'Element'; name: string; - attributes: (Attribute | Binding | EventHandler | Ref | Transition | Action)[]; // TODO split these up sooner + scope: any; // TODO + attributes: Attribute[]; + actions: Action[]; + bindings: Binding[]; + handlers: EventHandler[]; + intro: Transition; + outro: Transition; children: Node[]; + ref: string; + namespace: string; + + constructor(compiler, parent, scope, info: any) { + super(compiler, parent, scope, info); + this.name = info.name; + this.scope = scope; + + const parentElement = parent.findNearest(/^Element/); + this.namespace = this.name === 'svg' ? + namespaces.svg : + parentElement ? parentElement.namespace : this.compiler.namespace; + + this.attributes = []; + this.actions = []; + this.bindings = []; + this.handlers = []; + + this.intro = null; + this.outro = null; + + if (this.name === 'textarea') { + // this is an egregious hack, but it's the easiest way to get