From 9b70523529580449cee791d706539a4d7e1a34dc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 28 Apr 2018 19:53:57 -0400 Subject: [PATCH] rename Generator -> Compiler --- src/Stats.ts | 10 +- .../Generator.ts => compile/Compiler.ts} | 2 +- src/{generators => compile}/dom/Block.ts | 12 +- src/{generators => compile}/dom/index.ts | 96 ++-- src/{generators => compile}/nodes/Action.ts | 0 .../nodes/Attribute.ts | 4 +- .../nodes/AwaitBlock.ts | 10 +- src/compile/nodes/Binding.ts | 296 ++++++++++ .../nodes/CatchBlock.ts | 0 src/{generators => compile}/nodes/Comment.ts | 0 .../nodes/Component.ts | 0 .../nodes/EachBlock.ts | 0 src/{generators => compile}/nodes/Element.ts | 5 +- .../nodes/ElseBlock.ts | 0 .../nodes/EventHandler.ts | 0 src/compile/nodes/Fragment.ts | 45 ++ src/{generators => compile}/nodes/Head.ts | 0 src/compile/nodes/IfBlock.ts | 505 ++++++++++++++++++ .../nodes/MustacheTag.ts | 0 .../nodes/PendingBlock.ts | 0 .../nodes/RawMustacheTag.ts | 0 src/{generators => compile}/nodes/Slot.ts | 0 src/{generators => compile}/nodes/Text.ts | 0 .../nodes/ThenBlock.ts | 0 src/{generators => compile}/nodes/Title.ts | 0 .../nodes/Transition.ts | 0 src/{generators => compile}/nodes/Window.ts | 0 .../nodes/shared/Expression.ts | 4 +- src/compile/nodes/shared/Node.ts | 172 ++++++ .../nodes/shared/Tag.ts | 0 .../nodes/shared/TemplateScope.ts | 0 .../nodes/shared/mapChildren.ts | 0 src/compile/server-side-rendering/index.ts | 175 ++++++ src/compile/shared.ts | 79 +++ src/{generators => compile}/wrapModule.ts | 0 src/css/Stylesheet.ts | 2 +- src/generators/nodes/Binding.ts | 8 +- src/generators/nodes/Fragment.ts | 7 +- src/generators/nodes/IfBlock.ts | 1 - src/generators/nodes/shared/Node.ts | 7 +- src/generators/server-side-rendering/index.ts | 18 +- src/index.ts | 4 +- src/shared/_build.js | 2 +- src/utils/createDebuggingComment.ts | 4 +- 44 files changed, 1369 insertions(+), 99 deletions(-) rename src/{generators/Generator.ts => compile/Compiler.ts} (99%) rename src/{generators => compile}/dom/Block.ts (96%) rename src/{generators => compile}/dom/index.ts (74%) rename src/{generators => compile}/nodes/Action.ts (100%) rename src/{generators => compile}/nodes/Attribute.ts (99%) rename src/{generators => compile}/nodes/AwaitBlock.ts (95%) create mode 100644 src/compile/nodes/Binding.ts rename src/{generators => compile}/nodes/CatchBlock.ts (100%) rename src/{generators => compile}/nodes/Comment.ts (100%) rename src/{generators => compile}/nodes/Component.ts (100%) rename src/{generators => compile}/nodes/EachBlock.ts (100%) rename src/{generators => compile}/nodes/Element.ts (99%) rename src/{generators => compile}/nodes/ElseBlock.ts (100%) rename src/{generators => compile}/nodes/EventHandler.ts (100%) create mode 100644 src/compile/nodes/Fragment.ts rename src/{generators => compile}/nodes/Head.ts (100%) create mode 100644 src/compile/nodes/IfBlock.ts rename src/{generators => compile}/nodes/MustacheTag.ts (100%) rename src/{generators => compile}/nodes/PendingBlock.ts (100%) rename src/{generators => compile}/nodes/RawMustacheTag.ts (100%) rename src/{generators => compile}/nodes/Slot.ts (100%) rename src/{generators => compile}/nodes/Text.ts (100%) rename src/{generators => compile}/nodes/ThenBlock.ts (100%) rename src/{generators => compile}/nodes/Title.ts (100%) rename src/{generators => compile}/nodes/Transition.ts (100%) rename src/{generators => compile}/nodes/Window.ts (100%) rename src/{generators => compile}/nodes/shared/Expression.ts (98%) create mode 100644 src/compile/nodes/shared/Node.ts rename src/{generators => compile}/nodes/shared/Tag.ts (100%) rename src/{generators => compile}/nodes/shared/TemplateScope.ts (100%) rename src/{generators => compile}/nodes/shared/mapChildren.ts (100%) create mode 100644 src/compile/server-side-rendering/index.ts create mode 100644 src/compile/shared.ts rename src/{generators => compile}/wrapModule.ts (100%) 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 99% rename from src/generators/Generator.ts rename to src/compile/Compiler.ts index d1b48174ec..6d0f614598 100644 --- a/src/generators/Generator.ts +++ b/src/compile/Compiler.ts @@ -78,7 +78,7 @@ function removeIndentation( childKeys.EachBlock = childKeys.IfBlock = ['children', 'else']; childKeys.Attribute = ['value']; -export default class Generator { +export default class Compiler { stats: Stats; ast: Ast; diff --git a/src/generators/dom/Block.ts b/src/compile/dom/Block.ts similarity index 96% rename from src/generators/dom/Block.ts rename to src/compile/dom/Block.ts index 15833e73fd..f41892ccc9 100644 --- a/src/generators/dom/Block.ts +++ b/src/compile/dom/Block.ts @@ -1,12 +1,12 @@ 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'; export interface BlockOptions { name: string; - generator?: DomGenerator; + compiler?: Compiler; comment?: string; key?: string; indexNames?: Map; @@ -15,7 +15,7 @@ export interface BlockOptions { } export default class Block { - generator: DomGenerator; + compiler: Compiler; name: string; comment?: string; @@ -52,7 +52,7 @@ export default class Block { autofocus: string; constructor(options: BlockOptions) { - this.generator = options.generator; + this.compiler = options.compiler; this.name = options.name; this.comment = options.comment; @@ -83,7 +83,7 @@ export default class Block { this.hasOutroMethod = false; this.outros = 0; - this.getUniqueName = this.generator.getUniqueNameMaker(); + this.getUniqueName = this.compiler.getUniqueNameMaker(); this.variables = new Map(); this.aliases = new Map() @@ -187,7 +187,7 @@ export default class Block { `); } - if (this.generator.options.hydratable) { + if (this.compiler.options.hydratable) { if (this.builders.claim.isEmpty() && this.builders.hydrate.isEmpty()) { properties.addBlock(`l: @noop,`); } else { diff --git a/src/generators/dom/index.ts b/src/compile/dom/index.ts similarity index 74% rename from src/generators/dom/index.ts rename to src/compile/dom/index.ts index dcf3c7fe4b..6dce884a09 100644 --- a/src/generators/dom/index.ts +++ b/src/compile/dom/index.ts @@ -9,7 +9,7 @@ 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'; @@ -44,17 +44,17 @@ export default function dom( const format = options.format || 'es'; const target = new DomTarget(); - const generator = new Generator(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, true, target); + const compiler = new Compiler(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, true, target); const { computations, name, templateProperties, namespace, - } = generator; + } = compiler; - generator.fragment.build(); - const { block } = generator.fragment; + compiler.fragment.build(); + const { block } = compiler.fragment; // prevent fragment being created twice (#1063) if (options.customElement) block.builders.create.addLine(`this.c = @noop;`); @@ -86,20 +86,20 @@ 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); } @@ -125,10 +125,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 = []; @@ -153,31 +153,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)}"`)}]);`} ${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];`} @@ -188,26 +188,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 || target.hasComplexBindings || target.hasIntroTransitions) && deindent` + ${(hasInitHooks || compiler.hasComponents || target.hasComplexBindings || target.hasIntroTransitions) && deindent` if (!options.root) { this._oncreate = []; - ${(generator.hasComponents || target.hasComplexBindings) && `this._beforecreate = [];`} - ${(generator.hasComponents || target.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); @@ -219,14 +219,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.options.hydratable + ${compiler.options.hydratable ? deindent` var nodes = @children(options.target); options.hydrate ? this._fragment.l(nodes) : this._fragment.c(); @@ -238,19 +238,19 @@ export default function dom( `} this._mount(options.target, options.anchor); - ${(generator.hasComponents || target.hasComplexBindings || hasInitHooks || target.hasIntroTransitions) && deindent` - ${generator.hasComponents && `this._lock = true;`} - ${(generator.hasComponents || target.hasComplexBindings) && `@callAll(this._beforecreate);`} - ${(generator.hasComponents || hasInitHooks) && `@callAll(this._oncreate);`} - ${(generator.hasComponents || target.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 { @@ -273,7 +273,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]); @@ -284,18 +284,18 @@ export default function dom( this.set({ [attr]: newValue }); } - ${(generator.hasComponents || target.hasComplexBindings || templateProperties.oncreate || target.hasIntroTransitions) && deindent` + ${(compiler.hasComponents || target.hasComplexBindings || templateProperties.oncreate || target.hasIntroTransitions) && deindent` connectedCallback() { - ${generator.hasComponents && `this._lock = true;`} - ${(generator.hasComponents || target.hasComplexBindings) && `@callAll(this._beforecreate);`} - ${(generator.hasComponents || templateProperties.oncreate) && `@callAll(this._oncreate);`} - ${(generator.hasComponents || target.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); @@ -322,7 +322,7 @@ export default function dom( builder.addBlock(deindent` ${options.dev && deindent` ${name}.prototype._checkReadOnly = function _checkReadOnly(newState) { - ${Array.from(generator.target.readonly).map( + ${Array.from(target.readonly).map( prop => `if ('${prop}' in newState && !this._updatingReadonlyProperty) throw new Error("${debugName}: Cannot set read-only property '${prop}'");` )} @@ -348,7 +348,7 @@ export default function dom( 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, name, diff --git a/src/generators/nodes/Action.ts b/src/compile/nodes/Action.ts similarity index 100% rename from src/generators/nodes/Action.ts rename to src/compile/nodes/Action.ts diff --git a/src/generators/nodes/Attribute.ts b/src/compile/nodes/Attribute.ts similarity index 99% rename from src/generators/nodes/Attribute.ts rename to src/compile/nodes/Attribute.ts index 5552dce0f5..37de5068a9 100644 --- a/src/generators/nodes/Attribute.ts +++ b/src/compile/nodes/Attribute.ts @@ -2,7 +2,7 @@ import deindent from '../../utils/deindent'; import { escape, escapeTemplate, stringify } from '../../utils/stringify'; import fixAttributeCasing from '../../utils/fixAttributeCasing'; import addToSet from '../../utils/addToSet'; -import { DomGenerator } from '../dom/index'; +import Compiler from '../Compiler'; import Node from './shared/Node'; import Element from './Element'; import Text from './Text'; @@ -19,7 +19,7 @@ export default class Attribute extends Node { start: number; end: number; - compiler: DomGenerator; + compiler: Compiler; parent: Element; name: string; isSpread: boolean; diff --git a/src/generators/nodes/AwaitBlock.ts b/src/compile/nodes/AwaitBlock.ts similarity index 95% rename from src/generators/nodes/AwaitBlock.ts rename to src/compile/nodes/AwaitBlock.ts index f3cd3fa7ce..bac863697c 100644 --- a/src/generators/nodes/AwaitBlock.ts +++ b/src/compile/nodes/AwaitBlock.ts @@ -1,12 +1,12 @@ 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 '../server-side-rendering'; export default class AwaitBlock extends Node { expression: Expression; @@ -221,21 +221,21 @@ export default class AwaitBlock extends Node { } ssr() { - const { compiler } = this; + const target: SsrTarget = this.compiler.target; const { snippet } = this.expression; - compiler.target.append('${(function(__value) { if(@isPromise(__value)) return `'); + target.append('${(function(__value) { if(@isPromise(__value)) return `'); this.pending.children.forEach((child: Node) => { child.ssr(); }); - compiler.target.append('`; return function(ctx) { return `'); + target.append('`; return function(ctx) { return `'); this.then.children.forEach((child: Node) => { child.ssr(); }); - compiler.target.append(`\`;}(Object.assign({}, ctx, { ${this.value}: __value }));}(${snippet})) }`); + target.append(`\`;}(Object.assign({}, ctx, { ${this.value}: __value }));}(${snippet})) }`); } } diff --git a/src/compile/nodes/Binding.ts b/src/compile/nodes/Binding.ts new file mode 100644 index 0000000000..3c56da0f43 --- /dev/null +++ b/src/compile/nodes/Binding.ts @@ -0,0 +1,296 @@ +import Node from './shared/Node'; +import Element from './Element'; +import getObject from '../../utils/getObject'; +import getTailSnippet from '../../utils/getTailSnippet'; +import flattenReference from '../../utils/flattenReference'; +import Compiler from '../Compiler'; +import Block from '../dom/Block'; +import Expression from './shared/Expression'; + +const readOnlyMediaAttributes = new Set([ + 'duration', + 'buffered', + 'seekable', + 'played' +]); + +export default class Binding extends Node { + name: string; + 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 + ) { + const node: Element = this.parent; + + const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(node.getStaticAttributeValue('type')); + const isReadOnly = node.isMediaNode() && readOnlyMediaAttributes.has(this.name); + + let updateCondition: string; + + 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(compiler, binding.value.node); + if (type === 'checkbox') { + return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`; + } + + return `${node.var}.__value`; + } + + // + if (type === 'range' || type === 'number') { + return `@toNumber(${node.var}.${binding.name})`; + } + + if ((binding.name === 'buffered' || binding.name === 'seekable' || binding.name === 'played')) { + return `@timeRangesToArray(${node.var}.${binding.name})` + } + + // everything else + return `${node.var}.${binding.name}`; +} + +function isComputed(node: Node) { + while (node.type === 'MemberExpression') { + if (node.computed) return true; + node = node.object; + } + + return false; +} diff --git a/src/generators/nodes/CatchBlock.ts b/src/compile/nodes/CatchBlock.ts similarity index 100% rename from src/generators/nodes/CatchBlock.ts rename to src/compile/nodes/CatchBlock.ts diff --git a/src/generators/nodes/Comment.ts b/src/compile/nodes/Comment.ts similarity index 100% rename from src/generators/nodes/Comment.ts rename to src/compile/nodes/Comment.ts diff --git a/src/generators/nodes/Component.ts b/src/compile/nodes/Component.ts similarity index 100% rename from src/generators/nodes/Component.ts rename to src/compile/nodes/Component.ts diff --git a/src/generators/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts similarity index 100% rename from src/generators/nodes/EachBlock.ts rename to src/compile/nodes/EachBlock.ts diff --git a/src/generators/nodes/Element.ts b/src/compile/nodes/Element.ts similarity index 99% rename from src/generators/nodes/Element.ts rename to src/compile/nodes/Element.ts index f13e98fea8..a025eb7b05 100644 --- a/src/generators/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -6,6 +6,7 @@ import validCalleeObjects from '../../utils/validCalleeObjects'; import reservedNames from '../../utils/reservedNames'; import fixAttributeCasing from '../../utils/fixAttributeCasing'; import quoteIfNecessary from '../../utils/quoteIfNecessary'; +import Compiler from '../Compiler'; import Node from './shared/Node'; import Block from '../dom/Block'; import Attribute from './Attribute'; @@ -915,7 +916,7 @@ export default class Element extends Node { } function getRenderStatement( - compiler: DomGenerator, + compiler: Compiler, namespace: string, name: string ) { @@ -931,7 +932,7 @@ function getRenderStatement( } function getClaimStatement( - compiler: DomGenerator, + compiler: Compiler, namespace: string, nodes: string, node: Node diff --git a/src/generators/nodes/ElseBlock.ts b/src/compile/nodes/ElseBlock.ts similarity index 100% rename from src/generators/nodes/ElseBlock.ts rename to src/compile/nodes/ElseBlock.ts diff --git a/src/generators/nodes/EventHandler.ts b/src/compile/nodes/EventHandler.ts similarity index 100% rename from src/generators/nodes/EventHandler.ts rename to src/compile/nodes/EventHandler.ts diff --git a/src/compile/nodes/Fragment.ts b/src/compile/nodes/Fragment.ts new file mode 100644 index 0000000000..f0de86fa97 --- /dev/null +++ b/src/compile/nodes/Fragment.ts @@ -0,0 +1,45 @@ +import Node from './shared/Node'; +import Compiler from '../Compiler'; +import mapChildren from './shared/mapChildren'; +import Block from '../dom/Block'; +import TemplateScope from './shared/TemplateScope'; + +export default class Fragment extends Node { + block: Block; + children: Node[]; + scope: TemplateScope; + + constructor(compiler: Compiler, info: any) { + const scope = new TemplateScope(); + super(compiler, null, scope, info); + + this.scope = scope; + this.children = mapChildren(compiler, this, scope, info.children); + } + + init() { + this.block = new Block({ + compiler: this.compiler, + name: '@create_main_fragment', + key: null, + + indexNames: new Map(), + listNames: new Map(), + + dependencies: new Set(), + }); + + this.compiler.target.blocks.push(this.block); + this.initChildren(this.block, true, null); + + this.block.hasUpdateMethod = true; + } + + build() { + this.init(); + + this.children.forEach(child => { + child.build(this.block, null, 'nodes'); + }); + } +} \ No newline at end of file diff --git a/src/generators/nodes/Head.ts b/src/compile/nodes/Head.ts similarity index 100% rename from src/generators/nodes/Head.ts rename to src/compile/nodes/Head.ts diff --git a/src/compile/nodes/IfBlock.ts b/src/compile/nodes/IfBlock.ts new file mode 100644 index 0000000000..6c89ed9848 --- /dev/null +++ b/src/compile/nodes/IfBlock.ts @@ -0,0 +1,505 @@ +import deindent from '../../utils/deindent'; +import Node from './shared/Node'; +import ElseBlock from './ElseBlock'; +import Compiler from '../Compiler'; +import Block from '../dom/Block'; +import createDebuggingComment from '../../utils/createDebuggingComment'; +import Expression from './shared/Expression'; +import mapChildren from './shared/mapChildren'; + +function isElseIf(node: ElseBlock) { + return ( + node && node.children.length === 1 && node.children[0].type === 'IfBlock' + ); +} + +function isElseBranch(branch) { + return branch.block && !branch.condition; +} + +export default class IfBlock extends Node { + type: 'IfBlock'; + expression: Expression; + children: any[]; + else: ElseBlock; + + block: Block; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + + this.expression = new Expression(compiler, this, scope, info.expression); + this.children = mapChildren(compiler, this, scope, info.children); + + this.else = info.else + ? new ElseBlock(compiler, this, scope, info.else) + : null; + } + + init( + block: Block, + stripWhitespace: boolean, + nextSibling: Node + ) { + const { compiler } = this; + + this.cannotUseInnerHTML(); + + const blocks: Block[] = []; + let dynamic = false; + let hasIntros = false; + let hasOutros = false; + + function attachBlocks(node: IfBlock) { + node.var = block.getUniqueName(`if_block`); + + block.addDependencies(node.expression.dependencies); + + node.block = block.child({ + comment: createDebuggingComment(node, compiler), + name: compiler.getUniqueName(`create_if_block`), + }); + + blocks.push(node.block); + node.initChildren(node.block, stripWhitespace, nextSibling); + + if (node.block.dependencies.size > 0) { + dynamic = true; + block.addDependencies(node.block.dependencies); + } + + if (node.block.hasIntroMethod) hasIntros = true; + if (node.block.hasOutroMethod) hasOutros = true; + + if (isElseIf(node.else)) { + attachBlocks(node.else.children[0]); + } else if (node.else) { + node.else.block = block.child({ + comment: createDebuggingComment(node.else, compiler), + name: compiler.getUniqueName(`create_if_block`), + }); + + blocks.push(node.else.block); + node.else.initChildren( + node.else.block, + stripWhitespace, + nextSibling + ); + + if (node.else.block.dependencies.size > 0) { + dynamic = true; + block.addDependencies(node.else.block.dependencies); + } + } + } + + attachBlocks(this); + + blocks.forEach(block => { + block.hasUpdateMethod = dynamic; + block.hasIntroMethod = hasIntros; + block.hasOutroMethod = hasOutros; + }); + + compiler.target.blocks.push(...blocks); + } + + build( + block: Block, + parentNode: string, + parentNodes: string + ) { + const name = this.var; + + const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode(); + const anchor = needsAnchor + ? block.getUniqueName(`${name}_anchor`) + : (this.next && this.next.var) || 'null'; + + const branches = this.getBranches(block, parentNode, parentNodes, this); + + const hasElse = isElseBranch(branches[branches.length - 1]); + const if_name = hasElse ? '' : `if (${name}) `; + + const dynamic = branches[0].hasUpdateMethod; // can use [0] as proxy for all, since they necessarily have the same value + const hasOutros = branches[0].hasOutroMethod; + + const vars = { name, anchor, if_name, hasElse }; + + if (this.else) { + if (hasOutros) { + this.buildCompoundWithOutros(block, parentNode, parentNodes, branches, dynamic, vars); + } else { + this.buildCompound(block, parentNode, parentNodes, branches, dynamic, vars); + } + } else { + this.buildSimple(block, parentNode, parentNodes, branches[0], dynamic, vars); + } + + block.builders.create.addLine(`${if_name}${name}.c();`); + + if (parentNodes) { + block.builders.claim.addLine( + `${if_name}${name}.l(${parentNodes});` + ); + } + + if (needsAnchor) { + block.addElement( + anchor, + `@createComment()`, + parentNodes && `@createComment()`, + parentNode + ); + } + } + + buildCompound( + block: Block, + parentNode: string, + parentNodes: string, + branches, + dynamic, + { name, anchor, hasElse, if_name } + ) { + const select_block_type = this.compiler.getUniqueName(`select_block_type`); + const current_block_type = block.getUniqueName(`current_block_type`); + const current_block_type_and = hasElse ? '' : `${current_block_type} && `; + + block.builders.init.addBlock(deindent` + function ${select_block_type}(ctx) { + ${branches + .map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block};`) + .join('\n')} + } + `); + + block.builders.init.addBlock(deindent` + var ${current_block_type} = ${select_block_type}(ctx); + var ${name} = ${current_block_type_and}${current_block_type}(#component, ctx); + `); + + const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm'; + + const initialMountNode = parentNode || '#target'; + const anchorNode = parentNode ? 'null' : 'anchor'; + block.builders.mount.addLine( + `${if_name}${name}.${mountOrIntro}(${initialMountNode}, ${anchorNode});` + ); + + const updateMountNode = this.getUpdateMountNode(anchor); + + const changeBlock = deindent` + ${hasElse + ? deindent` + ${name}.u(); + ${name}.d(); + ` + : deindent` + if (${name}) { + ${name}.u(); + ${name}.d(); + }`} + ${name} = ${current_block_type_and}${current_block_type}(#component, ctx); + ${if_name}${name}.c(); + ${if_name}${name}.${mountOrIntro}(${updateMountNode}, ${anchor}); + `; + + if (dynamic) { + block.builders.update.addBlock(deindent` + if (${current_block_type} === (${current_block_type} = ${select_block_type}(ctx)) && ${name}) { + ${name}.p(changed, ctx); + } else { + ${changeBlock} + } + `); + } else { + block.builders.update.addBlock(deindent` + if (${current_block_type} !== (${current_block_type} = ${select_block_type}(ctx))) { + ${changeBlock} + } + `); + } + + block.builders.unmount.addLine(`${if_name}${name}.u();`); + + block.builders.destroy.addLine(`${if_name}${name}.d();`); + } + + // if any of the siblings have outros, we need to keep references to the blocks + // (TODO does this only apply to bidi transitions?) + buildCompoundWithOutros( + block: Block, + parentNode: string, + parentNodes: string, + branches, + dynamic, + { name, anchor, hasElse } + ) { + const select_block_type = block.getUniqueName(`select_block_type`); + const current_block_type_index = block.getUniqueName(`current_block_type_index`); + const previous_block_index = block.getUniqueName(`previous_block_index`); + const if_block_creators = block.getUniqueName(`if_block_creators`); + const if_blocks = block.getUniqueName(`if_blocks`); + + const if_current_block_type_index = hasElse + ? '' + : `if (~${current_block_type_index}) `; + + block.addVariable(current_block_type_index); + block.addVariable(name); + + block.builders.init.addBlock(deindent` + var ${if_block_creators} = [ + ${branches.map(branch => branch.block).join(',\n')} + ]; + + var ${if_blocks} = []; + + function ${select_block_type}(ctx) { + ${branches + .map(({ condition, block }, i) => `${condition ? `if (${condition}) ` : ''}return ${block ? i : -1};`) + .join('\n')} + } + `); + + if (hasElse) { + block.builders.init.addBlock(deindent` + ${current_block_type_index} = ${select_block_type}(ctx); + ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx); + `); + } else { + block.builders.init.addBlock(deindent` + if (~(${current_block_type_index} = ${select_block_type}(ctx))) { + ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx); + } + `); + } + + const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm'; + const initialMountNode = parentNode || '#target'; + const anchorNode = parentNode ? 'null' : 'anchor'; + + block.builders.mount.addLine( + `${if_current_block_type_index}${if_blocks}[${current_block_type_index}].${mountOrIntro}(${initialMountNode}, ${anchorNode});` + ); + + const updateMountNode = this.getUpdateMountNode(anchor); + + const destroyOldBlock = deindent` + ${name}.o(function() { + ${if_blocks}[ ${previous_block_index} ].u(); + ${if_blocks}[ ${previous_block_index} ].d(); + ${if_blocks}[ ${previous_block_index} ] = null; + }); + `; + + const createNewBlock = deindent` + ${name} = ${if_blocks}[${current_block_type_index}]; + if (!${name}) { + ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx); + ${name}.c(); + } + ${name}.${mountOrIntro}(${updateMountNode}, ${anchor}); + `; + + const changeBlock = hasElse + ? deindent` + ${destroyOldBlock} + + ${createNewBlock} + ` + : deindent` + if (${name}) { + ${destroyOldBlock} + } + + if (~${current_block_type_index}) { + ${createNewBlock} + } else { + ${name} = null; + } + `; + + if (dynamic) { + block.builders.update.addBlock(deindent` + var ${previous_block_index} = ${current_block_type_index}; + ${current_block_type_index} = ${select_block_type}(ctx); + if (${current_block_type_index} === ${previous_block_index}) { + ${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, ctx); + } else { + ${changeBlock} + } + `); + } else { + block.builders.update.addBlock(deindent` + var ${previous_block_index} = ${current_block_type_index}; + ${current_block_type_index} = ${select_block_type}(ctx); + if (${current_block_type_index} !== ${previous_block_index}) { + ${changeBlock} + } + `); + } + + block.builders.destroy.addLine(deindent` + ${if_current_block_type_index}{ + ${if_blocks}[${current_block_type_index}].u(); + ${if_blocks}[${current_block_type_index}].d(); + } + `); + } + + buildSimple( + block: Block, + parentNode: string, + parentNodes: string, + branch, + dynamic, + { name, anchor, if_name } + ) { + block.builders.init.addBlock(deindent` + var ${name} = (${branch.condition}) && ${branch.block}(#component, ctx); + `); + + const mountOrIntro = branch.hasIntroMethod ? 'i' : 'm'; + const initialMountNode = parentNode || '#target'; + const anchorNode = parentNode ? 'null' : 'anchor'; + + block.builders.mount.addLine( + `if (${name}) ${name}.${mountOrIntro}(${initialMountNode}, ${anchorNode});` + ); + + const updateMountNode = this.getUpdateMountNode(anchor); + + const enter = dynamic + ? branch.hasIntroMethod + ? deindent` + if (${name}) { + ${name}.p(changed, ctx); + } else { + ${name} = ${branch.block}(#component, ctx); + if (${name}) ${name}.c(); + } + + ${name}.i(${updateMountNode}, ${anchor}); + ` + : deindent` + if (${name}) { + ${name}.p(changed, ctx); + } else { + ${name} = ${branch.block}(#component, ctx); + ${name}.c(); + ${name}.m(${updateMountNode}, ${anchor}); + } + ` + : branch.hasIntroMethod + ? deindent` + if (!${name}) { + ${name} = ${branch.block}(#component, ctx); + ${name}.c(); + } + ${name}.i(${updateMountNode}, ${anchor}); + ` + : deindent` + if (!${name}) { + ${name} = ${branch.block}(#component, ctx); + ${name}.c(); + ${name}.m(${updateMountNode}, ${anchor}); + } + `; + + // no `p()` here — we don't want to update outroing nodes, + // as that will typically result in glitching + const exit = branch.hasOutroMethod + ? deindent` + ${name}.o(function() { + ${name}.u(); + ${name}.d(); + ${name} = null; + }); + ` + : deindent` + ${name}.u(); + ${name}.d(); + ${name} = null; + `; + + block.builders.update.addBlock(deindent` + if (${branch.condition}) { + ${enter} + } else if (${name}) { + ${exit} + } + `); + + block.builders.unmount.addLine(`${if_name}${name}.u();`); + + block.builders.destroy.addLine(`${if_name}${name}.d();`); + } + + getBranches( + block: Block, + parentNode: string, + parentNodes: string, + node: IfBlock + ) { + const branches = [ + { + condition: node.expression.snippet, + block: node.block.name, + hasUpdateMethod: node.block.hasUpdateMethod, + hasIntroMethod: node.block.hasIntroMethod, + hasOutroMethod: node.block.hasOutroMethod, + }, + ]; + + this.visitChildren(block, node); + + if (isElseIf(node.else)) { + branches.push( + ...this.getBranches(block, parentNode, parentNodes, node.else.children[0]) + ); + } else { + branches.push({ + condition: null, + block: node.else ? node.else.block.name : null, + hasUpdateMethod: node.else ? node.else.block.hasUpdateMethod : false, + hasIntroMethod: node.else ? node.else.block.hasIntroMethod : false, + hasOutroMethod: node.else ? node.else.block.hasOutroMethod : false, + }); + + if (node.else) { + this.visitChildren(block, node.else); + } + } + + return branches; + } + + ssr() { + const { compiler } = this; + const { snippet } = this.expression; + + compiler.target.append('${ ' + snippet + ' ? `'); + + this.children.forEach((child: Node) => { + child.ssr(); + }); + + compiler.target.append('` : `'); + + if (this.else) { + this.else.children.forEach((child: Node) => { + child.ssr(); + }); + } + + compiler.target.append('` }'); + } + + visitChildren(block: Block, node: Node) { + node.children.forEach((child: Node) => { + child.build(node.block, null, 'nodes'); + }); + } +} \ No newline at end of file diff --git a/src/generators/nodes/MustacheTag.ts b/src/compile/nodes/MustacheTag.ts similarity index 100% rename from src/generators/nodes/MustacheTag.ts rename to src/compile/nodes/MustacheTag.ts diff --git a/src/generators/nodes/PendingBlock.ts b/src/compile/nodes/PendingBlock.ts similarity index 100% rename from src/generators/nodes/PendingBlock.ts rename to src/compile/nodes/PendingBlock.ts diff --git a/src/generators/nodes/RawMustacheTag.ts b/src/compile/nodes/RawMustacheTag.ts similarity index 100% rename from src/generators/nodes/RawMustacheTag.ts rename to src/compile/nodes/RawMustacheTag.ts diff --git a/src/generators/nodes/Slot.ts b/src/compile/nodes/Slot.ts similarity index 100% rename from src/generators/nodes/Slot.ts rename to src/compile/nodes/Slot.ts diff --git a/src/generators/nodes/Text.ts b/src/compile/nodes/Text.ts similarity index 100% rename from src/generators/nodes/Text.ts rename to src/compile/nodes/Text.ts diff --git a/src/generators/nodes/ThenBlock.ts b/src/compile/nodes/ThenBlock.ts similarity index 100% rename from src/generators/nodes/ThenBlock.ts rename to src/compile/nodes/ThenBlock.ts diff --git a/src/generators/nodes/Title.ts b/src/compile/nodes/Title.ts similarity index 100% rename from src/generators/nodes/Title.ts rename to src/compile/nodes/Title.ts diff --git a/src/generators/nodes/Transition.ts b/src/compile/nodes/Transition.ts similarity index 100% rename from src/generators/nodes/Transition.ts rename to src/compile/nodes/Transition.ts diff --git a/src/generators/nodes/Window.ts b/src/compile/nodes/Window.ts similarity index 100% rename from src/generators/nodes/Window.ts rename to src/compile/nodes/Window.ts diff --git a/src/generators/nodes/shared/Expression.ts b/src/compile/nodes/shared/Expression.ts similarity index 98% rename from src/generators/nodes/shared/Expression.ts rename to src/compile/nodes/shared/Expression.ts index 1d3f52f15b..f4278cebec 100644 --- a/src/generators/nodes/shared/Expression.ts +++ b/src/compile/nodes/shared/Expression.ts @@ -1,4 +1,4 @@ -import Generator from '../../Generator'; +import Compiler from '../../Compiler'; import { walk } from 'estree-walker'; import isReference from 'is-reference'; import flattenReference from '../../../utils/flattenReference'; @@ -54,7 +54,7 @@ const precedence: Record number> = { }; export default class Expression { - compiler: Generator; + compiler: Compiler; node: any; snippet: string; diff --git a/src/compile/nodes/shared/Node.ts b/src/compile/nodes/shared/Node.ts new file mode 100644 index 0000000000..a68b0e1544 --- /dev/null +++ b/src/compile/nodes/shared/Node.ts @@ -0,0 +1,172 @@ +import Compiler from './../../Compiler'; +import Block from '../../dom/Block'; +import { trimStart, trimEnd } from '../../../utils/trim'; + +export default class Node { + readonly start: number; + readonly end: number; + readonly compiler: Compiler; + readonly parent: Node; + readonly type: string; + + prev?: Node; + next?: Node; + + canUseInnerHTML: boolean; + var: string; + + constructor(compiler: Compiler, parent, scope, info: any) { + this.start = info.start; + this.end = info.end; + this.type = info.type; + + // this makes properties non-enumerable, which makes logging + // bearable. might have a performance cost. TODO remove in prod? + Object.defineProperties(this, { + compiler: { + value: compiler + }, + parent: { + value: parent + } + }); + } + + cannotUseInnerHTML() { + if (this.canUseInnerHTML !== false) { + this.canUseInnerHTML = false; + if (this.parent) this.parent.cannotUseInnerHTML(); + } + } + + init( + block: Block, + stripWhitespace: boolean, + nextSibling: Node + ) { + // implemented by subclasses + } + + initChildren( + block: Block, + stripWhitespace: boolean, + nextSibling: Node + ) { + // glue text nodes together + const cleaned: Node[] = []; + let lastChild: Node; + + let windowComponent; + + this.children.forEach((child: Node) => { + if (child.type === 'Comment') return; + + // special case — this is an easy way to remove whitespace surrounding + // . lil hacky but it works + if (child.type === 'Window') { + windowComponent = child; + return; + } + + if (child.type === 'Text' && lastChild && lastChild.type === 'Text') { + lastChild.data += child.data; + lastChild.end = child.end; + } else { + if (child.type === 'Text' && stripWhitespace && cleaned.length === 0) { + child.data = trimStart(child.data); + if (child.data) cleaned.push(child); + } else { + cleaned.push(child); + } + } + + lastChild = child; + }); + + lastChild = null; + + cleaned.forEach((child: Node, i: number) => { + child.canUseInnerHTML = !this.compiler.options.hydratable; + + child.init(block, stripWhitespace, cleaned[i + 1] || nextSibling); + + if (child.shouldSkip) return; + + if (lastChild) lastChild.next = child; + child.prev = lastChild; + + lastChild = child; + }); + + // We want to remove trailing whitespace inside an element/component/block, + // *unless* there is no whitespace between this node and its next sibling + if (stripWhitespace && lastChild && lastChild.type === 'Text') { + const shouldTrim = ( + nextSibling ? (nextSibling.type === 'Text' && /^\s/.test(nextSibling.data)) : !this.hasAncestor('EachBlock') + ); + + if (shouldTrim) { + lastChild.data = trimEnd(lastChild.data); + if (!lastChild.data) { + cleaned.pop(); + lastChild = cleaned[cleaned.length - 1]; + lastChild.next = null; + } + } + } + + this.children = cleaned; + if (windowComponent) cleaned.unshift(windowComponent); + } + + build( + block: Block, + parentNode: string, + parentNodes: string + ) { + // implemented by subclasses + } + + isDomNode() { + return this.type === 'Element' || this.type === 'Text' || this.type === 'MustacheTag'; + } + + hasAncestor(type: string) { + return this.parent ? + this.parent.type === type || this.parent.hasAncestor(type) : + false; + } + + findNearest(selector: RegExp) { + if (selector.test(this.type)) return this; + if (this.parent) return this.parent.findNearest(selector); + } + + getOrCreateAnchor(block: Block, parentNode: string, parentNodes: string) { + // TODO use this in EachBlock and IfBlock — tricky because + // children need to be created first + const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode(); + const anchor = needsAnchor + ? block.getUniqueName(`${this.var}_anchor`) + : (this.next && this.next.var) || 'null'; + + if (needsAnchor) { + block.addElement( + anchor, + `@createComment()`, + parentNodes && `@createComment()`, + parentNode + ); + } + + return anchor; + } + + getUpdateMountNode(anchor: string) { + return this.parent.isDomNode() ? this.parent.var : `${anchor}.parentNode`; + } + + remount(name: string) { + return `${this.var}.m(${name}._slotted.default, null);`; + } +} \ No newline at end of file diff --git a/src/generators/nodes/shared/Tag.ts b/src/compile/nodes/shared/Tag.ts similarity index 100% rename from src/generators/nodes/shared/Tag.ts rename to src/compile/nodes/shared/Tag.ts diff --git a/src/generators/nodes/shared/TemplateScope.ts b/src/compile/nodes/shared/TemplateScope.ts similarity index 100% rename from src/generators/nodes/shared/TemplateScope.ts rename to src/compile/nodes/shared/TemplateScope.ts diff --git a/src/generators/nodes/shared/mapChildren.ts b/src/compile/nodes/shared/mapChildren.ts similarity index 100% rename from src/generators/nodes/shared/mapChildren.ts rename to src/compile/nodes/shared/mapChildren.ts diff --git a/src/compile/server-side-rendering/index.ts b/src/compile/server-side-rendering/index.ts new file mode 100644 index 0000000000..5f5a410a94 --- /dev/null +++ b/src/compile/server-side-rendering/index.ts @@ -0,0 +1,175 @@ +import deindent from '../../utils/deindent'; +import Compiler from '../Compiler'; +import Stats from '../../Stats'; +import Stylesheet from '../../css/Stylesheet'; +import { removeNode, removeObjectKey } from '../../utils/removeNode'; +import getName from '../../utils/getName'; +import globalWhitelist from '../../utils/globalWhitelist'; +import { Ast, Node, CompileOptions } from '../../interfaces'; +import { AppendTarget } from '../../interfaces'; +import { stringify } from '../../utils/stringify'; + +export class SsrTarget { + bindings: string[]; + renderCode: string; + appendTargets: AppendTarget[]; + + constructor() { + this.bindings = []; + this.renderCode = ''; + this.appendTargets = []; + } + + append(code: string) { + if (this.appendTargets.length) { + const appendTarget = this.appendTargets[this.appendTargets.length - 1]; + const slotName = appendTarget.slotStack[appendTarget.slotStack.length - 1]; + appendTarget.slots[slotName] += code; + } else { + this.renderCode += code; + } + } +} + +export default function ssr( + ast: Ast, + source: string, + stylesheet: Stylesheet, + options: CompileOptions, + stats: Stats +) { + const format = options.format || 'cjs'; + + const target = new SsrTarget(); + const compiler = new Compiler(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, false, target); + + const { computations, name, templateProperties } = compiler; + + // create main render() function + trim(compiler.fragment.children).forEach((node: Node) => { + node.ssr(); + }); + + const css = compiler.customElement ? + { code: null, map: null } : + compiler.stylesheet.render(options.filename, true); + + // generate initial state object + const expectedProperties = Array.from(compiler.expectedProperties); + const globals = expectedProperties.filter(prop => globalWhitelist.has(prop)); + const storeProps = expectedProperties.filter(prop => prop[0] === '$'); + + const initialState = []; + if (globals.length > 0) { + initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`); + } + + if (storeProps.length > 0) { + const initialize = `_init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])` + initialState.push(`options.store.${initialize}`); + } + + if (templateProperties.data) { + initialState.push(`%data()`); + } else if (globals.length === 0 && storeProps.length === 0) { + initialState.push('{}'); + } + + initialState.push('ctx'); + + const helpers = new Set(); + + // TODO concatenate CSS maps + const result = deindent` + ${compiler.javascript} + + var ${name} = {}; + + ${options.filename && `${name}.filename = ${stringify(options.filename)}`}; + + ${name}.data = function() { + return ${templateProperties.data ? `%data()` : `{}`}; + }; + + ${name}.render = function(state, options = {}) { + var components = new Set(); + + function addComponent(component) { + components.add(component); + } + + var result = { head: '', addComponent }; + var html = ${name}._render(result, state, options); + + var cssCode = Array.from(components).map(c => c.css && c.css.code).filter(Boolean).join('\\n'); + + return { + html, + head: result.head, + css: { code: cssCode, map: null }, + toString() { + return html; + } + }; + } + + ${name}._render = function(__result, ctx, options) { + ${templateProperties.store && `options.store = %store();`} + __result.addComponent(${name}); + + ctx = Object.assign(${initialState.join(', ')}); + + ${computations.map( + ({ key, deps }) => + `ctx.${key} = %computed-${key}(ctx);` + )} + + ${target.bindings.length && + deindent` + var settled = false; + var tmp; + + while (!settled) { + settled = true; + + ${target.bindings.join('\n\n')} + } + `} + + return \`${target.renderCode}\`; + }; + + ${name}.css = { + code: ${css.code ? stringify(css.code) : `''`}, + map: ${css.map ? stringify(css.map.toString()) : 'null'} + }; + + var warned = false; + + ${templateProperties.preload && `${name}.preload = %preload;`} + `; + + return compiler.generate(result, options, { name, format }); +} + +function trim(nodes) { + let start = 0; + for (; start < nodes.length; start += 1) { + const node = nodes[start]; + if (node.type !== 'Text') break; + + node.data = node.data.replace(/^\s+/, ''); + if (node.data) break; + } + + let end = nodes.length; + for (; end > start; end -= 1) { + const node = nodes[end - 1]; + if (node.type !== 'Text') break; + + node.data = node.data.replace(/\s+$/, ''); + if (node.data) break; + } + + return nodes.slice(start, end); +} diff --git a/src/compile/shared.ts b/src/compile/shared.ts new file mode 100644 index 0000000000..fc7b83ecdc --- /dev/null +++ b/src/compile/shared.ts @@ -0,0 +1,79 @@ +// this file is auto-generated, do not edit it +const shared: Record = { + "appendNode": "function appendNode(node, target) {\n\ttarget.appendChild(node);\n}", + "insertNode": "function insertNode(node, target, anchor) {\n\ttarget.insertBefore(node, anchor);\n}", + "detachNode": "function detachNode(node) {\n\tnode.parentNode.removeChild(node);\n}", + "detachBetween": "function detachBetween(before, after) {\n\twhile (before.nextSibling && before.nextSibling !== after) {\n\t\tbefore.parentNode.removeChild(before.nextSibling);\n\t}\n}", + "detachBefore": "function detachBefore(after) {\n\twhile (after.previousSibling) {\n\t\tafter.parentNode.removeChild(after.previousSibling);\n\t}\n}", + "detachAfter": "function detachAfter(before) {\n\twhile (before.nextSibling) {\n\t\tbefore.parentNode.removeChild(before.nextSibling);\n\t}\n}", + "reinsertBetween": "function reinsertBetween(before, after, target) {\n\twhile (before.nextSibling && before.nextSibling !== after) {\n\t\ttarget.appendChild(before.parentNode.removeChild(before.nextSibling));\n\t}\n}", + "reinsertChildren": "function reinsertChildren(parent, target) {\n\twhile (parent.firstChild) target.appendChild(parent.firstChild);\n}", + "reinsertAfter": "function reinsertAfter(before, target) {\n\twhile (before.nextSibling) target.appendChild(before.nextSibling);\n}", + "reinsertBefore": "function reinsertBefore(after, target) {\n\tvar parent = after.parentNode;\n\twhile (parent.firstChild !== after) target.appendChild(parent.firstChild);\n}", + "destroyEach": "function destroyEach(iterations) {\n\tfor (var i = 0; i < iterations.length; i += 1) {\n\t\tif (iterations[i]) iterations[i].d();\n\t}\n}", + "createFragment": "function createFragment() {\n\treturn document.createDocumentFragment();\n}", + "createElement": "function createElement(name) {\n\treturn document.createElement(name);\n}", + "createSvgElement": "function createSvgElement(name) {\n\treturn document.createElementNS('http://www.w3.org/2000/svg', name);\n}", + "createText": "function createText(data) {\n\treturn document.createTextNode(data);\n}", + "createComment": "function createComment() {\n\treturn document.createComment('');\n}", + "addListener": "function addListener(node, event, handler) {\n\tnode.addEventListener(event, handler, false);\n}", + "removeListener": "function removeListener(node, event, handler) {\n\tnode.removeEventListener(event, handler, false);\n}", + "setAttribute": "function setAttribute(node, attribute, value) {\n\tnode.setAttribute(attribute, value);\n}", + "setAttributes": "function setAttributes(node, attributes) {\n\tfor (var key in attributes) {\n\t\tif (key in node) {\n\t\t\tnode[key] = attributes[key];\n\t\t} else {\n\t\t\tif (attributes[key] === undefined) removeAttribute(node, key);\n\t\t\telse setAttribute(node, key, attributes[key]);\n\t\t}\n\t}\n}", + "removeAttribute": "function removeAttribute(node, attribute) {\n\tnode.removeAttribute(attribute);\n}", + "setXlinkAttribute": "function setXlinkAttribute(node, attribute, value) {\n\tnode.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);\n}", + "getBindingGroupValue": "function getBindingGroupValue(group) {\n\tvar value = [];\n\tfor (var i = 0; i < group.length; i += 1) {\n\t\tif (group[i].checked) value.push(group[i].__value);\n\t}\n\treturn value;\n}", + "toNumber": "function toNumber(value) {\n\treturn value === '' ? undefined : +value;\n}", + "timeRangesToArray": "function timeRangesToArray(ranges) {\n\tvar array = [];\n\tfor (var i = 0; i < ranges.length; i += 1) {\n\t\tarray.push({ start: ranges.start(i), end: ranges.end(i) });\n\t}\n\treturn array;\n}", + "children": "function children (element) {\n\treturn Array.from(element.childNodes);\n}", + "claimElement": "function claimElement (nodes, name, attributes, svg) {\n\tfor (var i = 0; i < nodes.length; i += 1) {\n\t\tvar node = nodes[i];\n\t\tif (node.nodeName === name) {\n\t\t\tfor (var j = 0; j < node.attributes.length; j += 1) {\n\t\t\t\tvar attribute = node.attributes[j];\n\t\t\t\tif (!attributes[attribute.name]) node.removeAttribute(attribute.name);\n\t\t\t}\n\t\t\treturn nodes.splice(i, 1)[0]; // TODO strip unwanted attributes\n\t\t}\n\t}\n\n\treturn svg ? createSvgElement(name) : createElement(name);\n}", + "claimText": "function claimText (nodes, data) {\n\tfor (var i = 0; i < nodes.length; i += 1) {\n\t\tvar node = nodes[i];\n\t\tif (node.nodeType === 3) {\n\t\t\tnode.data = data;\n\t\t\treturn nodes.splice(i, 1)[0];\n\t\t}\n\t}\n\n\treturn createText(data);\n}", + "setInputType": "function setInputType(input, type) {\n\ttry {\n\t\tinput.type = type;\n\t} catch (e) {}\n}", + "setStyle": "function setStyle(node, key, value) {\n\tnode.style.setProperty(key, value);\n}", + "selectOption": "function selectOption(select, value) {\n\tfor (var i = 0; i < select.options.length; i += 1) {\n\t\tvar option = select.options[i];\n\n\t\tif (option.__value === value) {\n\t\t\toption.selected = true;\n\t\t\treturn;\n\t\t}\n\t}\n}", + "selectOptions": "function selectOptions(select, value) {\n\tfor (var i = 0; i < select.options.length; i += 1) {\n\t\tvar option = select.options[i];\n\t\toption.selected = ~value.indexOf(option.__value);\n\t}\n}", + "selectValue": "function selectValue(select) {\n\tvar selectedOption = select.querySelector(':checked') || select.options[0];\n\treturn selectedOption && selectedOption.__value;\n}", + "selectMultipleValue": "function selectMultipleValue(select) {\n\treturn [].map.call(select.querySelectorAll(':checked'), function(option) {\n\t\treturn option.__value;\n\t});\n}", + "blankObject": "function blankObject() {\n\treturn Object.create(null);\n}", + "destroy": "function destroy(detach) {\n\tthis.destroy = noop;\n\tthis.fire('destroy');\n\tthis.set = noop;\n\n\tif (detach !== false) this._fragment.u();\n\tthis._fragment.d();\n\tthis._fragment = null;\n\tthis._state = {};\n}", + "destroyDev": "function destroyDev(detach) {\n\tdestroy.call(this, detach);\n\tthis.destroy = function() {\n\t\tconsole.warn('Component was already destroyed');\n\t};\n}", + "_differs": "function _differs(a, b) {\n\treturn a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');\n}", + "_differsImmutable": "function _differsImmutable(a, b) {\n\treturn a != a ? b == b : a !== b;\n}", + "fire": "function fire(eventName, data) {\n\tvar handlers =\n\t\teventName in this._handlers && this._handlers[eventName].slice();\n\tif (!handlers) return;\n\n\tfor (var i = 0; i < handlers.length; i += 1) {\n\t\tvar handler = handlers[i];\n\n\t\tif (!handler.__calling) {\n\t\t\thandler.__calling = true;\n\t\t\thandler.call(this, data);\n\t\t\thandler.__calling = false;\n\t\t}\n\t}\n}", + "get": "function get() {\n\treturn this._state;\n}", + "init": "function init(component, options) {\n\tcomponent._handlers = blankObject();\n\tcomponent._bind = options._bind;\n\n\tcomponent.options = options;\n\tcomponent.root = options.root || component;\n\tcomponent.store = component.root.store || options.store;\n}", + "on": "function on(eventName, handler) {\n\tvar handlers = this._handlers[eventName] || (this._handlers[eventName] = []);\n\thandlers.push(handler);\n\n\treturn {\n\t\tcancel: function() {\n\t\t\tvar index = handlers.indexOf(handler);\n\t\t\tif (~index) handlers.splice(index, 1);\n\t\t}\n\t};\n}", + "run": "function run(fn) {\n\tfn();\n}", + "set": "function set(newState) {\n\tthis._set(assign({}, newState));\n\tif (this.root._lock) return;\n\tthis.root._lock = true;\n\tcallAll(this.root._beforecreate);\n\tcallAll(this.root._oncreate);\n\tcallAll(this.root._aftercreate);\n\tthis.root._lock = false;\n}", + "_set": "function _set(newState) {\n\tvar oldState = this._state,\n\t\tchanged = {},\n\t\tdirty = false;\n\n\tfor (var key in newState) {\n\t\tif (this._differs(newState[key], oldState[key])) changed[key] = dirty = true;\n\t}\n\tif (!dirty) return;\n\n\tthis._state = assign(assign({}, oldState), newState);\n\tthis._recompute(changed, this._state);\n\tif (this._bind) this._bind(changed, this._state);\n\n\tif (this._fragment) {\n\t\tthis.fire(\"state\", { changed: changed, current: this._state, previous: oldState });\n\t\tthis._fragment.p(changed, this._state);\n\t\tthis.fire(\"update\", { changed: changed, current: this._state, previous: oldState });\n\t}\n}", + "setDev": "function setDev(newState) {\n\tif (typeof newState !== 'object') {\n\t\tthrow new Error(\n\t\t\tthis._debugName + '.set was called without an object of data key-values to update.'\n\t\t);\n\t}\n\n\tthis._checkReadOnly(newState);\n\tset.call(this, newState);\n}", + "callAll": "function callAll(fns) {\n\twhile (fns && fns.length) fns.shift()();\n}", + "_mount": "function _mount(target, anchor) {\n\tthis._fragment[this._fragment.i ? 'i' : 'm'](target, anchor || null);\n}", + "_unmount": "function _unmount() {\n\tif (this._fragment) this._fragment.u();\n}", + "isPromise": "function isPromise(value) {\n\treturn value && typeof value.then === 'function';\n}", + "PENDING": "{}", + "SUCCESS": "{}", + "FAILURE": "{}", + "removeFromStore": "function removeFromStore() {\n\tthis.store._remove(this);\n}", + "proto": "{\n\tdestroy,\n\tget,\n\tfire,\n\ton,\n\tset,\n\t_recompute: noop,\n\t_set,\n\t_mount,\n\t_unmount,\n\t_differs\n}", + "protoDev": "{\n\tdestroy: destroyDev,\n\tget,\n\tfire,\n\ton,\n\tset: setDev,\n\t_recompute: noop,\n\t_set,\n\t_mount,\n\t_unmount,\n\t_differs\n}", + "destroyBlock": "function destroyBlock(block, lookup) {\n\tblock.u();\n\tblock.d();\n\tlookup[block.key] = null;\n}", + "outroAndDestroyBlock": "function outroAndDestroyBlock(block, lookup) {\n\tblock.o(function() {\n\t\tdestroyBlock(block, lookup);\n\t});\n}", + "updateKeyedEach": "function updateKeyedEach(old_blocks, component, changed, key_prop, dynamic, list, lookup, node, has_outro, create_each_block, intro_method, next, get_context) {\n\tvar o = old_blocks.length;\n\tvar n = list.length;\n\n\tvar i = o;\n\tvar old_indexes = {};\n\twhile (i--) old_indexes[old_blocks[i].key] = i;\n\n\tvar new_blocks = [];\n\tvar new_lookup = {};\n\tvar deltas = {};\n\n\tvar i = n;\n\twhile (i--) {\n\t\tvar key = list[i][key_prop];\n\t\tvar block = lookup[key];\n\n\t\tif (!block) {\n\t\t\tblock = create_each_block(component, key, get_context(i));\n\t\t\tblock.c();\n\t\t} else if (dynamic) {\n\t\t\tblock.p(changed, get_context(i));\n\t\t}\n\n\t\tnew_blocks[i] = new_lookup[key] = block;\n\n\t\tif (key in old_indexes) deltas[key] = Math.abs(i - old_indexes[key]);\n\t}\n\n\tvar will_move = {};\n\tvar did_move = {};\n\n\tvar destroy = has_outro ? outroAndDestroyBlock : destroyBlock;\n\n\tfunction insert(block) {\n\t\tblock[intro_method](node, next);\n\t\tlookup[block.key] = block;\n\t\tnext = block.first;\n\t\tn--;\n\t}\n\n\twhile (o && n) {\n\t\tvar new_block = new_blocks[n - 1];\n\t\tvar old_block = old_blocks[o - 1];\n\t\tvar new_key = new_block.key;\n\t\tvar old_key = old_block.key;\n\n\t\tif (new_block === old_block) {\n\t\t\t// do nothing\n\t\t\tnext = new_block.first;\n\t\t\to--;\n\t\t\tn--;\n\t\t}\n\n\t\telse if (!new_lookup[old_key]) {\n\t\t\t// remove old block\n\t\t\tdestroy(old_block, lookup);\n\t\t\to--;\n\t\t}\n\n\t\telse if (!lookup[new_key] || will_move[new_key]) {\n\t\t\tinsert(new_block);\n\t\t}\n\n\t\telse if (did_move[old_key]) {\n\t\t\to--;\n\n\t\t} else if (deltas[new_key] > deltas[old_key]) {\n\t\t\tdid_move[new_key] = true;\n\t\t\tinsert(new_block);\n\n\t\t} else {\n\t\t\twill_move[old_key] = true;\n\t\t\to--;\n\t\t}\n\t}\n\n\twhile (o--) {\n\t\tvar old_block = old_blocks[o];\n\t\tif (!new_lookup[old_block.key]) destroy(old_block, lookup);\n\t}\n\n\twhile (n) insert(new_blocks[n - 1]);\n\n\treturn new_blocks;\n}", + "getSpreadUpdate": "function getSpreadUpdate(levels, updates) {\n\tvar update = {};\n\n\tvar to_null_out = {};\n\tvar accounted_for = {};\n\n\tvar i = levels.length;\n\twhile (i--) {\n\t\tvar o = levels[i];\n\t\tvar n = updates[i];\n\n\t\tif (n) {\n\t\t\tfor (var key in o) {\n\t\t\t\tif (!(key in n)) to_null_out[key] = 1;\n\t\t\t}\n\n\t\t\tfor (var key in n) {\n\t\t\t\tif (!accounted_for[key]) {\n\t\t\t\t\tupdate[key] = n[key];\n\t\t\t\t\taccounted_for[key] = 1;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlevels[i] = n;\n\t\t} else {\n\t\t\tfor (var key in o) {\n\t\t\t\taccounted_for[key] = 1;\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (var key in to_null_out) {\n\t\tif (!(key in update)) update[key] = undefined;\n\t}\n\n\treturn update;\n}", + "spread": "function spread(args) {\n\tconst attributes = Object.assign({}, ...args);\n\tlet str = '';\n\n\tObject.keys(attributes).forEach(name => {\n\t\tconst value = attributes[name];\n\t\tif (value === undefined) return;\n\t\tif (value === true) str += \" \" + name;\n\t\tstr += \" \" + name + \"=\" + JSON.stringify(value);\n\t});\n\n\treturn str;\n}", + "escaped": "{\n\t'\"': '"',\n\t\"'\": ''',\n\t'&': '&',\n\t'<': '<',\n\t'>': '>'\n}", + "escape": "function escape(html) {\n\treturn String(html).replace(/[\"'&<>]/g, match => escaped[match]);\n}", + "each": "function each(items, assign, fn) {\n\tlet str = '';\n\tfor (let i = 0; i < items.length; i += 1) {\n\t\tstr += fn(assign(items[i], i));\n\t}\n\treturn str;\n}", + "missingComponent": "{\n\t_render: () => ''\n}", + "linear": "function linear(t) {\n\treturn t;\n}", + "generateRule": "function generateRule(\n\ta,\n\tb,\n\tdelta,\n\tduration,\n\tease,\n\tfn\n) {\n\tvar keyframes = '{\\n';\n\n\tfor (var p = 0; p <= 1; p += 16.666 / duration) {\n\t\tvar t = a + delta * ease(p);\n\t\tkeyframes += p * 100 + '%{' + fn(t) + '}\\n';\n\t}\n\n\treturn keyframes + '100% {' + fn(b) + '}\\n}';\n}", + "hash": "function hash(str) {\n\tvar hash = 5381;\n\tvar i = str.length;\n\n\twhile (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i);\n\treturn hash >>> 0;\n}", + "wrapTransition": "function wrapTransition(component, node, fn, params, intro, outgroup) {\n\tvar obj = fn(node, params);\n\tvar duration = obj.duration || 300;\n\tvar ease = obj.easing || linear;\n\tvar cssText;\n\n\t// TODO share