Merge pull request from sveltejs/gh-1316

refactor
pull/1374/head
Rich Harris 7 years ago committed by GitHub
commit 2616ab1520
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

34
.gitignore vendored

@ -1,22 +1,16 @@
.DS_Store .DS_Store
node_modules
compiler
ssr
shared.js
scratch
!test/compiler
!test/ssr
.nyc_output .nyc_output
coverage node_modules
coverage.lcov /compiler/
test/sourcemaps/samples/*/output.js /ssr/
test/sourcemaps/samples/*/output.js.map /shared.js
_actual.* /scratch/
_actual-v2.* /coverage/
_actual-bundle.* /coverage.lcov/
src/generators/dom/shared.ts /test/sourcemaps/samples/*/output.js
package-lock.json /test/sourcemaps/samples/*/output.js.map
.idea/ /src/compile/shared.ts
*.iml /package-lock.json
store.umd.js /store.umd.js
yarn-error.log /yarn-error.log
_actual*.*

@ -34,7 +34,7 @@ export default [
/* ssr/register.js */ /* ssr/register.js */
{ {
input: 'src/server-side-rendering/register.js', input: 'src/ssr/register.js',
plugins: [ plugins: [
resolve(), resolve(),
commonjs(), commonjs(),

@ -1,5 +1,5 @@
import { Node, Warning } from './interfaces'; import { Node, Warning } from './interfaces';
import Generator from './generators/Generator'; import Compiler from './compile/Compiler';
const now = (typeof process !== 'undefined' && process.hrtime) const now = (typeof process !== 'undefined' && process.hrtime)
? () => { ? () => {
@ -73,12 +73,12 @@ export default class Stats {
this.currentChildren = this.currentTiming ? this.currentTiming.children : this.timings; this.currentChildren = this.currentTiming ? this.currentTiming.children : this.timings;
} }
render(generator: Generator) { render(compiler: Compiler) {
const timings = Object.assign({ const timings = Object.assign({
total: now() - this.startTime total: now() - this.startTime
}, collapseTimings(this.timings)); }, collapseTimings(this.timings));
const imports = generator.imports.map(node => { const imports = compiler.imports.map(node => {
return { return {
source: node.source.value, source: node.source.value,
specifiers: node.specifiers.map(specifier => { specifiers: node.specifiers.map(specifier => {
@ -95,8 +95,8 @@ export default class Stats {
}); });
const hooks: Record<string, boolean> = {}; const hooks: Record<string, boolean> = {};
if (generator.templateProperties.oncreate) hooks.oncreate = true; if (compiler.templateProperties.oncreate) hooks.oncreate = true;
if (generator.templateProperties.ondestroy) hooks.ondestroy = true; if (compiler.templateProperties.ondestroy) hooks.ondestroy = true;
return { return {
timings, timings,

@ -1,3 +1,4 @@
import { parseExpressionAt } from 'acorn';
import MagicString, { Bundle } from 'magic-string'; import MagicString, { Bundle } from 'magic-string';
import isReference from 'is-reference'; import isReference from 'is-reference';
import { walk, childKeys } from 'estree-walker'; import { walk, childKeys } from 'estree-walker';
@ -13,11 +14,13 @@ import nodeToString from '../utils/nodeToString';
import wrapModule from './wrapModule'; import wrapModule from './wrapModule';
import annotateWithScopes, { Scope } from '../utils/annotateWithScopes'; import annotateWithScopes, { Scope } from '../utils/annotateWithScopes';
import getName from '../utils/getName'; import getName from '../utils/getName';
import clone from '../utils/clone';
import Stylesheet from '../css/Stylesheet'; import Stylesheet from '../css/Stylesheet';
import { test } from '../config'; import { test } from '../config';
import nodes from './nodes/index'; import Fragment from './nodes/Fragment';
import { Node, GenerateOptions, ShorthandImport, Parsed, CompileOptions, CustomElementOptions } from '../interfaces'; 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 { interface Computation {
key: string; key: string;
@ -75,14 +78,15 @@ function removeIndentation(
childKeys.EachBlock = childKeys.IfBlock = ['children', 'else']; childKeys.EachBlock = childKeys.IfBlock = ['children', 'else'];
childKeys.Attribute = ['value']; childKeys.Attribute = ['value'];
export default class Generator { export default class Compiler {
stats: Stats; stats: Stats;
ast: Parsed; ast: Ast;
parsed: Parsed;
source: string; source: string;
name: string; name: string;
options: CompileOptions; options: CompileOptions;
fragment: Fragment;
target: DomTarget | SsrTarget;
customElement: CustomElementOptions; customElement: CustomElementOptions;
tag: string; tag: string;
@ -122,22 +126,22 @@ export default class Generator {
usedNames: Set<string>; usedNames: Set<string>;
constructor( constructor(
parsed: Parsed, ast: Ast,
source: string, source: string,
name: string, name: string,
stylesheet: Stylesheet, stylesheet: Stylesheet,
options: CompileOptions, options: CompileOptions,
stats: Stats, stats: Stats,
dom: boolean dom: boolean,
target: DomTarget | SsrTarget
) { ) {
stats.start('compile'); stats.start('compile');
this.stats = stats; this.stats = stats;
this.ast = clone(parsed); this.ast = ast;
this.parsed = parsed;
this.source = source; this.source = source;
this.options = options; this.options = options;
this.target = target;
this.imports = []; this.imports = [];
this.shorthandImports = []; this.shorthandImports = [];
@ -191,8 +195,11 @@ export default class Generator {
throw new Error(`No tag name specified`); // TODO better error 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(); if (!this.customElement) this.stylesheet.reify();
stylesheet.warnOnUnusedSelectors(options.onwarn);
} }
addSourcemapLocations(node: Node) { addSourcemapLocations(node: Node) {
@ -212,112 +219,108 @@ export default class Generator {
return this.aliases.get(name); return this.aliases.get(name);
} }
contextualise( generate(result: string, options: CompileOptions, { banner = '', name, format }: GenerateOptions ) {
contexts: Map<string, string>, const pattern = /\[✂(\d+)-(\d+)$/;
indexes: Map<string, string>,
expression: Node,
context: string,
isEventHandler: boolean
): {
contexts: Set<string>,
indexes: Set<string>
} {
// this.addSourcemapLocations(expression);
const usedContexts: Set<string> = new Set(); const helpers = new Set();
const usedIndexes: Set<string> = 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; return this.alias(name);
let lexicalDepth = 0; }
const self = this; if (sigil === '%') {
return this.templateVars.get(name);
}
walk(expression, { return sigil.slice(1) + name;
enter(node: Node, parent: Node, key: string) { });
if (/^Function/.test(node.type)) lexicalDepth += 1;
if (node._scope) { let importedHelpers;
scope = node._scope;
return;
}
if (node.type === 'ThisExpression') { if (options.shared) {
if (lexicalDepth === 0 && context) if (format !== 'es' && format !== 'cjs') {
code.overwrite(node.start, node.end, context, { throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``);
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);
}
}
usedContexts.add(name); importedHelpers = Array.from(helpers).sort().map(name => {
} else if (helpers.has(name)) { const alias = this.alias(name);
let object = node; return { name, alias };
while (object.type === 'MemberExpression') object = object.object; });
} else {
const alias = self.templateVars.get(`helpers-${name}`); let inlineHelpers = '';
if (alias !== name) code.overwrite(object.start, object.end, alias);
} else if (indexes.has(name)) { const compiler = this;
const context = indexes.get(name);
usedContexts.add(context); // TODO is this right? importedHelpers = [];
usedIndexes.add(name);
} else { helpers.forEach(name => {
// handle shorthand properties const str = shared[name];
if (parent && parent.type === 'Property' && parent.shorthand) { const code = new MagicString(str);
if (key === 'key') { const expression = parseExpressionAt(str, 0);
code.appendLeft(node.start, `${name}: `);
return; 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.`); inlineHelpers += `\n\nvar ${this.alias('transitionManager')} = window.${global} || (window.${global} = ${code});\n\n`;
usedContexts.add('state'); } 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 { result += inlineHelpers;
contexts: usedContexts, }
indexes: usedIndexes
};
}
generate(result: string, options: CompileOptions, { banner = '', sharedPath, helpers, name, format }: GenerateOptions ) { const sharedPath = options.shared === true
const pattern = /\[✂(\d+)-(\d+)$/; ? '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 parts = module.split('✂]');
const finalChunk = parts.pop(); const finalChunk = parts.pop();
@ -393,7 +396,7 @@ export default class Generator {
return alias; return alias;
} }
getUniqueNameMaker(names: string[]) { getUniqueNameMaker() {
const localUsedNames = new Set(); const localUsedNames = new Set();
function add(name: string) { function add(name: string) {
@ -402,7 +405,6 @@ export default class Generator {
reservedNames.forEach(add); reservedNames.forEach(add);
this.userVars.forEach(add); this.userVars.forEach(add);
names.forEach(add);
return (name: string) => { return (name: string) => {
if (test) name = `${name}$`; if (test) name = `${name}$`;
@ -428,7 +430,7 @@ export default class Generator {
imports imports
} = this; } = this;
const { js } = this.parsed; const { js } = this.ast;
const componentDefinition = new CodeBuilder(); 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<string, string[]>,
indexes: Set<string>,
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<string> = 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<string, string[]>[] = [contextDependencies];
let indexes = new Set();
const indexesStack: Set<string>[] = [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:
// <option>{{foo}}</option>
// <option value='{{foo}}'>{{foo}}</option>
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
}));
}
}
}
});
}
} }

@ -1,42 +1,27 @@
import CodeBuilder from '../../utils/CodeBuilder'; import CodeBuilder from '../../utils/CodeBuilder';
import deindent from '../../utils/deindent'; import deindent from '../../utils/deindent';
import { escape } from '../../utils/stringify'; import { escape } from '../../utils/stringify';
import { DomGenerator } from './index'; import Compiler from '../Compiler';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
import shared from './shared';
export interface BlockOptions { export interface BlockOptions {
name: string; name: string;
generator?: DomGenerator; compiler?: Compiler;
expression?: Node;
context?: string;
destructuredContexts?: string[];
comment?: string; comment?: string;
key?: string; key?: string;
contexts?: Map<string, string>;
contextTypes?: Map<string, string>;
indexes?: Map<string, string>;
changeableIndexes?: Map<string, boolean>;
indexNames?: Map<string, string>; indexNames?: Map<string, string>;
listNames?: Map<string, string>; listNames?: Map<string, string>;
dependencies?: Set<string>; dependencies?: Set<string>;
} }
export default class Block { export default class Block {
generator: DomGenerator; compiler: Compiler;
name: string; name: string;
expression: Node;
context: string;
destructuredContexts?: string[];
comment?: string; comment?: string;
key: string; key: string;
first: string; first: string;
contexts: Map<string, string>;
contextTypes: Map<string, string>;
indexes: Map<string, string>;
changeableIndexes: Map<string, boolean>;
dependencies: Set<string>; dependencies: Set<string>;
indexNames: Map<string, string>; indexNames: Map<string, string>;
listNames: Map<string, string>; listNames: Map<string, string>;
@ -67,21 +52,14 @@ export default class Block {
autofocus: string; autofocus: string;
constructor(options: BlockOptions) { constructor(options: BlockOptions) {
this.generator = options.generator; this.compiler = options.compiler;
this.name = options.name; this.name = options.name;
this.expression = options.expression;
this.context = options.context;
this.destructuredContexts = options.destructuredContexts;
this.comment = options.comment; this.comment = options.comment;
// for keyed each blocks // for keyed each blocks
this.key = options.key; this.key = options.key;
this.first = null; this.first = null;
this.contexts = options.contexts;
this.contextTypes = options.contextTypes;
this.indexes = options.indexes;
this.changeableIndexes = options.changeableIndexes;
this.dependencies = new Set(); this.dependencies = new Set();
this.indexNames = options.indexNames; this.indexNames = options.indexNames;
@ -105,18 +83,18 @@ export default class Block {
this.hasOutroMethod = false; this.hasOutroMethod = false;
this.outros = 0; this.outros = 0;
this.getUniqueName = this.generator.getUniqueNameMaker([...this.contexts.values()]); this.getUniqueName = this.compiler.getUniqueNameMaker();
this.variables = new Map(); this.variables = new Map();
this.aliases = new Map() this.aliases = new Map()
.set('component', this.getUniqueName('component')) .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')); if (this.key) this.aliases.set('key', this.getUniqueName('key'));
this.hasUpdateMethod = false; // determined later this.hasUpdateMethod = false; // determined later
} }
addDependencies(dependencies: string[]) { addDependencies(dependencies: Set<string>) {
dependencies.forEach(dependency => { dependencies.forEach(dependency => {
this.dependencies.add(dependency); this.dependencies.add(dependency);
}); });
@ -163,10 +141,6 @@ export default class Block {
return new Block(Object.assign({}, this, { key: null }, options, { parent: this })); 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() { toString() {
let introing; let introing;
const hasIntros = !this.builders.intro.isEmpty(); const hasIntros = !this.builders.intro.isEmpty();
@ -186,23 +160,6 @@ export default class Block {
this.builders.mount.addLine(`${this.autofocus}.focus();`); 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 // minor hack we need to ensure that any {{{triples}}} are detached first
this.builders.unmount.addBlockAtStart(this.builders.detachRaw.toString()); 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()) { if (this.builders.claim.isEmpty() && this.builders.hydrate.isEmpty()) {
properties.addBlock(`l: @noop,`); properties.addBlock(`l: @noop,`);
} else { } else {
@ -261,13 +218,13 @@ export default class Block {
`); `);
} }
if (this.hasUpdateMethod) { if (this.hasUpdateMethod || this.maintainContext) {
if (this.builders.update.isEmpty() && initializers.length === 0) { if (this.builders.update.isEmpty() && !this.maintainContext) {
properties.addBlock(`p: @noop,`); properties.addBlock(`p: @noop,`);
} else { } else {
properties.addBlock(deindent` properties.addBlock(deindent`
p: function update(changed, state) { p: function update(changed, ${this.maintainContext ? '_ctx' : 'ctx'}) {
${initializers.map(str => `${str};`)} ${this.maintainContext && `ctx = _ctx;`}
${this.builders.update} ${this.builders.update}
}, },
`); `);
@ -338,9 +295,7 @@ export default class Block {
return deindent` return deindent`
${this.comment && `// ${escape(this.comment)}`} ${this.comment && `// ${escape(this.comment)}`}
function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, state) { function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, ctx) {
${initializers.length > 0 &&
`var ${initializers.join(', ')};`}
${this.variables.size > 0 && ${this.variables.size > 0 &&
`var ${Array.from(this.variables.keys()) `var ${Array.from(this.variables.keys())
.map(key => { .map(key => {

@ -8,52 +8,33 @@ import { stringify, escape } from '../../utils/stringify';
import CodeBuilder from '../../utils/CodeBuilder'; import CodeBuilder from '../../utils/CodeBuilder';
import globalWhitelist from '../../utils/globalWhitelist'; import globalWhitelist from '../../utils/globalWhitelist';
import reservedNames from '../../utils/reservedNames'; import reservedNames from '../../utils/reservedNames';
import shared from './shared'; import Compiler from '../Compiler';
import Generator from '../Generator';
import Stylesheet from '../../css/Stylesheet'; import Stylesheet from '../../css/Stylesheet';
import Stats from '../../Stats'; import Stats from '../../Stats';
import Block from './Block'; import Block from './Block';
import { test } from '../../config'; 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)[]; blocks: (Block|string)[];
readonly: Set<string>; readonly: Set<string>;
metaBindings: string[]; metaBindings: string[];
hydratable: boolean;
legacy: boolean;
hasIntroTransitions: boolean; hasIntroTransitions: boolean;
hasOutroTransitions: boolean; hasOutroTransitions: boolean;
hasComplexBindings: boolean; hasComplexBindings: boolean;
needsEncapsulateHelper: boolean; constructor() {
constructor(
parsed: Parsed,
source: string,
name: string,
stylesheet: Stylesheet,
options: CompileOptions,
stats: Stats
) {
super(parsed, source, name, stylesheet, options, stats, true);
this.blocks = []; this.blocks = [];
this.readonly = new Set(); 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 <svelte:window> meta tag // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
this.metaBindings = []; this.metaBindings = [];
} }
} }
export default function dom( export default function dom(
parsed: Parsed, ast: Ast,
source: string, source: string,
stylesheet: Stylesheet, stylesheet: Stylesheet,
options: CompileOptions, options: CompileOptions,
@ -61,23 +42,22 @@ export default function dom(
) { ) {
const format = options.format || 'es'; 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 { const {
computations, computations,
name, name,
templateProperties, templateProperties,
namespace, namespace,
} = generator; } = compiler;
parsed.html.build(); compiler.fragment.build();
const { block } = parsed.html; const { block } = compiler.fragment;
// prevent fragment being created twice (#1063) // prevent fragment being created twice (#1063)
if (options.customElement) block.builders.create.addLine(`this.c = @noop;`); if (options.customElement) block.builders.create.addLine(`this.c = @noop;`);
generator.stylesheet.warnOnUnusedSelectors(options.onwarn);
const builder = new CodeBuilder(); const builder = new CodeBuilder();
const computationBuilder = new CodeBuilder(); const computationBuilder = new CodeBuilder();
const computationDeps = new Set(); const computationDeps = new Set();
@ -88,14 +68,14 @@ export default function dom(
computationDeps.add(dep); computationDeps.add(dep);
}); });
if (generator.readonly.has(key)) { if (target.readonly.has(key)) {
// <svelte:window> bindings // <svelte:window> bindings
throw new Error( throw new Error(
`Cannot have a computed value '${key}' that clashes with a read-only property` `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(' || ')}`; const condition = `${deps.map(dep => `changed.${dep}`).join(' || ')}`;
@ -105,27 +85,27 @@ export default function dom(
}); });
} }
if (generator.javascript) { if (compiler.javascript) {
builder.addBlock(generator.javascript); builder.addBlock(compiler.javascript);
} }
const css = generator.stylesheet.render(options.filename, !generator.customElement); const css = compiler.stylesheet.render(options.filename, !compiler.customElement);
const styles = generator.stylesheet.hasStyles && stringify(options.dev ? const styles = compiler.stylesheet.hasStyles && stringify(options.dev ?
`${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` : `${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` :
css.code, { onlyEscapeAtSymbol: true }); css.code, { onlyEscapeAtSymbol: true });
if (styles && generator.options.css !== false && !generator.customElement) { if (styles && compiler.options.css !== false && !compiler.customElement) {
builder.addBlock(deindent` builder.addBlock(deindent`
function @add_css() { function @add_css() {
var style = @createElement("style"); var style = @createElement("style");
style.id = '${generator.stylesheet.id}-style'; style.id = '${compiler.stylesheet.id}-style';
style.textContent = ${styles}; style.textContent = ${styles};
@appendNode(style, document.head); @appendNode(style, document.head);
} }
`); `);
} }
generator.blocks.forEach(block => { target.blocks.forEach(block => {
builder.addBlock(block.toString()); builder.addBlock(block.toString());
}); });
@ -144,10 +124,10 @@ export default function dom(
.join(',\n')} .join(',\n')}
}`; }`;
const debugName = `<${generator.customElement ? generator.tag : name}>`; const debugName = `<${compiler.customElement ? compiler.tag : name}>`;
// generate initial state object // 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 globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = expectedProperties.filter(prop => prop[0] === '$'); const storeProps = expectedProperties.filter(prop => prop[0] === '$');
const initialState = []; const initialState = [];
@ -172,31 +152,31 @@ export default function dom(
const constructorBody = deindent` const constructorBody = deindent`
${options.dev && `this._debugName = '${debugName}';`} ${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");`} `if (!options || (!options.target && !options.root)) throw new Error("'target' is a required option");`}
@init(this, options); @init(this, options);
${templateProperties.store && `this.store = %store();`} ${templateProperties.store && `this.store = %store();`}
${generator.usesRefs && `this.refs = {};`} ${compiler.usesRefs && `this.refs = {};`}
this._state = ${initialState.reduce((state, piece) => `@assign(${state}, ${piece})`)}; this._state = ${initialState.reduce((state, piece) => `@assign(${state}, ${piece})`)};
${storeProps.length > 0 && `this.store._add(this, [${storeProps.map(prop => `"${prop.slice(1)}"`)}]);`} ${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);`} ${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`}
${options.dev && ${options.dev &&
Array.from(generator.expectedProperties).map(prop => { Array.from(compiler.expectedProperties).map(prop => {
if (globalWhitelist.has(prop)) return; if (globalWhitelist.has(prop)) return;
if (computations.find(c => c.key === 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} expected to find '${prop}' in \`data\`, but found it in \`components\` instead` :
`${debugName} was created without expected data property '${prop}'`; `${debugName} was created without expected data property '${prop}'`;
const conditions = [`!('${prop}' in this._state)`]; 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}");` return `if (${conditions.join(' && ')}) console.warn("${message}");`
})} })}
${generator.bindingGroups.length && ${compiler.bindingGroups.length &&
`this._bindingGroups = [${Array(generator.bindingGroups.length).fill('[]').join(', ')}];`} `this._bindingGroups = [${Array(compiler.bindingGroups.length).fill('[]').join(', ')}];`}
${templateProperties.onstate && `this._handlers.state = [%onstate];`} ${templateProperties.onstate && `this._handlers.state = [%onstate];`}
${templateProperties.onupdate && `this._handlers.update = [%onupdate];`} ${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` deindent`
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
${css.code && `this.shadowRoot.innerHTML = \`<style>${escape(css.code, { onlyEscapeAtSymbol: true }).replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`} ${css.code && `this.shadowRoot.innerHTML = \`<style>${escape(css.code, { onlyEscapeAtSymbol: true }).replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
` : ` :
(generator.stylesheet.hasStyles && options.css !== false && (compiler.stylesheet.hasStyles && options.css !== false &&
`if (!document.getElementById("${generator.stylesheet.id}-style")) @add_css();`) `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) { if (!options.root) {
this._oncreate = []; this._oncreate = [];
${(generator.hasComponents || generator.hasComplexBindings) && `this._beforecreate = [];`} ${(compiler.hasComponents || target.hasComplexBindings) && `this._beforecreate = [];`}
${(generator.hasComponents || generator.hasIntroTransitions) && `this._aftercreate = [];`} ${(compiler.hasComponents || target.hasIntroTransitions) && `this._aftercreate = [];`}
} }
`} `}
${generator.slots.size && `this.slots = {};`} ${compiler.slots.size && `this.slots = {};`}
this._fragment = @create_main_fragment(this, this._state); 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.c();
this._fragment.${block.hasIntroMethod ? 'i' : 'm'}(this.shadowRoot, null); this._fragment.${block.hasIntroMethod ? 'i' : 'm'}(this.shadowRoot, null);
if (options.target) this._mount(options.target, options.anchor); if (options.target) this._mount(options.target, options.anchor);
` : deindent` ` : deindent`
if (options.target) { if (options.target) {
${generator.hydratable ${compiler.options.hydratable
? deindent` ? deindent`
var nodes = @children(options.target); var nodes = @children(options.target);
options.hydrate ? this._fragment.l(nodes) : this._fragment.c(); options.hydrate ? this._fragment.l(nodes) : this._fragment.c();
@ -257,19 +237,19 @@ export default function dom(
`} `}
this._mount(options.target, options.anchor); this._mount(options.target, options.anchor);
${(generator.hasComponents || generator.hasComplexBindings || hasInitHooks || generator.hasIntroTransitions) && deindent` ${(compiler.hasComponents || target.hasComplexBindings || hasInitHooks || target.hasIntroTransitions) && deindent`
${generator.hasComponents && `this._lock = true;`} ${compiler.hasComponents && `this._lock = true;`}
${(generator.hasComponents || generator.hasComplexBindings) && `@callAll(this._beforecreate);`} ${(compiler.hasComponents || target.hasComplexBindings) && `@callAll(this._beforecreate);`}
${(generator.hasComponents || hasInitHooks) && `@callAll(this._oncreate);`} ${(compiler.hasComponents || hasInitHooks) && `@callAll(this._oncreate);`}
${(generator.hasComponents || generator.hasIntroTransitions) && `@callAll(this._aftercreate);`} ${(compiler.hasComponents || target.hasIntroTransitions) && `@callAll(this._aftercreate);`}
${generator.hasComponents && `this._lock = false;`} ${compiler.hasComponents && `this._lock = false;`}
`} `}
} }
`} `}
`; `;
if (generator.customElement) { if (compiler.customElement) {
const props = generator.props || Array.from(generator.expectedProperties); const props = compiler.props || Array.from(compiler.expectedProperties);
builder.addBlock(deindent` builder.addBlock(deindent`
class ${name} extends HTMLElement { class ${name} extends HTMLElement {
@ -292,7 +272,7 @@ export default function dom(
} }
`).join('\n\n')} `).join('\n\n')}
${generator.slots.size && deindent` ${compiler.slots.size && deindent`
connectedCallback() { connectedCallback() {
Object.keys(this._slotted).forEach(key => { Object.keys(this._slotted).forEach(key => {
this.appendChild(this._slotted[key]); this.appendChild(this._slotted[key]);
@ -303,18 +283,18 @@ export default function dom(
this.set({ [attr]: newValue }); this.set({ [attr]: newValue });
} }
${(generator.hasComponents || generator.hasComplexBindings || templateProperties.oncreate || generator.hasIntroTransitions) && deindent` ${(compiler.hasComponents || target.hasComplexBindings || templateProperties.oncreate || target.hasIntroTransitions) && deindent`
connectedCallback() { connectedCallback() {
${generator.hasComponents && `this._lock = true;`} ${compiler.hasComponents && `this._lock = true;`}
${(generator.hasComponents || generator.hasComplexBindings) && `@callAll(this._beforecreate);`} ${(compiler.hasComponents || target.hasComplexBindings) && `@callAll(this._beforecreate);`}
${(generator.hasComponents || templateProperties.oncreate) && `@callAll(this._oncreate);`} ${(compiler.hasComponents || templateProperties.oncreate) && `@callAll(this._oncreate);`}
${(generator.hasComponents || generator.hasIntroTransitions) && `@callAll(this._aftercreate);`} ${(compiler.hasComponents || target.hasIntroTransitions) && `@callAll(this._aftercreate);`}
${generator.hasComponents && `this._lock = false;`} ${compiler.hasComponents && `this._lock = false;`}
} }
`} `}
} }
customElements.define("${generator.tag}", ${name}); customElements.define("${compiler.tag}", ${name});
@assign(@assign(${prototypeBase}, ${proto}), { @assign(@assign(${prototypeBase}, ${proto}), {
_mount(target, anchor) { _mount(target, anchor) {
target.insertBefore(this, anchor); target.insertBefore(this, anchor);
@ -341,7 +321,7 @@ export default function dom(
builder.addBlock(deindent` builder.addBlock(deindent`
${options.dev && deindent` ${options.dev && deindent`
${name}.prototype._checkReadOnly = function _checkReadOnly(newState) { ${name}.prototype._checkReadOnly = function _checkReadOnly(newState) {
${Array.from(generator.readonly).map( ${Array.from(target.readonly).map(
prop => prop =>
`if ('${prop}' in newState && !this._updatingReadonlyProperty) throw new Error("${debugName}: Cannot set read-only property '${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;`} ${immutable && `${name}.prototype._differs = @_differsImmutable;`}
`); `);
const usedHelpers = new Set(); let result = builder.toString();
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;
}
const filename = options.filename && ( const filename = options.filename && (
typeof process !== 'undefined' ? options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : 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__"} */`, banner: `/* ${filename ? `${filename} ` : ``}generated by Svelte v${"__VERSION__"} */`,
sharedPath, sharedPath,
helpers,
name, name,
format, format,
}); });

@ -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;
}
}

@ -1,45 +1,105 @@
import deindent from '../../utils/deindent'; import deindent from '../../utils/deindent';
import { stringify } from '../../utils/stringify'; import { escape, escapeTemplate, stringify } from '../../utils/stringify';
import fixAttributeCasing from '../../utils/fixAttributeCasing'; import fixAttributeCasing from '../../utils/fixAttributeCasing';
import getExpressionPrecedence from '../../utils/getExpressionPrecedence'; import addToSet from '../../utils/addToSet';
import { DomGenerator } from '../dom/index'; import Compiler from '../Compiler';
import Node from './shared/Node'; import Node from './shared/Node';
import Element from './Element'; import Element from './Element';
import Text from './Text';
import Block from '../dom/Block'; import Block from '../dom/Block';
import Expression from './shared/Expression';
export interface StyleProp { export interface StyleProp {
key: string; key: string;
value: Node[]; value: Node[];
} }
export default class Attribute { export default class Attribute extends Node {
type: 'Attribute'; type: 'Attribute';
start: number; start: number;
end: number; end: number;
generator: DomGenerator; compiler: Compiler;
parent: Element; parent: Element;
name: string; name: string;
value: true | Node[] isSpread: boolean;
expression: Node; isTrue: boolean;
isDynamic: boolean;
constructor({ isSynthetic: boolean;
generator, shouldCache: boolean;
name, expression?: Expression;
value, chunks: (Text | Expression)[];
parent dependencies: Set<string>;
}: {
generator: DomGenerator, constructor(compiler, parent, scope, info) {
name: string, super(compiler, parent, scope, info);
value: Node[],
parent: Element if (info.type === 'Spread') {
}) { this.name = null;
this.type = 'Attribute'; this.isSpread = true;
this.generator = generator; this.isTrue = false;
this.parent = parent; this.isSynthetic = false;
this.name = name; this.expression = new Expression(compiler, this, scope, info.expression);
this.value = value; 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) { render(block: Block) {
@ -47,7 +107,7 @@ export default class Attribute {
const name = fixAttributeCasing(this.name); const name = fixAttributeCasing(this.name);
if (name === 'style') { if (name === 'style') {
const styleProps = optimizeStyle(this.value); const styleProps = optimizeStyle(this.chunks);
if (styleProps) { if (styleProps) {
this.renderStyle(block, styleProps); this.renderStyle(block, styleProps);
return; return;
@ -62,9 +122,9 @@ export default class Attribute {
name === 'value' && name === 'value' &&
(node.name === 'option' || // TODO check it's actually bound (node.name === 'option' || // TODO check it's actually bound
(node.name === 'input' && (node.name === 'input' &&
node.attributes.find( node.bindings.find(
(attribute: Attribute) => (binding: Binding) =>
attribute.type === 'Binding' && /checked|group/.test(attribute.name) /checked|group/.test(binding.name)
))); )));
const propertyName = isIndirectlyBoundValue const propertyName = isIndirectlyBoundValue
@ -78,77 +138,48 @@ export default class Attribute {
? '@setXlinkAttribute' ? '@setXlinkAttribute'
: '@setAttribute'; : '@setAttribute';
const isDynamic = this.isDynamic(); const isLegacyInputType = this.compiler.options.legacy && name === 'type' && this.parent.name === 'input';
const isLegacyInputType = this.generator.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) { const camelCaseName = isDataSet ? name.replace('data-', '').replace(/(-\w)/g, function (m) {
return m[1].toUpperCase(); return m[1].toUpperCase();
}) : name; }) : name;
if (isDynamic) { if (this.isDynamic) {
let value; let value;
const allDependencies = new Set();
let shouldCache;
let hasChangeableIndex;
// TODO some of this code is repeated in Tag.ts — would be good to // 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 // DRY it out if that's possible without introducing crazy indirection
if (this.value.length === 1) { if (this.chunks.length === 1) {
// single {{tag}} — may be a non-string // single {tag} — may be a non-string
const { expression } = this.value[0]; value = this.chunks[0].snippet;
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
);
} else { } else {
// '{{foo}} {{bar}}' — treat as string concatenation // '{foo} {bar}' — treat as string concatenation
value = value =
(this.value[0].type === 'Text' ? '' : `"" + `) + (this.chunks[0].type === 'Text' ? '' : `"" + `) +
this.value this.chunks
.map((chunk: Node) => { .map((chunk: Node) => {
if (chunk.type === 'Text') { if (chunk.type === 'Text') {
return stringify(chunk.data); return stringify(chunk.data);
} else { } else {
const { indexes } = block.contextualise(chunk.expression); return chunk.getPrecedence() <= 13
const { dependencies, snippet } = chunk.metadata; ? `(${chunk.snippet})`
: chunk.snippet;
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;
} }
}) })
.join(' + '); .join(' + ');
shouldCache = true;
} }
const isSelectValueAttribute = const isSelectValueAttribute =
name === 'value' && node.name === 'select'; 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` `${node.var}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
); );
if (shouldCache || isSelectValueAttribute) block.addVariable(last); if (shouldCache) block.addVariable(last);
let updater; let updater;
const init = shouldCache ? `${last} = ${value}` : value; const init = shouldCache ? `${last} = ${value}` : value;
@ -185,8 +216,6 @@ export default class Attribute {
${last} = ${value}; ${last} = ${value};
${updater} ${updater}
`); `);
block.builders.update.addLine(`${last} = ${value};`);
} else if (propertyName) { } else if (propertyName) {
block.builders.hydrate.addLine( block.builders.hydrate.addLine(
`${node.var}.${propertyName} = ${init};` `${node.var}.${propertyName} = ${init};`
@ -204,8 +233,8 @@ export default class Attribute {
updater = `${method}(${node.var}, "${name}", ${shouldCache ? last : value});`; updater = `${method}(${node.var}, "${name}", ${shouldCache ? last : value});`;
} }
if (allDependencies.size || hasChangeableIndex || isSelectValueAttribute) { if (this.dependencies.size || isSelectValueAttribute) {
const dependencies = Array.from(allDependencies); const dependencies = Array.from(this.dependencies);
const changedCheck = ( const changedCheck = (
( block.hasOutroMethod ? `#outroing || ` : '' ) + ( block.hasOutroMethod ? `#outroing || ` : '' ) +
dependencies.map(dependency => `changed.${dependency}`).join(' || ') dependencies.map(dependency => `changed.${dependency}`).join(' || ')
@ -223,9 +252,9 @@ export default class Attribute {
); );
} }
} else { } else {
const value = this.value === true const value = this.isTrue
? 'true' ? 'true'
: this.value.length === 0 ? `""` : stringify(this.value[0].data); : this.chunks.length === 0 ? `""` : stringify(this.chunks[0].data);
const statement = ( const statement = (
isLegacyInputType isLegacyInputType
@ -240,7 +269,7 @@ export default class Attribute {
block.builders.hydrate.addLine(statement); block.builders.hydrate.addLine(statement);
// special case autofocus. has to be handled in a bit of a weird way // 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; block.autofocus = node.var;
} }
} }
@ -249,7 +278,7 @@ export default class Attribute {
const updateValue = `${node.var}.value = ${node.var}.__value;`; const updateValue = `${node.var}.value = ${node.var}.__value;`;
block.builders.hydrate.addLine(updateValue); 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; let value;
if (isDynamic(prop.value)) { if (isDynamic(prop.value)) {
const allDependencies = new Set(); const propDependencies = new Set();
let shouldCache; let shouldCache;
let hasChangeableIndex;
value = value =
((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) + ((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) +
@ -272,26 +300,21 @@ export default class Attribute {
if (chunk.type === 'Text') { if (chunk.type === 'Text') {
return stringify(chunk.data); return stringify(chunk.data);
} else { } else {
const { indexes } = block.contextualise(chunk.expression); const { dependencies, snippet } = chunk;
const { dependencies, snippet } = chunk.metadata;
if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
hasChangeableIndex = true;
}
dependencies.forEach(d => { dependencies.forEach(d => {
allDependencies.add(d); propDependencies.add(d);
}); });
return getExpressionPrecedence(chunk.expression) <= 13 ? `( ${snippet} )` : snippet; return chunk.getPrecedence() <= 13 ? `(${snippet})` : snippet;
} }
}) })
.join(' + '); .join(' + ');
if (allDependencies.size || hasChangeableIndex) { if (propDependencies.size) {
const dependencies = Array.from(allDependencies); const dependencies = Array.from(propDependencies);
const condition = ( const condition = (
( block.hasOutroMethod ? `#outroing || ` : '' ) + (block.hasOutroMethod ? `#outroing || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ') dependencies.map(dependency => `changed.${dependency}`).join(' || ')
); );
@ -310,10 +333,16 @@ export default class Attribute {
}); });
} }
isDynamic() { stringifyForSsr() {
if (this.value === true || this.value.length === 0) return false; return this.chunks
if (this.value.length > 1) return true; .map((chunk: Node) => {
return this.value[0].type !== 'Text'; if (chunk.type === 'Text') {
return escapeTemplate(escape(chunk.data).replace(/"/g, '&quot;'));
}
return '${@escape(' + chunk.snippet + ')}';
})
.join('');
} }
} }

@ -1,21 +1,36 @@
import deindent from '../../utils/deindent'; import deindent from '../../utils/deindent';
import Node from './shared/Node'; import Node from './shared/Node';
import { DomGenerator } from '../dom/index';
import Block from '../dom/Block'; import Block from '../dom/Block';
import PendingBlock from './PendingBlock'; import PendingBlock from './PendingBlock';
import ThenBlock from './ThenBlock'; import ThenBlock from './ThenBlock';
import CatchBlock from './CatchBlock'; import CatchBlock from './CatchBlock';
import createDebuggingComment from '../../utils/createDebuggingComment'; import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression';
import { SsrTarget } from '../ssr';
export default class AwaitBlock extends Node { export default class AwaitBlock extends Node {
expression: Expression;
value: string; value: string;
error: string; error: string;
expression: Node;
pending: PendingBlock; pending: PendingBlock;
then: ThenBlock; then: ThenBlock;
catch: CatchBlock; 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( init(
block: Block, block: Block,
stripWhitespace: boolean, stripWhitespace: boolean,
@ -24,42 +39,30 @@ export default class AwaitBlock extends Node {
this.cannotUseInnerHTML(); this.cannotUseInnerHTML();
this.var = block.getUniqueName('await_block'); this.var = block.getUniqueName('await_block');
block.addDependencies(this.metadata.dependencies); block.addDependencies(this.expression.dependencies);
let dynamic = false; let isDynamic = false;
[ ['pending', 'then', 'catch'].forEach(status => {
['pending', null],
['then', this.value],
['catch', this.error]
].forEach(([status, arg]) => {
const child = this[status]; const child = this[status];
child.block = block.child({ child.block = block.child({
comment: createDebuggingComment(child, this.generator), comment: createDebuggingComment(child, this.compiler),
name: this.generator.getUniqueName(`create_${status}_block`), name: this.compiler.getUniqueName(`create_${status}_block`)
contexts: new Map(block.contexts),
contextTypes: new Map(block.contextTypes)
}); });
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); 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) { if (child.block.dependencies.size > 0) {
dynamic = true; isDynamic = true;
block.addDependencies(child.block.dependencies); block.addDependencies(child.block.dependencies);
} }
}); });
this.pending.block.hasUpdateMethod = dynamic; this.pending.block.hasUpdateMethod = isDynamic;
this.then.block.hasUpdateMethod = dynamic; this.then.block.hasUpdateMethod = isDynamic;
this.catch.block.hasUpdateMethod = dynamic; this.catch.block.hasUpdateMethod = isDynamic;
} }
build( build(
@ -72,8 +75,7 @@ export default class AwaitBlock extends Node {
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes); const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
const updateMountNode = this.getUpdateMountNode(anchor); const updateMountNode = this.getUpdateMountNode(anchor);
block.contextualise(this.expression); const { snippet } = this.expression;
const { snippet } = this.metadata;
const promise = block.getUniqueName(`promise`); const promise = block.getUniqueName(`promise`);
const resolved = block.getUniqueName(`resolved`); const resolved = block.getUniqueName(`resolved`);
@ -101,11 +103,11 @@ export default class AwaitBlock extends Node {
// but it's probably not worth it // but it's probably not worth it
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
function ${replace_await_block}(${token}, type, state) { function ${replace_await_block}(${token}, type, ctx) {
if (${token} !== ${await_token}) return; if (${token} !== ${await_token}) return;
var ${old_block} = ${await_block}; 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}) { if (${old_block}) {
${old_block}.u(); ${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} = {}; var ${token} = ${await_token} = {};
if (@isPromise(${promise})) { if (@isPromise(${promise})) {
${promise}.then(function(${value}) { ${promise}.then(function(${value}) {
${this.then.block.context ? deindent` ${this.value ? deindent`
var state = #component.get(); var ctx = #component.get();
${resolved} = { ${this.then.block.context}: ${value} }; ${resolved} = { ${this.value}: ${value} };
${replace_await_block}(${token}, ${create_then_block}, @assign(@assign({}, state), ${resolved})); ${replace_await_block}(${token}, ${create_then_block}, @assign(@assign({}, ctx), ${resolved}));
` : deindent` ` : deindent`
${replace_await_block}(${token}, null, null); ${replace_await_block}(${token}, null, null);
`} `}
}, function (${error}) { }, function (${error}) {
${this.catch.block.context ? deindent` ${this.error ? deindent`
var state = #component.get(); var ctx = #component.get();
${resolved} = { ${this.catch.block.context}: ${error} }; ${resolved} = { ${this.error}: ${error} };
${replace_await_block}(${token}, ${create_catch_block}, @assign(@assign({}, state), ${resolved})); ${replace_await_block}(${token}, ${create_catch_block}, @assign(@assign({}, ctx), ${resolved}));
` : deindent` ` : deindent`
${replace_await_block}(${token}, null, null); ${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 we previously had a then/catch block, destroy it
if (${await_block_type} !== ${create_pending_block}) { 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; return true;
} }
} else { } else {
${resolved} = { ${this.then.block.context}: ${promise} }; ${resolved} = { ${this.value}: ${promise} };
if (${await_block_type} !== ${create_then_block}) { 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; return true;
} }
} }
} }
${handle_promise}(${promise} = ${snippet}, state); ${handle_promise}(${promise} = ${snippet}, ctx);
`); `);
block.builders.create.addBlock(deindent` block.builders.create.addBlock(deindent`
@ -174,15 +176,15 @@ export default class AwaitBlock extends Node {
`); `);
const conditions = []; const conditions = [];
if (this.metadata.dependencies) { if (this.expression.dependencies.size > 0) {
conditions.push( conditions.push(
`(${this.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})` `(${[...this.expression.dependencies].map(dep => `'${dep}' in changed`).join(' || ')})`
); );
} }
conditions.push( conditions.push(
`${promise} !== (${promise} = ${snippet})`, `${promise} !== (${promise} = ${snippet})`,
`${handle_promise}(${promise}, state)` `${handle_promise}(${promise}, ctx)`
); );
if (this.pending.block.hasUpdateMethod) { if (this.pending.block.hasUpdateMethod) {
@ -190,7 +192,7 @@ export default class AwaitBlock extends Node {
if (${conditions.join(' && ')}) { if (${conditions.join(' && ')}) {
// nothing // nothing
} else { } else {
${await_block}.p(changed, @assign(@assign({}, state), ${resolved})); ${await_block}.p(changed, @assign(@assign({}, ctx), ${resolved}));
} }
`); `);
} else { } else {
@ -217,4 +219,23 @@ export default class AwaitBlock extends Node {
}); });
}); });
} }
ssr() {
const target: SsrTarget = <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})) }`);
}
} }

@ -3,8 +3,9 @@ import Element from './Element';
import getObject from '../../utils/getObject'; import getObject from '../../utils/getObject';
import getTailSnippet from '../../utils/getTailSnippet'; import getTailSnippet from '../../utils/getTailSnippet';
import flattenReference from '../../utils/flattenReference'; import flattenReference from '../../utils/flattenReference';
import { DomGenerator } from '../dom/index'; import Compiler from '../Compiler';
import Block from '../dom/Block'; import Block from '../dom/Block';
import Expression from './shared/Expression';
const readOnlyMediaAttributes = new Set([ const readOnlyMediaAttributes = new Set([
'duration', 'duration',
@ -15,12 +16,43 @@ const readOnlyMediaAttributes = new Set([
export default class Binding extends Node { export default class Binding extends Node {
name: string; name: string;
value: Node; value: Expression;
expression: Node; 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( munge(
block: Block, block: Block
allUsedContexts: Set<string>
) { ) {
const node: Element = this.parent; const node: Element = this.parent;
@ -29,32 +61,27 @@ export default class Binding extends Node {
let updateCondition: string; let updateCondition: string;
const { name } = getObject(this.value); const { name } = getObject(this.value.node);
const { contexts } = block.contextualise(this.value); const { snippet } = this.value;
const { snippet } = this.metadata;
// special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>` // special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>`
// and `selected` is an object chosen with a <select>, then when `checked` changes, // and `selected` is an object chosen with a <select>, then when `checked` changes,
// we need to tell the component to update all the values `selected` might be // we need to tell the component to update all the values `selected` might be
// pointing to // pointing to
// TODO should this happen in preprocess? // TODO should this happen in preprocess?
const dependencies = this.metadata.dependencies.slice(); const dependencies = new Set(this.value.dependencies);
this.metadata.dependencies.forEach((prop: string) => { this.value.dependencies.forEach((prop: string) => {
const indirectDependencies = this.generator.indirectDependencies.get(prop); const indirectDependencies = this.compiler.indirectDependencies.get(prop);
if (indirectDependencies) { if (indirectDependencies) {
indirectDependencies.forEach(indirectDependency => { indirectDependencies.forEach(indirectDependency => {
if (!~dependencies.indexOf(indirectDependency)) dependencies.push(indirectDependency); dependencies.add(indirectDependency);
}); });
} }
}); });
contexts.forEach(context => {
allUsedContexts.add(context);
});
// view to model // view to model
const valueFromDom = getValueFromDom(this.generator, node, this); const valueFromDom = getValueFromDom(this.compiler, node, this);
const handler = getEventHandler(this.generator, block, name, snippet, this, dependencies, valueFromDom); const handler = getEventHandler(this, this.compiler, block, name, snippet, dependencies, valueFromDom);
// model to view // model to view
let updateDom = getDomUpdater(node, this, snippet); let updateDom = getDomUpdater(node, this, snippet);
@ -62,7 +89,7 @@ export default class Binding extends Node {
// special cases // special cases
if (this.name === 'group') { if (this.name === 'group') {
const bindingGroup = getBindingGroup(this.generator, this.value); const bindingGroup = getBindingGroup(this.compiler, this.value.node);
block.builders.hydrate.addLine( block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${node.var});` `#component._bindingGroups[${bindingGroup}].push(${node.var});`
@ -135,67 +162,68 @@ function getDomUpdater(
return `${node.var}.${binding.name} = ${snippet};`; return `${node.var}.${binding.name} = ${snippet};`;
} }
function getBindingGroup(generator: DomGenerator, value: Node) { function getBindingGroup(compiler: Compiler, value: Node) {
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
const keypath = parts.join('.'); const keypath = parts.join('.');
// TODO handle contextual bindings — `keypath` should include unique ID of // TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context // each block that provides context
let index = generator.bindingGroups.indexOf(keypath); let index = compiler.bindingGroups.indexOf(keypath);
if (index === -1) { if (index === -1) {
index = generator.bindingGroups.length; index = compiler.bindingGroups.length;
generator.bindingGroups.push(keypath); compiler.bindingGroups.push(keypath);
} }
return index; return index;
} }
function getEventHandler( function getEventHandler(
generator: DomGenerator, binding: Binding,
compiler: Compiler,
block: Block, block: Block,
name: string, name: string,
snippet: string, snippet: string,
attribute: Node,
dependencies: string[], dependencies: string[],
value: string, value: string,
isContextual: boolean
) { ) {
const storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1)); const storeDependencies = [...dependencies].filter(prop => prop[0] === '$').map(prop => prop.slice(1));
dependencies = dependencies.filter(prop => prop[0] !== '$'); dependencies = [...dependencies].filter(prop => prop[0] !== '$');
if (block.contexts.has(name)) { if (binding.isContextual) {
const tail = attribute.value.type === 'MemberExpression' const tail = binding.value.node.type === 'MemberExpression'
? getTailSnippet(attribute.value) ? getTailSnippet(binding.value.node)
: ''; : '';
const list = `context.${block.listNames.get(name)}`; const list = `ctx.${block.listNames.get(name)}`;
const index = `context.${block.indexNames.get(name)}`; const index = `ctx.${block.indexNames.get(name)}`;
return { return {
usesContext: true, usesContext: true,
usesState: true, usesState: true,
usesStore: storeDependencies.length > 0, usesStore: storeDependencies.length > 0,
mutation: `${list}[${index}]${tail} = ${value};`, mutation: `${list}[${index}]${tail} = ${value};`,
props: dependencies.map(prop => `${prop}: state.${prop}`), props: dependencies.map(prop => `${prop}: ctx.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`) storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
}; };
} }
if (attribute.value.type === 'MemberExpression') { if (binding.value.node.type === 'MemberExpression') {
// This is a little confusing, and should probably be tidied up // This is a little confusing, and should probably be tidied up
// at some point. It addresses a tricky bug (#893), wherein // at some point. It addresses a tricky bug (#893), wherein
// Svelte tries to `set()` a computed property, which throws an // Svelte tries to `set()` a computed property, which throws an
// error in dev mode. a) it's possible that we should be // error in dev mode. a) it's possible that we should be
// replacing computations with *their* dependencies, and b) // replacing computations with *their* dependencies, and b)
// we should probably populate `generator.readonly` sooner so // we should probably populate `compiler.target.readonly` sooner so
// that we don't have to do the `.some()` here // that we don't have to do the `.some()` here
dependencies = dependencies.filter(prop => !generator.computations.some(computation => computation.key === prop)); dependencies = dependencies.filter(prop => !compiler.computations.some(computation => computation.key === prop));
return { return {
usesContext: false, usesContext: false,
usesState: true, usesState: true,
usesStore: storeDependencies.length > 0, usesStore: storeDependencies.length > 0,
mutation: `${snippet} = ${value}`, mutation: `${snippet} = ${value}`,
props: dependencies.map((prop: string) => `${prop}: state.${prop}`), props: dependencies.map((prop: string) => `${prop}: ctx.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`) storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
}; };
} }
@ -222,7 +250,7 @@ function getEventHandler(
} }
function getValueFromDom( function getValueFromDom(
generator: DomGenerator, compiler: Compiler,
node: Element, node: Element,
binding: Node binding: Node
) { ) {
@ -237,7 +265,7 @@ function getValueFromDom(
// <input type='checkbox' bind:group='foo'> // <input type='checkbox' bind:group='foo'>
if (binding.name === 'group') { if (binding.name === 'group') {
const bindingGroup = getBindingGroup(generator, binding.value); const bindingGroup = getBindingGroup(compiler, binding.value.node);
if (type === 'checkbox') { if (type === 'checkbox') {
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`; return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
} }

@ -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);
}
}

@ -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(`<!--${this.data}-->`);
}
}
}

@ -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;
}

@ -3,22 +3,57 @@ import Node from './shared/Node';
import ElseBlock from './ElseBlock'; import ElseBlock from './ElseBlock';
import Block from '../dom/Block'; import Block from '../dom/Block';
import createDebuggingComment from '../../utils/createDebuggingComment'; 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 { export default class EachBlock extends Node {
type: 'EachBlock'; type: 'EachBlock';
block: Block; block: Block;
expression: Node; expression: Expression;
iterations: string; iterations: string;
index: string; index: string;
context: string; context: string;
key: string; key: string;
scope: TemplateScope;
destructuredContexts: string[]; destructuredContexts: string[];
children: Node[]; children: Node[];
else?: ElseBlock; 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( init(
block: Block, block: Block,
stripWhitespace: boolean, stripWhitespace: boolean,
@ -30,45 +65,26 @@ export default class EachBlock extends Node {
this.iterations = block.getUniqueName(`${this.var}_blocks`); this.iterations = block.getUniqueName(`${this.var}_blocks`);
this.each_context = block.getUniqueName(`${this.var}_context`); this.each_context = block.getUniqueName(`${this.var}_context`);
const { dependencies } = this.metadata; const { dependencies } = this.expression;
block.addDependencies(dependencies); block.addDependencies(dependencies);
this.block = block.child({ this.block = block.child({
comment: createDebuggingComment(this, this.generator), comment: createDebuggingComment(this, this.compiler),
name: this.generator.getUniqueName('create_each_block'), name: this.compiler.getUniqueName('create_each_block'),
context: this.context,
key: this.key, 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), indexNames: new Map(block.indexNames),
listNames: new Map(block.listNames) listNames: new Map(block.listNames)
}); });
const listName = this.generator.getUniqueName('each_value'); const listName = this.compiler.getUniqueName('each_value');
const indexName = this.index || this.generator.getUniqueName(`${this.context}_index`); 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.indexNames.set(this.context, indexName);
this.block.listNames.set(this.context, listName); this.block.listNames.set(this.context, listName);
if (this.index) { if (this.index) {
this.block.getUniqueName(this.index); // this prevents name collisions (#1254) 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 = [ 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); this.initChildren(this.block, stripWhitespace, nextSibling);
block.addDependencies(this.block.dependencies); block.addDependencies(this.block.dependencies);
this.block.hasUpdateMethod = this.block.dependencies.size > 0; this.block.hasUpdateMethod = this.block.dependencies.size > 0;
if (this.else) { if (this.else) {
this.else.block = block.child({ this.else.block = block.child({
comment: createDebuggingComment(this.else, this.generator), comment: createDebuggingComment(this.else, this.compiler),
name: this.generator.getUniqueName(`${this.block.name}_else`), 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.initChildren(
this.else.block, this.else.block,
stripWhitespace, stripWhitespace,
@ -111,7 +127,7 @@ export default class EachBlock extends Node {
) { ) {
if (this.children.length === 0) return; if (this.children.length === 0) return;
const { generator } = this; const { compiler } = this;
const each = this.var; 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 // hack the sourcemap, so that if data is missing the bug
// is easy to find // is easy to find
let c = this.start + 2; let c = this.start + 2;
while (generator.source[c] !== 'e') c += 1; while (compiler.source[c] !== 'e') c += 1;
generator.code.overwrite(c, c + 4, 'length'); compiler.code.overwrite(c, c + 4, 'length');
const length = `[✂${c}-${c+4}✂]`; const length = `[✂${c}-${c+4}✂]`;
const mountOrIntro = this.block.hasIntroMethod ? 'i' : 'm'; const mountOrIntro = this.block.hasIntroMethod ? 'i' : 'm';
@ -142,8 +158,7 @@ export default class EachBlock extends Node {
mountOrIntro, mountOrIntro,
}; };
block.contextualise(this.expression); const { snippet } = this.expression;
const { snippet } = this.metadata;
block.builders.init.addLine(`var ${each_block_value} = ${snippet};`); block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
@ -163,14 +178,14 @@ export default class EachBlock extends Node {
} }
if (this.else) { 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;`); block.builders.init.addLine(`var ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block // TODO neaten this up... will end up with an empty line in the block
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
if (!${each_block_value}.${length}) { 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}.c();
} }
`); `);
@ -186,9 +201,9 @@ export default class EachBlock extends Node {
if (this.else.block.hasUpdateMethod) { if (this.else.block.hasUpdateMethod) {
block.builders.update.addBlock(deindent` block.builders.update.addBlock(deindent`
if (!${each_block_value}.${length} && ${each_block_else}) { 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}) { } 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}.c();
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor}); ${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
} else if (${each_block_else}) { } else if (${each_block_else}) {
@ -206,7 +221,7 @@ export default class EachBlock extends Node {
${each_block_else} = null; ${each_block_else} = null;
} }
} else if (!${each_block_else}) { } 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}.c();
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor}); ${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
} }
@ -269,7 +284,7 @@ export default class EachBlock extends Node {
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) { for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
var ${key} = ${each_block_value}[#i].${this.key}; 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')} ${this.contextProps.join(',\n')}
})); }));
} }
@ -299,7 +314,7 @@ export default class EachBlock extends Node {
var ${each_block_value} = ${snippet}; 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) { ${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')} ${this.contextProps.join(',\n')}
}); });
}); });
@ -334,7 +349,7 @@ export default class EachBlock extends Node {
var ${iterations} = []; var ${iterations} = [];
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) { 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')} ${this.contextProps.join(',\n')}
})); }));
} }
@ -365,7 +380,7 @@ export default class EachBlock extends Node {
`); `);
const allDependencies = new Set(this.block.dependencies); const allDependencies = new Set(this.block.dependencies);
const { dependencies } = this.metadata; const { dependencies } = this.expression;
dependencies.forEach((dependency: string) => { dependencies.forEach((dependency: string) => {
allDependencies.add(dependency); allDependencies.add(dependency);
}); });
@ -432,7 +447,7 @@ export default class EachBlock extends Node {
if (${condition}) { if (${condition}) {
for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) { 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')} ${this.contextProps.join(',\n')}
}); });
@ -457,4 +472,36 @@ export default class EachBlock extends Node {
// TODO consider keyed blocks // TODO consider keyed blocks
return `for (var #i = 0; #i < ${this.iterations}.length; #i += 1) ${this.iterations}[#i].m(${name}._slotted.default, null);`; 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('}');
}
} }

@ -6,24 +6,132 @@ import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames'; import reservedNames from '../../utils/reservedNames';
import fixAttributeCasing from '../../utils/fixAttributeCasing'; import fixAttributeCasing from '../../utils/fixAttributeCasing';
import quoteIfNecessary from '../../utils/quoteIfNecessary'; import quoteIfNecessary from '../../utils/quoteIfNecessary';
import mungeAttribute from './shared/mungeAttribute'; import Compiler from '../Compiler';
import Node from './shared/Node'; import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import Attribute from './Attribute'; import Attribute from './Attribute';
import Binding from './Binding'; import Binding from './Binding';
import EventHandler from './EventHandler'; import EventHandler from './EventHandler';
import Ref from './Ref';
import Transition from './Transition'; import Transition from './Transition';
import Action from './Action'; import Action from './Action';
import Text from './Text'; import Text from './Text';
import * as namespaces from '../../utils/namespaces'; 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 { export default class Element extends Node {
type: 'Element'; type: 'Element';
name: string; 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[]; 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 <textarea>
// children treated the same way as a value attribute
if (info.children.length > 0) {
info.attributes.push({
type: 'Attribute',
name: 'value',
value: info.children
});
info.children = [];
}
}
if (this.name === 'option') {
// Special case — treat these the same way:
// <option>{foo}</option>
// <option value={foo}>{foo}</option>
const valueAttribute = info.attributes.find((attribute: Node) => attribute.name === 'value');
if (!valueAttribute) {
info.attributes.push({
type: 'Attribute',
name: 'value',
value: info.children,
synthetic: true
});
}
}
info.attributes.forEach(node => {
switch (node.type) {
case 'Action':
this.actions.push(new Action(compiler, this, scope, node));
break;
case 'Attribute':
case 'Spread':
// special case
if (node.name === 'xmlns') this.namespace = node.value[0].data;
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 'Transition':
const transition = new Transition(compiler, this, scope, node);
if (node.intro) this.intro = transition;
if (node.outro) this.outro = transition;
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}`);
}
});
// TODO break out attributes and directives here
this.children = mapChildren(compiler, this, scope, info.children);
compiler.stylesheet.apply(this);
}
init( init(
block: Block, block: Block,
stripWhitespace: boolean, stripWhitespace: boolean,
@ -33,103 +141,75 @@ export default class Element extends Node {
this.cannotUseInnerHTML(); this.cannotUseInnerHTML();
} }
const parentElement = this.parent && this.parent.findNearest(/^Element/); this.attributes.forEach(attr => {
this.namespace = this.name === 'svg' ? if (attr.dependencies.size) {
namespaces.svg : this.parent.cannotUseInnerHTML();
parentElement ? parentElement.namespace : this.generator.namespace; block.addDependencies(attr.dependencies);
this.attributes.forEach(attribute => { // special case — <option value={foo}> — see below
if (attribute.type === 'Attribute' && attribute.value !== true) { if (this.name === 'option' && attr.name === 'value') {
// special case — xmlns let select = this.parent;
if (attribute.name === 'xmlns') { while (select && (select.type !== 'Element' || select.name !== 'select')) select = select.parent;
// TODO this attribute must be static enforce at compile time
this.namespace = attribute.value[0].data; if (select && select.selectBindingDependencies) {
} select.selectBindingDependencies.forEach(prop => {
attr.dependencies.forEach((dependency: string) => {
attribute.value.forEach((chunk: Node) => { this.compiler.indirectDependencies.get(prop).add(dependency);
if (chunk.type !== 'Text') { });
if (this.parent) this.parent.cannotUseInnerHTML(); });
const dependencies = chunk.metadata.dependencies;
block.addDependencies(dependencies);
// special case — <option value='{{foo}}'> — see below
if (
this.name === 'option' &&
attribute.name === 'value'
) {
let select = this.parent;
while (select && (select.type !== 'Element' || select.name !== 'select')) select = select.parent;
if (select && select.selectBindingDependencies) {
select.selectBindingDependencies.forEach(prop => {
dependencies.forEach((dependency: string) => {
this.generator.indirectDependencies.get(prop).add(dependency);
});
});
}
}
}
});
} else {
if (this.parent) this.parent.cannotUseInnerHTML();
if (attribute.type === 'EventHandler' && attribute.expression) {
attribute.expression.arguments.forEach((arg: Node) => {
block.addDependencies(arg.metadata.dependencies);
});
} else if (attribute.type === 'Binding') {
block.addDependencies(attribute.metadata.dependencies);
} else if (attribute.type === 'Transition') {
if (attribute.intro)
this.generator.hasIntroTransitions = block.hasIntroMethod = true;
if (attribute.outro) {
this.generator.hasOutroTransitions = block.hasOutroMethod = true;
block.outros += 1;
} }
} else if (attribute.type === 'Action' && attribute.expression) {
block.addDependencies(attribute.metadata.dependencies);
} else if (attribute.type === 'Spread') {
block.addDependencies(attribute.metadata.dependencies);
} }
} }
}); });
const valueAttribute = this.attributes.find((attribute: Attribute) => attribute.name === 'value'); this.actions.forEach(action => {
this.parent.cannotUseInnerHTML();
if (action.expression) {
block.addDependencies(action.expression.dependencies);
}
});
if (this.name === 'textarea') { this.bindings.forEach(binding => {
// this is an egregious hack, but it's the easiest way to get <textarea> this.parent.cannotUseInnerHTML();
// children treated the same way as a value attribute block.addDependencies(binding.value.dependencies);
if (this.children.length > 0) { });
this.attributes.push(new Attribute({
generator: this.generator,
name: 'value',
value: this.children,
parent: this
}));
this.children = []; this.handlers.forEach(handler => {
} this.parent.cannotUseInnerHTML();
block.addDependencies(handler.dependencies);
});
if (this.intro) {
this.parent.cannotUseInnerHTML();
this.compiler.target.hasIntroTransitions = block.hasIntroMethod = true;
} }
if (this.outro) {
this.parent.cannotUseInnerHTML();
this.compiler.target.hasOutroTransitions = block.hasOutroMethod = true;
block.outros += 1;
}
const valueAttribute = this.attributes.find((attribute: Attribute) => attribute.name === 'value');
// special case — in a case like this... // special case — in a case like this...
// //
// <select bind:value='foo'> // <select bind:value='foo'>
// <option value='{{bar}}'>bar</option> // <option value='{bar}'>bar</option>
// <option value='{{baz}}'>baz</option> // <option value='{baz}'>baz</option>
// </option> // </option>
// //
// ...we need to know that `foo` depends on `bar` and `baz`, // ...we need to know that `foo` depends on `bar` and `baz`,
// so that if `foo.qux` changes, we know that we need to // so that if `foo.qux` changes, we know that we need to
// mark `bar` and `baz` as dirty too // mark `bar` and `baz` as dirty too
if (this.name === 'select') { if (this.name === 'select') {
const binding = this.attributes.find(node => node.type === 'Binding' && node.name === 'value'); const binding = this.bindings.find(node => node.name === 'value');
if (binding) { if (binding) {
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`? // TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
const dependencies = binding.metadata.dependencies; const dependencies = binding.value.dependencies;
this.selectBindingDependencies = dependencies; this.selectBindingDependencies = dependencies;
dependencies.forEach((prop: string) => { dependencies.forEach((prop: string) => {
this.generator.indirectDependencies.set(prop, new Set()); this.compiler.indirectDependencies.set(prop, new Set());
}); });
} else { } else {
this.selectBindingDependencies = null; this.selectBindingDependencies = null;
@ -145,10 +225,6 @@ export default class Element extends Node {
component._slots.add(slot); component._slots.add(slot);
} }
if (this.spread) {
block.addDependencies(this.spread.metadata.dependencies);
}
this.var = block.getUniqueName( this.var = block.getUniqueName(
this.name.replace(/[^a-zA-Z0-9_$]/g, '_') this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
); );
@ -164,11 +240,11 @@ export default class Element extends Node {
parentNode: string, parentNode: string,
parentNodes: string parentNodes: string
) { ) {
const { generator } = this; const { compiler } = this;
if (this.name === 'slot') { if (this.name === 'slot') {
const slotName = this.getStaticAttributeValue('name') || 'default'; const slotName = this.getStaticAttributeValue('name') || 'default';
this.generator.slots.add(slotName); this.compiler.slots.add(slotName);
} }
if (this.name === 'noscript') return; if (this.name === 'noscript') return;
@ -179,23 +255,22 @@ export default class Element extends Node {
}; };
const name = this.var; const name = this.var;
const allUsedContexts: Set<string> = new Set();
const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot'); const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot');
const initialMountNode = this.slotted ? const initialMountNode = this.slotted ?
`${this.findNearest(/^Component/).var}._slotted.${slot.value[0].data}` : // TODO this looks bonkers `${this.findNearest(/^Component/).var}._slotted.${slot.chunks[0].data}` : // TODO this looks bonkers
parentNode; parentNode;
block.addVariable(name); block.addVariable(name);
const renderStatement = getRenderStatement(this.generator, this.namespace, this.name); const renderStatement = getRenderStatement(this.compiler, this.namespace, this.name);
block.builders.create.addLine( block.builders.create.addLine(
`${name} = ${renderStatement};` `${name} = ${renderStatement};`
); );
if (this.generator.hydratable) { if (this.compiler.options.hydratable) {
if (parentNodes) { if (parentNodes) {
block.builders.claim.addBlock(deindent` block.builders.claim.addBlock(deindent`
${name} = ${getClaimStatement(generator, this.namespace, parentNodes, this)}; ${name} = ${getClaimStatement(compiler, this.namespace, parentNodes, this)};
var ${childState.parentNodes} = @children(${name}); var ${childState.parentNodes} = @children(${name});
`); `);
} else { } else {
@ -245,49 +320,52 @@ export default class Element extends Node {
}); });
} }
this.addBindings(block, allUsedContexts); let hasHoistedEventHandlerOrBinding = (
const eventHandlerUsesComponent = this.addEventHandlers(block, allUsedContexts); //(this.hasAncestor('EachBlock') && this.bindings.length > 0) ||
this.addRefs(block); this.handlers.some(handler => handler.shouldHoist)
this.addAttributes(block); );
this.addTransitions(block); const eventHandlerOrBindingUsesComponent = (
this.addActions(block); this.bindings.length > 0 ||
this.handlers.some(handler => handler.usesComponent)
);
const eventHandlerOrBindingUsesContext = (
this.bindings.some(binding => binding.usesContext) ||
this.handlers.some(handler => handler.usesContext)
);
if (allUsedContexts.size || eventHandlerUsesComponent) { if (hasHoistedEventHandlerOrBinding) {
const initialProps: string[] = []; const initialProps: string[] = [];
const updates: string[] = []; const updates: string[] = [];
if (eventHandlerUsesComponent) { if (eventHandlerOrBindingUsesComponent) {
initialProps.push(`component: #component`); const component = block.alias('component');
initialProps.push(component === 'component' ? 'component' : `component: ${component}`);
} }
allUsedContexts.forEach((contextName: string) => { if (eventHandlerOrBindingUsesContext) {
if (contextName === 'state') return; initialProps.push(`ctx`);
if (block.contextTypes.get(contextName) !== 'each') return; block.builders.update.addLine(`${name}._svelte.ctx = ctx;`);
}
const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get(contextName);
initialProps.push(
`${listName}: state.${listName},\n${indexName}: state.${indexName}`
);
updates.push(
`${name}._svelte.${listName} = state.${listName};\n${name}._svelte.${indexName} = state.${indexName};`
);
});
if (initialProps.length) { if (initialProps.length) {
block.builders.hydrate.addBlock(deindent` block.builders.hydrate.addBlock(deindent`
${name}._svelte = { ${name}._svelte = { ${initialProps.join(', ')} };
${initialProps.join(',\n')}
};
`); `);
} }
} else {
if (updates.length) { if (eventHandlerOrBindingUsesContext) {
block.builders.update.addBlock(updates.join('\n')); block.maintainContext = true;
} }
} }
this.addBindings(block);
this.addEventHandlers(block);
if (this.ref) this.addRef(block);
this.addAttributes(block);
this.addTransitions(block);
this.addActions(block);
if (this.initialUpdate) { if (this.initialUpdate) {
block.builders.mount.addBlock(this.initialUpdate); block.builders.mount.addBlock(this.initialUpdate);
} }
@ -316,7 +394,7 @@ export default class Element extends Node {
} }
node.attributes.forEach((attr: Node) => { node.attributes.forEach((attr: Node) => {
open += ` ${fixAttributeCasing(attr.name)}${stringifyAttributeValue(attr.value)}` open += ` ${fixAttributeCasing(attr.name)}${stringifyAttributeValue(attr.chunks)}`
}); });
if (isVoidElementName(node.name)) return open + '>'; if (isVoidElementName(node.name)) return open + '>';
@ -326,17 +404,16 @@ export default class Element extends Node {
} }
addBindings( addBindings(
block: Block, block: Block
allUsedContexts: Set<string>
) { ) {
const bindings: Binding[] = this.attributes.filter((a: Binding) => a.type === 'Binding'); if (this.bindings.length === 0) return;
if (bindings.length === 0) return;
if (this.name === 'select' || this.isMediaNode()) this.generator.hasComplexBindings = true; if (this.name === 'select' || this.isMediaNode()) this.compiler.target.hasComplexBindings = true;
const needsLock = this.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type')); const needsLock = this.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type'));
const mungedBindings = bindings.map(binding => binding.munge(block, allUsedContexts)); // TODO munge in constructor
const mungedBindings = this.bindings.map(binding => binding.munge(block));
const lock = mungedBindings.some(binding => binding.needsLock) ? const lock = mungedBindings.some(binding => binding.needsLock) ?
block.getUniqueName(`${this.var}_updating`) : block.getUniqueName(`${this.var}_updating`) :
@ -402,8 +479,6 @@ export default class Element extends Node {
cancelAnimationFrame(${animation_frame}); cancelAnimationFrame(${animation_frame});
if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});` if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
} }
${usesContext && `var context = ${this.var}._svelte;`}
${usesState && `var state = #component.get();`}
${usesStore && `var $ = #component.store.get();`} ${usesStore && `var $ = #component.store.get();`}
${needsLock && `${lock} = true;`} ${needsLock && `${lock} = true;`}
${mutations.length > 0 && mutations} ${mutations.length > 0 && mutations}
@ -424,11 +499,11 @@ export default class Element extends Node {
}); });
const allInitialStateIsDefined = group.bindings const allInitialStateIsDefined = group.bindings
.map(binding => `'${binding.object}' in state`) .map(binding => `'${binding.object}' in ctx`)
.join(' && '); .join(' && ');
if (this.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) { if (this.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
this.generator.hasComplexBindings = true; this.compiler.target.hasComplexBindings = true;
block.builders.hydrate.addLine( block.builders.hydrate.addLine(
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});` `if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
@ -445,7 +520,7 @@ export default class Element extends Node {
return; return;
} }
this.attributes.filter((a: Attribute) => a.type === 'Attribute').forEach((attribute: Attribute) => { this.attributes.forEach((attribute: Attribute) => {
attribute.render(block); attribute.render(block);
}); });
} }
@ -460,23 +535,20 @@ export default class Element extends Node {
this.attributes this.attributes
.filter(attr => attr.type === 'Attribute' || attr.type === 'Spread') .filter(attr => attr.type === 'Attribute' || attr.type === 'Spread')
.forEach(attr => { .forEach(attr => {
if (attr.type === 'Attribute') { const condition = attr.dependencies.size > 0
const { dynamic, value, dependencies } = mungeAttribute(attr, block); ? [...attr.dependencies].map(d => `changed.${d}`).join(' || ')
: null;
if (attr.isSpread) {
const { snippet, dependencies } = attr.expression;
const snippet = `{ ${quoteIfNecessary(attr.name)}: ${value} }`;
initialProps.push(snippet); initialProps.push(snippet);
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
updates.push(condition ? `${condition} && ${snippet}` : snippet); updates.push(condition ? `${condition} && ${snippet}` : snippet);
} } else {
const snippet = `{ ${quoteIfNecessary(attr.name)}: ${attr.getValue()} }`;
else {
block.contextualise(attr.expression); // TODO gah
const { snippet, dependencies } = attr.metadata;
initialProps.push(snippet); initialProps.push(snippet);
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
updates.push(condition ? `${condition} && ${snippet}` : snippet); updates.push(condition ? `${condition} && ${snippet}` : snippet);
} }
}); });
@ -503,90 +575,44 @@ export default class Element extends Node {
`); `);
} }
addEventHandlers(block: Block, allUsedContexts) { addEventHandlers(block: Block) {
const { generator } = this; const { compiler } = this;
let eventHandlerUsesComponent = false;
this.attributes.filter((a: EventHandler) => a.type === 'EventHandler').forEach((attribute: EventHandler) => {
const isCustomEvent = generator.events.has(attribute.name);
const shouldHoist = !isCustomEvent && this.hasAncestor('EachBlock');
const context = shouldHoist ? null : this.var;
const usedContexts: string[] = [];
if (attribute.expression) {
generator.addSourcemapLocations(attribute.expression);
const flattened = flattenReference(attribute.expression.callee);
if (!validCalleeObjects.has(flattened.name)) {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
if (flattened.name[0] === '$' && !generator.methods.has(flattened.name)) {
generator.code.overwrite(
attribute.expression.start,
attribute.expression.start + 1,
`${block.alias('component')}.store.`
);
} else {
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
}
if (shouldHoist) eventHandlerUsesComponent = true; // this feels a bit hacky but it works!
}
attribute.expression.arguments.forEach((arg: Node) => { this.handlers.forEach(handler => {
const { contexts } = block.contextualise(arg, context, true); const isCustomEvent = compiler.events.has(handler.name);
contexts.forEach(context => { if (handler.callee) {
if (!~usedContexts.indexOf(context)) usedContexts.push(context); handler.render(this.compiler, block, handler.shouldHoist);
allUsedContexts.add(context);
});
});
} }
const ctx = context || 'this'; const target = handler.shouldHoist ? 'this' : this.var;
const declarations = usedContexts
.map(name => {
if (name === 'state') {
if (shouldHoist) eventHandlerUsesComponent = true;
return `var state = ${block.alias('component')}.get();`;
}
const contextType = block.contextTypes.get(name);
if (contextType === 'each') {
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
const contextName = block.contexts.get(name);
return `var ${listName} = ${ctx}._svelte.${listName}, ${indexName} = ${ctx}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
}
})
.filter(Boolean);
// get a name for the event handler that is globally unique // get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise // if hoisted, locally unique otherwise
const handlerName = (shouldHoist ? generator : block).getUniqueName( const handlerName = (handler.shouldHoist ? compiler : block).getUniqueName(
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler` `${handler.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
); );
const component = block.alias('component'); // can't use #component, might be hoisted
// create the handler body // create the handler body
const handlerBody = deindent` const handlerBody = deindent`
${eventHandlerUsesComponent && ${handler.shouldHoist && (
`var ${block.alias('component')} = ${ctx}._svelte.component;`} handler.usesComponent || handler.usesContext
${declarations} ? `const { ${[handler.usesComponent && 'component', handler.usesContext && 'ctx'].filter(Boolean).join(', ')} } = ${target}._svelte;`
${attribute.expression ? : null
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` : )}
`${block.alias('component')}.fire("${attribute.name}", event);`}
${handler.snippet ?
handler.snippet :
`${component}.fire("${handler.name}", event);`}
`; `;
if (isCustomEvent) { if (isCustomEvent) {
block.addVariable(handlerName); block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent` block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${attribute.name}.call(#component, ${this.var}, function(event) { ${handlerName} = %events-${handler.name}.call(${component}, ${this.var}, function(event) {
${handlerBody} ${handlerBody}
}); });
`); `);
@ -595,61 +621,52 @@ export default class Element extends Node {
${handlerName}.destroy(); ${handlerName}.destroy();
`); `);
} else { } else {
const handler = deindent` const handlerFunction = deindent`
function ${handlerName}(event) { function ${handlerName}(event) {
${handlerBody} ${handlerBody}
} }
`; `;
if (shouldHoist) { if (handler.shouldHoist) {
generator.blocks.push(handler); compiler.target.blocks.push(handlerFunction);
} else { } else {
block.builders.init.addBlock(handler); block.builders.init.addBlock(handlerFunction);
} }
block.builders.hydrate.addLine( block.builders.hydrate.addLine(
`@addListener(${this.var}, "${attribute.name}", ${handlerName});` `@addListener(${this.var}, "${handler.name}", ${handlerName});`
); );
block.builders.destroy.addLine( block.builders.destroy.addLine(
`@removeListener(${this.var}, "${attribute.name}", ${handlerName});` `@removeListener(${this.var}, "${handler.name}", ${handlerName});`
); );
} }
}); });
return eventHandlerUsesComponent;
} }
addRefs(block: Block) { addRef(block: Block) {
// TODO it should surely be an error to have more than one ref const ref = `#component.refs.${this.ref}`;
this.attributes.filter((a: Ref) => a.type === 'Ref').forEach((attribute: Ref) => {
const ref = `#component.refs.${attribute.name}`;
block.builders.mount.addLine(
`${ref} = ${this.var};`
);
block.builders.destroy.addLine( block.builders.mount.addLine(
`if (${ref} === ${this.var}) ${ref} = null;` `${ref} = ${this.var};`
); );
this.generator.usesRefs = true; // so component.refs object is created block.builders.destroy.addLine(
}); `if (${ref} === ${this.var}) ${ref} = null;`
);
} }
addTransitions( addTransitions(
block: Block block: Block
) { ) {
const intro = this.attributes.find((a: Transition) => a.type === 'Transition' && a.intro); const { intro, outro } = this;
const outro = this.attributes.find((a: Transition) => a.type === 'Transition' && a.outro);
if (!intro && !outro) return; if (!intro && !outro) return;
if (intro === outro) { if (intro === outro) {
block.contextualise(intro.expression); // TODO remove all these
const name = block.getUniqueName(`${this.var}_transition`); const name = block.getUniqueName(`${this.var}_transition`);
const snippet = intro.expression const snippet = intro.expression
? intro.metadata.snippet ? intro.expression.snippet
: '{}'; : '{}';
block.addVariable(name); block.addVariable(name);
@ -677,11 +694,9 @@ export default class Element extends Node {
const outroName = outro && block.getUniqueName(`${this.var}_outro`); const outroName = outro && block.getUniqueName(`${this.var}_outro`);
if (intro) { if (intro) {
block.contextualise(intro.expression);
block.addVariable(introName); block.addVariable(introName);
const snippet = intro.expression const snippet = intro.expression
? intro.metadata.snippet ? intro.expression.snippet
: '{}'; : '{}';
const fn = `%transitions-${intro.name}`; // TODO add built-in transitions? const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?
@ -704,11 +719,9 @@ export default class Element extends Node {
} }
if (outro) { if (outro) {
block.contextualise(outro.expression);
block.addVariable(outroName); block.addVariable(outroName);
const snippet = outro.expression const snippet = outro.expression
? outro.metadata.snippet ? outro.expression.snippet
: '{}'; : '{}';
const fn = `%transitions-${outro.name}`; const fn = `%transitions-${outro.name}`;
@ -727,31 +740,29 @@ export default class Element extends Node {
} }
addActions(block: Block) { addActions(block: Block) {
this.attributes.filter((a: Action) => a.type === 'Action').forEach((attribute:Action) => { this.actions.forEach(action => {
const { expression } = attribute; const { expression } = action;
let snippet, dependencies; let snippet, dependencies;
if (expression) { if (expression) {
this.generator.addSourcemapLocations(expression); snippet = action.expression.snippet;
block.contextualise(expression); dependencies = action.expression.dependencies;
snippet = attribute.metadata.snippet;
dependencies = attribute.metadata.dependencies;
} }
const name = block.getUniqueName( const name = block.getUniqueName(
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action` `${action.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action`
); );
block.addVariable(name); block.addVariable(name);
const fn = `%actions-${attribute.name}`; const fn = `%actions-${action.name}`;
block.builders.hydrate.addLine( block.builders.hydrate.addLine(
`${name} = ${fn}.call(#component, ${this.var}${snippet ? `, ${snippet}` : ''}) || {};` `${name} = ${fn}.call(#component, ${this.var}${snippet ? `, ${snippet}` : ''}) || {};`
); );
if (dependencies && dependencies.length) { if (dependencies && dependencies.size > 0) {
let conditional = `typeof ${name}.update === 'function' && `; let conditional = `typeof ${name}.update === 'function' && `;
const deps = dependencies.map(dependency => `changed.${dependency}`).join(' || '); const deps = [...dependencies].map(dependency => `changed.${dependency}`).join(' || ');
conditional += dependencies.length > 1 ? `(${deps})` : deps; conditional += dependencies.size > 1 ? `(${deps})` : deps;
block.builders.update.addConditional( block.builders.update.addConditional(
conditional, conditional,
@ -772,11 +783,11 @@ export default class Element extends Node {
if (!attribute) return null; if (!attribute) return null;
if (attribute.value === true) return true; if (attribute.isTrue) return true;
if (attribute.value.length === 0) return ''; if (attribute.chunks.length === 0) return '';
if (attribute.value.length === 1 && attribute.value[0].type === 'Text') { if (attribute.chunks.length === 1 && attribute.chunks[0].type === 'Text') {
return attribute.value[0].data; return attribute.chunks[0].data;
} }
return null; return null;
@ -797,29 +808,115 @@ export default class Element extends Node {
addCssClass() { addCssClass() {
const classAttribute = this.attributes.find(a => a.name === 'class'); const classAttribute = this.attributes.find(a => a.name === 'class');
if (classAttribute && classAttribute.value !== true) { if (classAttribute && !classAttribute.isTrue) {
if (classAttribute.value.length === 1 && classAttribute.value[0].type === 'Text') { if (classAttribute.chunks.length === 1 && classAttribute.chunks[0].type === 'Text') {
classAttribute.value[0].data += ` ${this.generator.stylesheet.id}`; (<Text>classAttribute.chunks[0]).data += ` ${this.compiler.stylesheet.id}`;
} else { } else {
(<Node[]>classAttribute.value).push( (<Node[]>classAttribute.chunks).push(
new Node({ type: 'Text', data: ` ${this.generator.stylesheet.id}` }) new Text(this.compiler, this, this.scope, {
type: 'Text',
data: ` ${this.compiler.stylesheet.id}`
})
// new Text({ type: 'Text', data: ` ${this.compiler.stylesheet.id}` })
); );
} }
} else { } else {
this.attributes.push( this.attributes.push(
new Attribute({ new Attribute(this.compiler, this, this.scope, {
generator: this.generator, type: 'Attribute',
name: 'class', name: 'class',
value: [new Node({ type: 'Text', data: `${this.generator.stylesheet.id}` })], value: [{ type: 'Text', data: `${this.compiler.stylesheet.id}` }]
parent: this,
}) })
); );
} }
} }
ssr() {
const { compiler } = this;
let openingTag = `<${this.name}`;
let textareaContents; // awkward special case
const slot = this.getStaticAttributeValue('slot');
if (slot && this.hasAncestor('Component')) {
const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot');
const slotName = slot.chunks[0].data;
const appendTarget = compiler.target.appendTargets[compiler.target.appendTargets.length - 1];
appendTarget.slotStack.push(slotName);
appendTarget.slots[slotName] = '';
}
if (this.attributes.find(attr => attr.isSpread)) {
// TODO dry this out
const args = [];
this.attributes.forEach(attribute => {
if (attribute.isSpread) {
args.push(attribute.expression.snippet);
} else {
if (attribute.name === 'value' && this.name === 'textarea') {
textareaContents = attribute.stringifyForSsr();
} else if (attribute.isTrue) {
args.push(`{ ${quoteIfNecessary(attribute.name)}: true }`);
} else if (
booleanAttributes.has(attribute.name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
args.push(`{ ${quoteIfNecessary(attribute.name)}: ${attribute.chunks[0].snippet} }`);
} else {
args.push(`{ ${quoteIfNecessary(attribute.name)}: \`${attribute.stringifyForSsr()}\` }`);
}
}
});
openingTag += "${@spread([" + args.join(', ') + "])}";
} else {
this.attributes.forEach((attribute: Node) => {
if (attribute.type !== 'Attribute') return;
if (attribute.name === 'value' && this.name === 'textarea') {
textareaContents = attribute.stringifyForSsr();
} else if (attribute.isTrue) {
openingTag += ` ${attribute.name}`;
} else if (
booleanAttributes.has(attribute.name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
openingTag += '${' + attribute.chunks[0].snippet + ' ? " ' + attribute.name + '" : "" }';
} else {
openingTag += ` ${attribute.name}="${attribute.stringifyForSsr()}"`;
}
});
}
if (this._cssRefAttribute) {
openingTag += ` svelte-ref-${this._cssRefAttribute}`;
}
openingTag += '>';
compiler.target.append(openingTag);
if (this.name === 'textarea' && textareaContents !== undefined) {
compiler.target.append(textareaContents);
} else {
this.children.forEach((child: Node) => {
child.ssr();
});
}
if (!isVoidElementName(this.name)) {
compiler.target.append(`</${this.name}>`);
}
}
} }
function getRenderStatement( function getRenderStatement(
generator: DomGenerator, compiler: Compiler,
namespace: string, namespace: string,
name: string name: string
) { ) {
@ -835,7 +932,7 @@ function getRenderStatement(
} }
function getClaimStatement( function getClaimStatement(
generator: DomGenerator, compiler: Compiler,
namespace: string, namespace: string,
nodes: string, nodes: string,
node: Node node: Node
@ -852,7 +949,7 @@ function getClaimStatement(
: `{}`}, ${namespace === namespaces.svg ? true : false})`; : `{}`}, ${namespace === namespaces.svg ? true : false})`;
} }
function stringifyAttributeValue(value: Node | true) { function stringifyAttributeValue(value: Node[] | true) {
if (value === true) return ''; if (value === true) return '';
if (value.length === 0) return `=""`; if (value.length === 0) return `=""`;

@ -0,0 +1,13 @@
import Node from './shared/Node';
import Block from '../dom/Block';
import mapChildren from './shared/mapChildren';
export default class ElseBlock extends Node {
type: 'ElseBlock';
children: Node[];
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, this, scope, info.children);
}
}

@ -0,0 +1,84 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import addToSet from '../../utils/addToSet';
import flattenReference from '../../utils/flattenReference';
import validCalleeObjects from '../../utils/validCalleeObjects';
export default class EventHandler extends Node {
name: string;
dependencies: Set<string>;
expression: Node;
callee: any; // TODO
usesComponent: boolean;
usesContext: boolean;
isCustomEvent: boolean;
shouldHoist: boolean;
insertionPoint: number;
args: Expression[];
snippet: string;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.name = info.name;
this.dependencies = new Set();
if (info.expression) {
this.callee = flattenReference(info.expression.callee);
this.insertionPoint = info.expression.start;
this.usesComponent = !validCalleeObjects.has(this.callee.name);
this.usesContext = false;
this.args = info.expression.arguments.map(param => {
const expression = new Expression(compiler, this, scope, param);
addToSet(this.dependencies, expression.dependencies);
if (expression.usesContext) this.usesContext = true;
return expression;
});
this.snippet = `[✂${info.expression.start}-${info.expression.end}✂];`;
} else {
this.callee = null;
this.insertionPoint = null;
this.args = null;
this.usesComponent = true;
this.usesContext = false;
this.snippet = null; // TODO handle shorthand events here?
}
this.isCustomEvent = compiler.events.has(this.name);
this.shouldHoist = !this.isCustomEvent && parent.hasAncestor('EachBlock');
}
render(compiler, block, hoisted) { // TODO hoist more event handlers
if (this.insertionPoint === null) return; // TODO handle shorthand events here?
if (!validCalleeObjects.has(this.callee.name)) {
const component = hoisted ? `component` : block.alias(`component`);
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
if (this.callee.name[0] === '$' && !compiler.methods.has(this.callee.name)) {
compiler.code.overwrite(
this.insertionPoint,
this.insertionPoint + 1,
`${component}.store.`
);
} else {
compiler.code.prependRight(
this.insertionPoint,
`${component}.`
);
}
}
this.args.forEach(arg => {
arg.overwriteThis(this.parent.var);
});
}
}

@ -1,28 +1,35 @@
import Node from './shared/Node'; import Node from './shared/Node';
import { DomGenerator } from '../dom/index'; import Compiler from '../Compiler';
import mapChildren from './shared/mapChildren';
import Block from '../dom/Block'; import Block from '../dom/Block';
import TemplateScope from './shared/TemplateScope';
export default class Fragment extends Node { export default class Fragment extends Node {
block: Block; block: Block;
children: Node[]; 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() { init() {
this.block = new Block({ this.block = new Block({
generator: this.generator, compiler: this.compiler,
name: '@create_main_fragment', name: '@create_main_fragment',
key: null, key: null,
contexts: new Map(),
indexes: new Map(),
changeableIndexes: new Map(),
indexNames: new Map(), indexNames: new Map(),
listNames: new Map(), listNames: new Map(),
dependencies: new Set(), dependencies: new Set(),
}); });
this.generator.blocks.push(this.block); this.compiler.target.blocks.push(this.block);
this.initChildren(this.block, true, null); this.initChildren(this.block, true, null);
this.block.hasUpdateMethod = true; this.block.hasUpdateMethod = true;

@ -3,10 +3,18 @@ import { stringify } from '../../utils/stringify';
import Node from './shared/Node'; import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import Attribute from './Attribute'; import Attribute from './Attribute';
import mapChildren from './shared/mapChildren';
export default class Head extends Node { export default class Head extends Node {
type: 'Head'; type: 'Head';
attributes: Attribute[]; children: any[]; // TODO
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children.filter(child => {
return (child.type !== 'Text' || /\S/.test(child.data));
}));
}
init( init(
block: Block, block: Block,
@ -21,12 +29,20 @@ export default class Head extends Node {
parentNode: string, parentNode: string,
parentNodes: string parentNodes: string
) { ) {
const { generator } = this;
this.var = 'document.head'; this.var = 'document.head';
this.children.forEach((child: Node) => { this.children.forEach((child: Node) => {
child.build(block, 'document.head', null); child.build(block, 'document.head', null);
}); });
} }
ssr() {
this.compiler.target.append('${(__result.head += `');
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append('`, "")}');
}
} }

@ -1,9 +1,11 @@
import deindent from '../../utils/deindent'; import deindent from '../../utils/deindent';
import Node from './shared/Node'; import Node from './shared/Node';
import ElseBlock from './ElseBlock'; import ElseBlock from './ElseBlock';
import { DomGenerator } from '../dom/index'; import Compiler from '../Compiler';
import Block from '../dom/Block'; import Block from '../dom/Block';
import createDebuggingComment from '../../utils/createDebuggingComment'; import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression';
import mapChildren from './shared/mapChildren';
function isElseIf(node: ElseBlock) { function isElseIf(node: ElseBlock) {
return ( return (
@ -17,16 +19,29 @@ function isElseBranch(branch) {
export default class IfBlock extends Node { export default class IfBlock extends Node {
type: 'IfBlock'; type: 'IfBlock';
expression: Expression;
children: any[];
else: ElseBlock; else: ElseBlock;
block: Block; 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( init(
block: Block, block: Block,
stripWhitespace: boolean, stripWhitespace: boolean,
nextSibling: Node nextSibling: Node
) { ) {
const { generator } = this; const { compiler } = this;
this.cannotUseInnerHTML(); this.cannotUseInnerHTML();
@ -38,11 +53,11 @@ export default class IfBlock extends Node {
function attachBlocks(node: IfBlock) { function attachBlocks(node: IfBlock) {
node.var = block.getUniqueName(`if_block`); node.var = block.getUniqueName(`if_block`);
block.addDependencies(node.metadata.dependencies); block.addDependencies(node.expression.dependencies);
node.block = block.child({ node.block = block.child({
comment: createDebuggingComment(node, generator), comment: createDebuggingComment(node, compiler),
name: generator.getUniqueName(`create_if_block`), name: compiler.getUniqueName(`create_if_block`),
}); });
blocks.push(node.block); blocks.push(node.block);
@ -60,8 +75,8 @@ export default class IfBlock extends Node {
attachBlocks(node.else.children[0]); attachBlocks(node.else.children[0]);
} else if (node.else) { } else if (node.else) {
node.else.block = block.child({ node.else.block = block.child({
comment: createDebuggingComment(node.else, generator), comment: createDebuggingComment(node.else, compiler),
name: generator.getUniqueName(`create_if_block`), name: compiler.getUniqueName(`create_if_block`),
}); });
blocks.push(node.else.block); blocks.push(node.else.block);
@ -86,7 +101,7 @@ export default class IfBlock extends Node {
block.hasOutroMethod = hasOutros; block.hasOutroMethod = hasOutros;
}); });
generator.blocks.push(...blocks); compiler.target.blocks.push(...blocks);
} }
build( build(
@ -147,12 +162,12 @@ export default class IfBlock extends Node {
dynamic, dynamic,
{ name, anchor, hasElse, if_name } { name, anchor, hasElse, if_name }
) { ) {
const select_block_type = this.generator.getUniqueName(`select_block_type`); const select_block_type = this.compiler.getUniqueName(`select_block_type`);
const current_block_type = block.getUniqueName(`current_block_type`); const current_block_type = block.getUniqueName(`current_block_type`);
const current_block_type_and = hasElse ? '' : `${current_block_type} && `; const current_block_type_and = hasElse ? '' : `${current_block_type} && `;
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
function ${select_block_type}(state) { function ${select_block_type}(ctx) {
${branches ${branches
.map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block};`) .map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block};`)
.join('\n')} .join('\n')}
@ -160,8 +175,8 @@ export default class IfBlock extends Node {
`); `);
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
var ${current_block_type} = ${select_block_type}(state); var ${current_block_type} = ${select_block_type}(ctx);
var ${name} = ${current_block_type_and}${current_block_type}(#component, state); var ${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
`); `);
const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm'; const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm';
@ -185,22 +200,22 @@ export default class IfBlock extends Node {
${name}.u(); ${name}.u();
${name}.d(); ${name}.d();
}`} }`}
${name} = ${current_block_type_and}${current_block_type}(#component, state); ${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
${if_name}${name}.c(); ${if_name}${name}.c();
${if_name}${name}.${mountOrIntro}(${updateMountNode}, ${anchor}); ${if_name}${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
`; `;
if (dynamic) { if (dynamic) {
block.builders.update.addBlock(deindent` block.builders.update.addBlock(deindent`
if (${current_block_type} === (${current_block_type} = ${select_block_type}(state)) && ${name}) { if (${current_block_type} === (${current_block_type} = ${select_block_type}(ctx)) && ${name}) {
${name}.p(changed, state); ${name}.p(changed, ctx);
} else { } else {
${changeBlock} ${changeBlock}
} }
`); `);
} else { } else {
block.builders.update.addBlock(deindent` block.builders.update.addBlock(deindent`
if (${current_block_type} !== (${current_block_type} = ${select_block_type}(state))) { if (${current_block_type} !== (${current_block_type} = ${select_block_type}(ctx))) {
${changeBlock} ${changeBlock}
} }
`); `);
@ -241,7 +256,7 @@ export default class IfBlock extends Node {
var ${if_blocks} = []; var ${if_blocks} = [];
function ${select_block_type}(state) { function ${select_block_type}(ctx) {
${branches ${branches
.map(({ condition, block }, i) => `${condition ? `if (${condition}) ` : ''}return ${block ? i : -1};`) .map(({ condition, block }, i) => `${condition ? `if (${condition}) ` : ''}return ${block ? i : -1};`)
.join('\n')} .join('\n')}
@ -250,13 +265,13 @@ export default class IfBlock extends Node {
if (hasElse) { if (hasElse) {
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
${current_block_type_index} = ${select_block_type}(state); ${current_block_type_index} = ${select_block_type}(ctx);
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, state); ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
`); `);
} else { } else {
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
if (~(${current_block_type_index} = ${select_block_type}(state))) { if (~(${current_block_type_index} = ${select_block_type}(ctx))) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, state); ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
} }
`); `);
} }
@ -282,7 +297,7 @@ export default class IfBlock extends Node {
const createNewBlock = deindent` const createNewBlock = deindent`
${name} = ${if_blocks}[${current_block_type_index}]; ${name} = ${if_blocks}[${current_block_type_index}];
if (!${name}) { if (!${name}) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, state); ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
${name}.c(); ${name}.c();
} }
${name}.${mountOrIntro}(${updateMountNode}, ${anchor}); ${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
@ -309,9 +324,9 @@ export default class IfBlock extends Node {
if (dynamic) { if (dynamic) {
block.builders.update.addBlock(deindent` block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_type_index}; var ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(state); ${current_block_type_index} = ${select_block_type}(ctx);
if (${current_block_type_index} === ${previous_block_index}) { if (${current_block_type_index} === ${previous_block_index}) {
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, state); ${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, ctx);
} else { } else {
${changeBlock} ${changeBlock}
} }
@ -319,7 +334,7 @@ export default class IfBlock extends Node {
} else { } else {
block.builders.update.addBlock(deindent` block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_type_index}; var ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(state); ${current_block_type_index} = ${select_block_type}(ctx);
if (${current_block_type_index} !== ${previous_block_index}) { if (${current_block_type_index} !== ${previous_block_index}) {
${changeBlock} ${changeBlock}
} }
@ -343,7 +358,7 @@ export default class IfBlock extends Node {
{ name, anchor, if_name } { name, anchor, if_name }
) { ) {
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
var ${name} = (${branch.condition}) && ${branch.block}(#component, state); var ${name} = (${branch.condition}) && ${branch.block}(#component, ctx);
`); `);
const mountOrIntro = branch.hasIntroMethod ? 'i' : 'm'; const mountOrIntro = branch.hasIntroMethod ? 'i' : 'm';
@ -360,9 +375,9 @@ export default class IfBlock extends Node {
? branch.hasIntroMethod ? branch.hasIntroMethod
? deindent` ? deindent`
if (${name}) { if (${name}) {
${name}.p(changed, state); ${name}.p(changed, ctx);
} else { } else {
${name} = ${branch.block}(#component, state); ${name} = ${branch.block}(#component, ctx);
if (${name}) ${name}.c(); if (${name}) ${name}.c();
} }
@ -370,9 +385,9 @@ export default class IfBlock extends Node {
` `
: deindent` : deindent`
if (${name}) { if (${name}) {
${name}.p(changed, state); ${name}.p(changed, ctx);
} else { } else {
${name} = ${branch.block}(#component, state); ${name} = ${branch.block}(#component, ctx);
${name}.c(); ${name}.c();
${name}.m(${updateMountNode}, ${anchor}); ${name}.m(${updateMountNode}, ${anchor});
} }
@ -380,14 +395,14 @@ export default class IfBlock extends Node {
: branch.hasIntroMethod : branch.hasIntroMethod
? deindent` ? deindent`
if (!${name}) { if (!${name}) {
${name} = ${branch.block}(#component, state); ${name} = ${branch.block}(#component, ctx);
${name}.c(); ${name}.c();
} }
${name}.i(${updateMountNode}, ${anchor}); ${name}.i(${updateMountNode}, ${anchor});
` `
: deindent` : deindent`
if (!${name}) { if (!${name}) {
${name} = ${branch.block}(#component, state); ${name} = ${branch.block}(#component, ctx);
${name}.c(); ${name}.c();
${name}.m(${updateMountNode}, ${anchor}); ${name}.m(${updateMountNode}, ${anchor});
} }
@ -426,13 +441,11 @@ export default class IfBlock extends Node {
block: Block, block: Block,
parentNode: string, parentNode: string,
parentNodes: string, parentNodes: string,
node: Node node: IfBlock
) { ) {
block.contextualise(node.expression); // TODO remove
const branches = [ const branches = [
{ {
condition: node.metadata.snippet, condition: node.expression.snippet,
block: node.block.name, block: node.block.name,
hasUpdateMethod: node.block.hasUpdateMethod, hasUpdateMethod: node.block.hasUpdateMethod,
hasIntroMethod: node.block.hasIntroMethod, hasIntroMethod: node.block.hasIntroMethod,
@ -463,6 +476,27 @@ export default class IfBlock extends Node {
return branches; 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) { visitChildren(block: Block, node: Node) {
node.children.forEach((child: Node) => { node.children.forEach((child: Node) => {
child.build(node.block, null, 'nodes'); child.build(node.block, null, 'nodes');

@ -3,12 +3,6 @@ import Tag from './shared/Tag';
import Block from '../dom/Block'; import Block from '../dom/Block';
export default class MustacheTag extends Tag { export default class MustacheTag extends Tag {
init(block: Block) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName('text');
block.addDependencies(this.metadata.dependencies);
}
build( build(
block: Block, block: Block,
parentNode: string, parentNode: string,
@ -30,4 +24,14 @@ export default class MustacheTag extends Tag {
remount(name: string) { remount(name: string) {
return `@appendNode(${this.var}, ${name}._slotted.default);`; return `@appendNode(${this.var}, ${name}._slotted.default);`;
} }
ssr() {
this.compiler.target.append(
this.parent &&
this.parent.type === 'Element' &&
this.parent.name === 'style'
? '${' + this.expression.snippet + '}'
: '${@escape(' + this.expression.snippet + ')}'
);
}
} }

@ -0,0 +1,13 @@
import Node from './shared/Node';
import Block from '../dom/Block';
import mapChildren from './shared/mapChildren';
export default class PendingBlock extends Node {
block: Block;
children: Node[];
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children);
}
}

@ -4,12 +4,6 @@ import Tag from './shared/Tag';
import Block from '../dom/Block'; import Block from '../dom/Block';
export default class RawMustacheTag extends Tag { export default class RawMustacheTag extends Tag {
init(block: Block) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName('raw');
block.addDependencies(this.metadata.dependencies);
}
build( build(
block: Block, block: Block,
parentNode: string, parentNode: string,
@ -93,4 +87,8 @@ export default class RawMustacheTag extends Tag {
remount(name: string) { remount(name: string) {
return `@appendNode(${this.var}, ${name}._slotted.default);`; return `@appendNode(${this.var}, ${name}._slotted.default);`;
} }
ssr() {
this.compiler.target.append('${' + this.expression.snippet + '}');
}
} }

@ -31,10 +31,10 @@ export default class Slot extends Element {
parentNode: string, parentNode: string,
parentNodes: string parentNodes: string
) { ) {
const { generator } = this; const { compiler } = this;
const slotName = this.getStaticAttributeValue('name') || 'default'; const slotName = this.getStaticAttributeValue('name') || 'default';
generator.slots.add(slotName); compiler.slots.add(slotName);
const content_name = block.getUniqueName(`slot_content_${slotName}`); const content_name = block.getUniqueName(`slot_content_${slotName}`);
const prop = !isValidIdentifier(slotName) ? `["${slotName}"]` : `.${slotName}`; const prop = !isValidIdentifier(slotName) ? `["${slotName}"]` : `.${slotName}`;
@ -136,18 +136,31 @@ export default class Slot extends Element {
getStaticAttributeValue(name: string) { getStaticAttributeValue(name: string) {
const attribute = this.attributes.find( const attribute = this.attributes.find(
(attr: Node) => attr.name.toLowerCase() === name attr => attr.name.toLowerCase() === name
); );
if (!attribute) return null; if (!attribute) return null;
if (attribute.value === true) return true; if (attribute.isTrue) return true;
if (attribute.value.length === 0) return ''; if (attribute.chunks.length === 0) return '';
if (attribute.value.length === 1 && attribute.value[0].type === 'Text') { if (attribute.chunks.length === 1 && attribute.chunks[0].type === 'Text') {
return attribute.value[0].data; return attribute.chunks[0].data;
} }
return null; return null;
} }
ssr() {
const name = this.attributes.find(attribute => attribute.name === 'name');
const slotName = name && name.chunks[0].data || 'default';
this.compiler.target.append(`\${options && options.slotted && options.slotted.${slotName} ? options.slotted.${slotName}() : \``);
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append(`\`}`);
}
} }

@ -1,4 +1,4 @@
import { stringify } from '../../utils/stringify'; import { escape, escapeHTML, escapeTemplate, stringify } from '../../utils/stringify';
import Node from './shared/Node'; import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
@ -33,6 +33,11 @@ export default class Text extends Node {
data: string; data: string;
shouldSkip: boolean; shouldSkip: boolean;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.data = info.data;
}
init(block: Block) { init(block: Block) {
const parentElement = this.findNearest(/(?:Element|Component)/); const parentElement = this.findNearest(/(?:Element|Component)/);
@ -62,4 +67,17 @@ export default class Text extends Node {
remount(name: string) { remount(name: string) {
return `@appendNode(${this.var}, ${name}._slotted.default);`; return `@appendNode(${this.var}, ${name}._slotted.default);`;
} }
ssr() {
let text = this.data;
if (
!this.parent ||
this.parent.type !== 'Element' ||
(this.parent.name !== 'script' && this.parent.name !== 'style')
) {
// unless this Text node is inside a <script> or <style> element, escape &,<,>
text = escapeHTML(text);
}
this.compiler.target.append(escape(escapeTemplate(text)));
}
} }

@ -0,0 +1,13 @@
import Node from './shared/Node';
import Block from '../dom/Block';
import mapChildren from './shared/mapChildren';
export default class ThenBlock extends Node {
block: Block;
children: Node[];
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children);
}
}

@ -1,9 +1,25 @@
import { stringify } from '../../utils/stringify'; import { stringify } from '../../utils/stringify';
import getExpressionPrecedence from '../../utils/getExpressionPrecedence';
import Node from './shared/Node'; import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import mapChildren from './shared/mapChildren';
export default class Title extends Node { export default class Title extends Node {
type: 'Title';
children: any[]; // TODO
shouldCache: boolean;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children);
this.shouldCache = info.children.length === 1
? (
info.children[0].type !== 'Identifier' ||
scope.names.has(info.children[0].name)
)
: true;
}
build( build(
block: Block, block: Block,
parentNode: string, parentNode: string,
@ -15,27 +31,20 @@ export default class Title extends Node {
let value; let value;
const allDependencies = new Set(); const allDependencies = new Set();
let shouldCache;
// TODO some of this code is repeated in Tag.ts — would be good to // 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 // DRY it out if that's possible without introducing crazy indirection
if (this.children.length === 1) { if (this.children.length === 1) {
// single {{tag}} — may be a non-string // single {{tag}} — may be a non-string
const { expression } = this.children[0]; const { expression } = this.children[0];
const { indexes } = block.contextualise(expression); const { dependencies, snippet } = this.children[0].expression;
const { dependencies, snippet } = this.children[0].metadata;
value = snippet; value = snippet;
dependencies.forEach(d => { dependencies.forEach(d => {
allDependencies.add(d); allDependencies.add(d);
}); });
shouldCache = (
expression.type !== 'Identifier' ||
block.contexts.has(expression.name)
);
} else { } else {
// '{{foo}} {{bar}}' — treat as string concatenation // '{foo} {bar}' — treat as string concatenation
value = value =
(this.children[0].type === 'Text' ? '' : `"" + `) + (this.children[0].type === 'Text' ? '' : `"" + `) +
this.children this.children
@ -43,34 +52,31 @@ export default class Title extends Node {
if (chunk.type === 'Text') { if (chunk.type === 'Text') {
return stringify(chunk.data); return stringify(chunk.data);
} else { } else {
const { indexes } = block.contextualise(chunk.expression); const { dependencies, snippet } = chunk.expression;
const { dependencies, snippet } = chunk.metadata;
dependencies.forEach(d => { dependencies.forEach(d => {
allDependencies.add(d); allDependencies.add(d);
}); });
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet; return chunk.expression.getPrecedence() <= 13 ? `(${snippet})` : snippet;
} }
}) })
.join(' + '); .join(' + ');
shouldCache = true;
} }
const last = shouldCache && block.getUniqueName( const last = this.shouldCache && block.getUniqueName(
`title_value` `title_value`
); );
if (shouldCache) block.addVariable(last); if (this.shouldCache) block.addVariable(last);
let updater; let updater;
const init = shouldCache ? `${last} = ${value}` : value; const init = this.shouldCache ? `${last} = ${value}` : value;
block.builders.init.addLine( block.builders.init.addLine(
`document.title = ${init};` `document.title = ${init};`
); );
updater = `document.title = ${shouldCache ? last : value};`; updater = `document.title = ${this.shouldCache ? last : value};`;
if (allDependencies.size) { if (allDependencies.size) {
const dependencies = Array.from(allDependencies); const dependencies = Array.from(allDependencies);
@ -81,7 +87,7 @@ export default class Title extends Node {
const updateCachedValue = `${last} !== (${last} = ${value})`; const updateCachedValue = `${last} !== (${last} = ${value})`;
const condition = shouldCache ? const condition = this.shouldCache ?
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) : ( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
changedCheck; changedCheck;
@ -95,4 +101,14 @@ export default class Title extends Node {
block.builders.hydrate.addLine(`document.title = ${value};`); block.builders.hydrate.addLine(`document.title = ${value};`);
} }
} }
ssr() {
this.compiler.target.append(`<title>`);
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append(`</title>`);
}
} }

@ -0,0 +1,18 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
export default class Transition extends Node {
type: 'Transition';
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;
}
}

@ -7,7 +7,8 @@ import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames'; import reservedNames from '../../utils/reservedNames';
import Node from './shared/Node'; import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import Attribute from './Attribute'; import Binding from './Binding';
import EventHandler from './EventHandler';
const associatedEvents = { const associatedEvents = {
innerWidth: 'resize', innerWidth: 'resize',
@ -34,99 +35,100 @@ const readonly = new Set([
export default class Window extends Node { export default class Window extends Node {
type: 'Window'; type: 'Window';
attributes: Attribute[]; handlers: EventHandler[];
bindings: Binding[];
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.handlers = [];
this.bindings = [];
info.attributes.forEach(node => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(compiler, this, scope, node));
} else if (node.type === 'Binding') {
this.bindings.push(new Binding(compiler, this, scope, node));
}
});
}
build( build(
block: Block, block: Block,
parentNode: string, parentNode: string,
parentNodes: string parentNodes: string
) { ) {
const { generator } = this; const { compiler } = this;
const events = {}; const events = {};
const bindings: Record<string, string> = {}; const bindings: Record<string, string> = {};
this.attributes.forEach((attribute: Node) => { this.handlers.forEach(handler => {
if (attribute.type === 'EventHandler') { // TODO verify that it's a valid callee (i.e. built-in or declared method)
// TODO verify that it's a valid callee (i.e. built-in or declared method) compiler.addSourcemapLocations(handler.expression);
generator.addSourcemapLocations(attribute.expression);
const isCustomEvent = generator.events.has(attribute.name); const isCustomEvent = compiler.events.has(handler.name);
let usesState = false; let usesState = handler.dependencies.size > 0;
attribute.expression.arguments.forEach((arg: Node) => { handler.render(compiler, block, false); // TODO hoist?
block.contextualise(arg, null, true);
const { dependencies } = arg.metadata;
if (dependencies.length) usesState = true;
});
const flattened = flattenReference(attribute.expression.callee);
if (flattened.name !== 'event' && flattened.name !== 'this') {
// allow event.stopPropagation(), this.select() etc
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
}
const handlerName = block.getUniqueName(`onwindow${attribute.name}`); const handlerName = block.getUniqueName(`onwindow${handler.name}`);
const handlerBody = deindent` const handlerBody = deindent`
${usesState && `var state = #component.get();`} ${usesState && `var ctx = #component.get();`}
[${attribute.expression.start}-${attribute.expression.end}]; ${handler.snippet};
`; `;
if (isCustomEvent) { if (isCustomEvent) {
// TODO dry this out // TODO dry this out
block.addVariable(handlerName); block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${handler.name}.call(#component, window, function(event) {
${handlerBody}
});
`);
block.builders.destroy.addLine(deindent`
${handlerName}.destroy();
`);
} else {
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
${handlerBody}
}
window.addEventListener("${handler.name}", ${handlerName});
`);
block.builders.hydrate.addBlock(deindent` block.builders.destroy.addBlock(deindent`
${handlerName} = %events-${attribute.name}.call(#component, window, function(event) { window.removeEventListener("${handler.name}", ${handlerName});
${handlerBody} `);
});
`);
block.builders.destroy.addLine(deindent`
${handlerName}.destroy();
`);
} else {
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
${handlerBody}
}
window.addEventListener("${attribute.name}", ${handlerName});
`);
block.builders.destroy.addBlock(deindent`
window.removeEventListener("${attribute.name}", ${handlerName});
`);
}
} }
});
if (attribute.type === 'Binding') { this.bindings.forEach(binding => {
// in dev mode, throw if read-only values are written to // in dev mode, throw if read-only values are written to
if (readonly.has(attribute.name)) { if (readonly.has(binding.name)) {
generator.readonly.add(attribute.value.name); compiler.target.readonly.add(binding.value.node.name);
} }
bindings[attribute.name] = attribute.value.name; bindings[binding.name] = binding.value.node.name;
// bind:online is a special case, we need to listen for two separate events // bind:online is a special case, we need to listen for two separate events
if (attribute.name === 'online') return; if (binding.name === 'online') return;
const associatedEvent = associatedEvents[attribute.name]; const associatedEvent = associatedEvents[binding.name];
const property = properties[attribute.name] || attribute.name; const property = properties[binding.name] || binding.name;
if (!events[associatedEvent]) events[associatedEvent] = []; if (!events[associatedEvent]) events[associatedEvent] = [];
events[associatedEvent].push( events[associatedEvent].push(
`${attribute.value.name}: this.${property}` `${binding.value.node.name}: this.${property}`
); );
// add initial value // add initial value
generator.metaBindings.push( compiler.target.metaBindings.push(
`this._state.${attribute.value.name} = window.${property};` `this._state.${binding.value.node.name} = window.${property};`
); );
}
}); });
const lock = block.getUniqueName(`window_updating`); const lock = block.getUniqueName(`window_updating`);
@ -149,13 +151,13 @@ export default class Window extends Node {
if (${lock}) return; if (${lock}) return;
${lock} = true; ${lock} = true;
`} `}
${generator.options.dev && `component._updatingReadonlyProperty = true;`} ${compiler.options.dev && `component._updatingReadonlyProperty = true;`}
#component.set({ #component.set({
${props} ${props}
}); });
${generator.options.dev && `component._updatingReadonlyProperty = false;`} ${compiler.options.dev && `component._updatingReadonlyProperty = false;`}
${event === 'scroll' && `${lock} = false;`} ${event === 'scroll' && `${lock} = false;`}
`; `;
@ -205,7 +207,7 @@ export default class Window extends Node {
`); `);
// add initial value // add initial value
generator.metaBindings.push( compiler.target.metaBindings.push(
`this._state.${bindings.online} = navigator.onLine;` `this._state.${bindings.online} = navigator.onLine;`
); );
@ -215,4 +217,8 @@ export default class Window extends Node {
`); `);
} }
} }
ssr() {
// noop
}
} }

@ -0,0 +1,173 @@
import Compiler from '../../Compiler';
import { walk } from 'estree-walker';
import isReference from 'is-reference';
import flattenReference from '../../../utils/flattenReference';
import { createScopes } from '../../../utils/annotateWithScopes';
import { Node } from '../../../interfaces';
const binaryOperators: Record<string, number> = {
'**': 15,
'*': 14,
'/': 14,
'%': 14,
'+': 13,
'-': 13,
'<<': 12,
'>>': 12,
'>>>': 12,
'<': 11,
'<=': 11,
'>': 11,
'>=': 11,
'in': 11,
'instanceof': 11,
'==': 10,
'!=': 10,
'===': 10,
'!==': 10,
'&': 9,
'^': 8,
'|': 7
};
const logicalOperators: Record<string, number> = {
'&&': 6,
'||': 5
};
const precedence: Record<string, (node?: Node) => number> = {
Literal: () => 21,
Identifier: () => 21,
ParenthesizedExpression: () => 20,
MemberExpression: () => 19,
NewExpression: () => 19, // can be 18 (if no args) but makes no practical difference
CallExpression: () => 19,
UpdateExpression: () => 17,
UnaryExpression: () => 16,
BinaryExpression: (node: Node) => binaryOperators[node.operator],
LogicalExpression: (node: Node) => logicalOperators[node.operator],
ConditionalExpression: () => 4,
AssignmentExpression: () => 3,
YieldExpression: () => 2,
SpreadElement: () => 1,
SequenceExpression: () => 0
};
export default class Expression {
compiler: Compiler;
node: any;
snippet: string;
usesContext: boolean;
references: Set<string>;
dependencies: Set<string>;
thisReferences: Array<{ start: number, end: number }>;
constructor(compiler, parent, scope, info) {
// TODO revert to direct property access in prod?
Object.defineProperties(this, {
compiler: {
value: compiler
}
});
this.node = info;
this.thisReferences = [];
this.snippet = `[✂${info.start}-${info.end}✂]`;
this.usesContext = false;
const dependencies = new Set();
const { code, helpers } = compiler;
let { map, scope: currentScope } = createScopes(info);
const isEventHandler = parent.type === 'EventHandler';
const expression = this;
const isSynthetic = parent.isSynthetic;
walk(info, {
enter(node: any, parent: any, key: string) {
// don't manipulate shorthand props twice
if (key === 'value' && parent.shorthand) return;
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
if (map.has(node)) {
currentScope = map.get(node);
return;
}
if (node.type === 'ThisExpression') {
expression.thisReferences.push(node);
}
if (isReference(node, parent)) {
const { name } = flattenReference(node);
if (currentScope.has(name) || (name === 'event' && isEventHandler)) return;
if (compiler.helpers.has(name)) {
let object = node;
while (object.type === 'MemberExpression') object = object.object;
const alias = compiler.templateVars.get(`helpers-${name}`);
if (alias !== name) code.overwrite(object.start, object.end, alias);
return;
}
expression.usesContext = true;
if (!isSynthetic) {
// <option> value attribute could be synthetic — avoid double editing
code.prependRight(node.start, key === 'key' && parent.shorthand
? `${name}: ctx.`
: 'ctx.');
}
if (scope.names.has(name)) {
scope.dependenciesForName.get(name).forEach(dependency => {
dependencies.add(dependency);
});
} else {
dependencies.add(name);
compiler.expectedProperties.add(name);
}
if (node.type === 'MemberExpression') {
walk(node, {
enter(node) {
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
}
});
}
this.skip();
}
},
leave(node: Node, parent: Node) {
if (map.has(node)) currentScope = currentScope.parent;
}
});
this.dependencies = dependencies;
}
getPrecedence() {
return this.node.type in precedence ? precedence[this.node.type](this.node) : 0;
}
overwriteThis(name) {
this.thisReferences.forEach(ref => {
this.compiler.code.overwrite(ref.start, ref.end, name, {
storeName: true
});
});
}
}

@ -1,37 +1,41 @@
import { DomGenerator } from '../../dom/index'; import Compiler from './../../Compiler';
import Block from '../../dom/Block'; import Block from '../../dom/Block';
import { trimStart, trimEnd } from '../../../utils/trim'; import { trimStart, trimEnd } from '../../../utils/trim';
export default class Node { export default class Node {
type: string; readonly start: number;
start: number; readonly end: number;
end: number; readonly compiler: Compiler;
[key: string]: any; readonly parent: Node;
readonly type: string;
metadata?: {
dependencies: string[];
snippet: string;
};
parent: Node;
prev?: Node; prev?: Node;
next?: Node; next?: Node;
generator: DomGenerator;
canUseInnerHTML: boolean; canUseInnerHTML: boolean;
var: string; var: string;
constructor(data: Record<string, any>) { constructor(compiler: Compiler, parent, scope, info: any) {
Object.assign(this, data); 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() { cannotUseInnerHTML() {
if (this.canUseInnerHTML !== false) { if (this.canUseInnerHTML !== false) {
this.canUseInnerHTML = false; this.canUseInnerHTML = false;
if (this.parent) { if (this.parent) this.parent.cannotUseInnerHTML();
if (!this.parent.cannotUseInnerHTML) console.log(this.parent.type, this.type);
this.parent.cannotUseInnerHTML();
}
} }
} }
@ -82,7 +86,7 @@ export default class Node {
lastChild = null; lastChild = null;
cleaned.forEach((child: Node, i: number) => { cleaned.forEach((child: Node, i: number) => {
child.canUseInnerHTML = !this.generator.hydratable; child.canUseInnerHTML = !this.compiler.options.hydratable;
child.init(block, stripWhitespace, cleaned[i + 1] || nextSibling); child.init(block, stripWhitespace, cleaned[i + 1] || nextSibling);

@ -0,0 +1,56 @@
import Node from './Node';
import Expression from './Expression';
import Block from '../../dom/Block';
export default class Tag extends Node {
expression: Expression;
shouldCache: boolean;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.expression = new Expression(compiler, this, scope, info.expression);
this.shouldCache = (
info.expression.type !== 'Identifier' ||
(this.expression.dependencies.size && scope.names.has(info.expression.name))
);
}
init(block: Block) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName(this.type === 'MustacheTag' ? 'text' : 'raw');
block.addDependencies(this.expression.dependencies);
}
renameThisMethod(
block: Block,
update: ((value: string) => string)
) {
const { snippet, dependencies } = this.expression;
const value = this.shouldCache && block.getUniqueName(`${this.var}_value`);
const content = this.shouldCache ? value : snippet;
if (this.shouldCache) block.addVariable(value, snippet);
if (dependencies.size) {
const changedCheck = (
(block.hasOutroMethod ? `#outroing || ` : '') +
[...dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')
);
const updateCachedValue = `${value} !== (${value} = ${snippet})`;
const condition = this.shouldCache ?
(dependencies.size ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
changedCheck;
block.builders.update.addConditional(
condition,
update(content)
);
}
return { init: content };
}
}

@ -0,0 +1,19 @@
export default class TemplateScope {
names: Set<string>;
dependenciesForName: Map<string, string>;
constructor(parent?: TemplateScope) {
this.names = new Set(parent ? parent.names : []);
this.dependenciesForName = new Map(parent ? parent.dependenciesForName : []);
}
add(name, dependencies) {
this.names.add(name);
this.dependenciesForName.set(name, dependencies);
return this;
}
child() {
return new TemplateScope(this);
}
}

@ -0,0 +1,47 @@
import AwaitBlock from '../AwaitBlock';
import Comment from '../Comment';
import Component from '../Component';
import EachBlock from '../EachBlock';
import Element from '../Element';
import Head from '../Head';
import IfBlock from '../IfBlock';
import MustacheTag from '../MustacheTag';
import RawMustacheTag from '../RawMustacheTag';
import Slot from '../Slot';
import Text from '../Text';
import Title from '../Title';
import Window from '../Window';
import Node from './Node';
function getConstructor(type): typeof Node {
switch (type) {
case 'AwaitBlock': return AwaitBlock;
case 'Comment': return Comment;
case 'Component': return Component;
case 'EachBlock': return EachBlock;
case 'Element': return Element;
case 'Head': return Head;
case 'IfBlock': return IfBlock;
case 'MustacheTag': return MustacheTag;
case 'RawMustacheTag': return RawMustacheTag;
case 'Slot': return Slot;
case 'Text': return Text;
case 'Title': return Title;
case 'Window': return Window;
default: throw new Error(`Not implemented: ${type}`);
}
}
export default function mapChildren(compiler, parent, scope, children: any[]) {
let last = null;
return children.map(child => {
const constructor = getConstructor(child.type);
const node = new constructor(compiler, parent, scope, child);
if (last) last.next = node;
node.prev = last;
last = node;
return node;
});
}

@ -1,35 +1,23 @@
import deindent from '../../utils/deindent'; import deindent from '../../utils/deindent';
import Generator from '../Generator'; import Compiler from '../Compiler';
import Stats from '../../Stats'; import Stats from '../../Stats';
import Stylesheet from '../../css/Stylesheet'; import Stylesheet from '../../css/Stylesheet';
import Block from './Block';
import visit from './visit';
import { removeNode, removeObjectKey } from '../../utils/removeNode'; import { removeNode, removeObjectKey } from '../../utils/removeNode';
import getName from '../../utils/getName'; import getName from '../../utils/getName';
import globalWhitelist from '../../utils/globalWhitelist'; import globalWhitelist from '../../utils/globalWhitelist';
import { Parsed, Node, CompileOptions } from '../../interfaces'; import { Ast, Node, CompileOptions } from '../../interfaces';
import { AppendTarget } from './interfaces'; import { AppendTarget } from '../../interfaces';
import { stringify } from '../../utils/stringify'; import { stringify } from '../../utils/stringify';
export class SsrGenerator extends Generator { export class SsrTarget {
bindings: string[]; bindings: string[];
renderCode: string; renderCode: string;
appendTargets: AppendTarget[]; appendTargets: AppendTarget[];
constructor( constructor() {
parsed: Parsed,
source: string,
name: string,
stylesheet: Stylesheet,
options: CompileOptions,
stats: Stats
) {
super(parsed, source, name, stylesheet, options, stats, false);
this.bindings = []; this.bindings = [];
this.renderCode = ''; this.renderCode = '';
this.appendTargets = []; this.appendTargets = [];
this.stylesheet.warnOnUnusedSelectors(options.onwarn);
} }
append(code: string) { append(code: string) {
@ -44,7 +32,7 @@ export class SsrGenerator extends Generator {
} }
export default function ssr( export default function ssr(
parsed: Parsed, ast: Ast,
source: string, source: string,
stylesheet: Stylesheet, stylesheet: Stylesheet,
options: CompileOptions, options: CompileOptions,
@ -52,28 +40,22 @@ export default function ssr(
) { ) {
const format = options.format || 'cjs'; const format = options.format || 'cjs';
const generator = new SsrGenerator(parsed, source, options.name || 'SvelteComponent', stylesheet, options, stats); const target = new SsrTarget();
const compiler = new Compiler(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, false, target);
const { computations, name, templateProperties } = generator; const { computations, name, templateProperties } = compiler;
// create main render() function // create main render() function
const mainBlock = new Block({ trim(compiler.fragment.children).forEach((node: Node) => {
generator, node.ssr();
contexts: new Map(),
indexes: new Map(),
conditions: [],
});
trim(parsed.html.children).forEach((node: Node) => {
visit(generator, mainBlock, node);
}); });
const css = generator.customElement ? const css = compiler.customElement ?
{ code: null, map: null } : { code: null, map: null } :
generator.stylesheet.render(options.filename, true); compiler.stylesheet.render(options.filename, true);
// generate initial state object // 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 globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = expectedProperties.filter(prop => prop[0] === '$'); const storeProps = expectedProperties.filter(prop => prop[0] === '$');
@ -93,11 +75,13 @@ export default function ssr(
initialState.push('{}'); initialState.push('{}');
} }
initialState.push('state'); initialState.push('ctx');
const helpers = new Set();
// TODO concatenate CSS maps // TODO concatenate CSS maps
const result = deindent` const result = deindent`
${generator.javascript} ${compiler.javascript}
var ${name} = {}; var ${name} = {};
@ -129,18 +113,18 @@ export default function ssr(
}; };
} }
${name}._render = function(__result, state, options) { ${name}._render = function(__result, ctx, options) {
${templateProperties.store && `options.store = %store();`} ${templateProperties.store && `options.store = %store();`}
__result.addComponent(${name}); __result.addComponent(${name});
state = Object.assign(${initialState.join(', ')}); ctx = Object.assign(${initialState.join(', ')});
${computations.map( ${computations.map(
({ key, deps }) => ({ key, deps }) =>
`state.${key} = %computed-${key}(state);` `ctx.${key} = %computed-${key}(ctx);`
)} )}
${generator.bindings.length && ${target.bindings.length &&
deindent` deindent`
var settled = false; var settled = false;
var tmp; var tmp;
@ -148,11 +132,11 @@ export default function ssr(
while (!settled) { while (!settled) {
settled = true; settled = true;
${generator.bindings.join('\n\n')} ${target.bindings.join('\n\n')}
} }
`} `}
return \`${generator.renderCode}\`; return \`${target.renderCode}\`;
}; };
${name}.css = { ${name}.css = {
@ -163,64 +147,9 @@ export default function ssr(
var warned = false; var warned = false;
${templateProperties.preload && `${name}.preload = %preload;`} ${templateProperties.preload && `${name}.preload = %preload;`}
`;
${ return compiler.generate(result, options, { name, format });
// TODO this is a bit hacky
/__escape/.test(generator.renderCode) && deindent`
var escaped = {
'"': '&quot;',
"'": '&##39;',
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
function __escape(html) {
return String(html).replace(/["'&<>]/g, match => escaped[match]);
}
`
}
${
/__isPromise/.test(generator.renderCode) && deindent`
function __isPromise(value) {
return value && typeof value.then === 'function';
}
`
}
${
/__missingComponent/.test(generator.renderCode) && deindent`
var __missingComponent = {
_render: () => ''
};
`
}
${
/__spread/.test(generator.renderCode) && deindent`
function __spread(args) {
const attributes = Object.assign({}, ...args);
let str = '';
Object.keys(attributes).forEach(name => {
const value = attributes[name];
if (value === undefined) return;
if (value === true) str += " " + name;
str += " " + name + "=" + JSON.stringify(value);
});
return str;
}
`
}
`.replace(/(@+|#+|%+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
if (sigil === '@') return generator.alias(name);
if (sigil === '%') return generator.templateVars.get(name);
return sigil.slice(1) + name;
});
return generator.generate(result, options, { name, format });
} }
function trim(nodes) { function trim(nodes) {

@ -90,7 +90,7 @@ function es(
shorthandImports: ShorthandImport[], shorthandImports: ShorthandImport[],
source: string source: string
) { ) {
const importHelpers = helpers && ( const importHelpers = helpers.length > 0 && (
`import { ${helpers.map(h => h.name === h.alias ? h.name : `${h.name} as ${h.alias}`).join(', ')} } from ${JSON.stringify(sharedPath)};` `import { ${helpers.map(h => h.name === h.alias ? h.name : `${h.name} as ${h.alias}`).join(', ')} } from ${JSON.stringify(sharedPath)};`
); );
@ -145,12 +145,10 @@ function cjs(
helpers: { name: string, alias: string }[], helpers: { name: string, alias: string }[],
dependencies: Dependency[] dependencies: Dependency[]
) { ) {
const SHARED = '__shared'; const helperDeclarations = helpers.map(h => `${h.alias === h.name ? h.name : `${h.name}: ${h.alias}`}`).join(', ');
const helperBlock = helpers && (
`var ${SHARED} = require(${JSON.stringify(sharedPath)});\n` + const helperBlock = helpers.length > 0 && (
helpers.map(helper => { `var { ${helperDeclarations} } = require(${JSON.stringify(sharedPath)});\n`
return `var ${helper.alias} = ${SHARED}.${helper.name};`;
}).join('\n')
); );
const requireBlock = dependencies.length > 0 && ( const requireBlock = dependencies.length > 0 && (

@ -178,7 +178,7 @@ function applySelector(blocks: Block[], node: Node, stack: Node[], toEncapsulate
} }
else if (selector.type === 'RefSelector') { else if (selector.type === 'RefSelector') {
if (node.attributes.some((attr: Node) => attr.type === 'Ref' && attr.name === selector.name)) { if (node.ref === selector.name) {
node._cssRefAttribute = selector.name; node._cssRefAttribute = selector.name;
toEncapsulate.push({ node, block }); toEncapsulate.push({ node, block });
return true; return true;
@ -235,18 +235,18 @@ function attributeMatches(node: Node, name: string, expectedValue: string, opera
const attr = node.attributes.find((attr: Node) => attr.name === name); const attr = node.attributes.find((attr: Node) => attr.name === name);
if (!attr) return false; if (!attr) return false;
if (attr.value === true) return operator === null; if (attr.isTrue) return operator === null;
if (attr.value.length > 1) return true; if (attr.chunks.length > 1) return true;
if (!expectedValue) return true; if (!expectedValue) return true;
const pattern = operators[operator](expectedValue, caseInsensitive ? 'i' : ''); const pattern = operators[operator](expectedValue, caseInsensitive ? 'i' : '');
const value = attr.value[0]; const value = attr.chunks[0];
if (!value) return false; if (!value) return false;
if (value.type === 'Text') return pattern.test(value.data); if (value.type === 'Text') return pattern.test(value.data);
const possibleValues = new Set(); const possibleValues = new Set();
gatherPossibleValues(value.expression, possibleValues); gatherPossibleValues(value.node, possibleValues);
if (possibleValues.has(UNKNOWN)) return true; if (possibleValues.has(UNKNOWN)) return true;
for (const x of Array.from(possibleValues)) { // TypeScript for-of is slightly unlike JS for (const x of Array.from(possibleValues)) { // TypeScript for-of is slightly unlike JS

@ -4,9 +4,9 @@ import { getLocator } from 'locate-character';
import Selector from './Selector'; import Selector from './Selector';
import getCodeFrame from '../utils/getCodeFrame'; import getCodeFrame from '../utils/getCodeFrame';
import hash from '../utils/hash'; import hash from '../utils/hash';
import Element from '../generators/nodes/Element'; import Element from '../compile/nodes/Element';
import { Validator } from '../validate/index'; import { Validator } from '../validate/index';
import { Node, Parsed, Warning } from '../interfaces'; import { Node, Ast, Warning } from '../interfaces';
class Rule { class Rule {
selectors: Selector[]; selectors: Selector[];
@ -236,7 +236,7 @@ const keys = {};
export default class Stylesheet { export default class Stylesheet {
source: string; source: string;
parsed: Parsed; ast: Ast;
filename: string; filename: string;
dev: boolean; dev: boolean;
@ -248,9 +248,9 @@ export default class Stylesheet {
nodesWithCssClass: Set<Node>; nodesWithCssClass: Set<Node>;
constructor(source: string, parsed: Parsed, filename: string, dev: boolean) { constructor(source: string, ast: Ast, filename: string, dev: boolean) {
this.source = source; this.source = source;
this.parsed = parsed; this.ast = ast;
this.filename = filename; this.filename = filename;
this.dev = dev; this.dev = dev;
@ -259,15 +259,15 @@ export default class Stylesheet {
this.nodesWithCssClass = new Set(); this.nodesWithCssClass = new Set();
if (parsed.css && parsed.css.children.length) { if (ast.css && ast.css.children.length) {
this.id = `svelte-${hash(parsed.css.content.styles)}`; this.id = `svelte-${hash(ast.css.content.styles)}`;
this.hasStyles = true; this.hasStyles = true;
const stack: (Rule | Atrule)[] = []; const stack: (Rule | Atrule)[] = [];
let currentAtrule: Atrule = null; let currentAtrule: Atrule = null;
walk(this.parsed.css, { walk(this.ast.css, {
enter: (node: Node) => { enter: (node: Node) => {
if (node.type === 'Atrule') { if (node.type === 'Atrule') {
const last = stack[stack.length - 1]; const last = stack[stack.length - 1];
@ -346,7 +346,7 @@ export default class Stylesheet {
const code = new MagicString(this.source); const code = new MagicString(this.source);
walk(this.parsed.css, { walk(this.ast.css, {
enter: (node: Node) => { enter: (node: Node) => {
code.addSourcemapLocation(node.start); code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end); code.addSourcemapLocation(node.end);

@ -1,7 +0,0 @@
import Node from './shared/Node';
export default class Action extends Node {
name: string;
value: Node[]
expression: Node
}

@ -1,7 +0,0 @@
import Node from './shared/Node';
import Block from '../dom/Block';
export default class CatchBlock extends Node {
block: Block;
children: Node[];
}

@ -1,5 +0,0 @@
import Node from './shared/Node';
export default class Comment extends Node {
type: 'Comment'
}

@ -1,533 +0,0 @@
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 mungeAttribute from './shared/mungeAttribute';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
import usesThisOrArguments from '../../validate/js/utils/usesThisOrArguments';
export default class Component extends Node {
type: 'Component';
name: string;
attributes: Attribute[];
children: Node[];
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
this.cannotUseInnerHTML();
this.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Attribute' && attribute.value !== true) {
attribute.value.forEach((chunk: Node) => {
if (chunk.type !== 'Text') {
const dependencies = chunk.metadata.dependencies;
block.addDependencies(dependencies);
}
});
} else {
if (attribute.type === 'EventHandler' && attribute.expression) {
attribute.expression.arguments.forEach((arg: Node) => {
block.addDependencies(arg.metadata.dependencies);
});
} else if (attribute.type === 'Binding' || attribute.type === 'Spread') {
block.addDependencies(attribute.metadata.dependencies);
}
}
});
this.var = block.getUniqueName(
(
this.name === 'svelte:self' ? this.generator.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 { generator } = this;
generator.hasComponents = true;
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 allContexts = new Set();
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 attributes = this.attributes
.filter(a => a.type === 'Attribute' || a.type === 'Spread')
.map(a => mungeAttribute(a, block));
const bindings = this.attributes
.filter(a => a.type === 'Binding')
.map(a => mungeBinding(a, block));
const eventHandlers = this.attributes
.filter((a: Node) => a.type === 'EventHandler')
.map(a => mungeEventHandler(generator, this, a, block, allContexts));
const ref = this.attributes.find((a: Node) => a.type === 'Ref');
if (ref) generator.usesRefs = true;
const updates: string[] = [];
const usesSpread = !!attributes.find(a => a.spread);
const attributeObject = usesSpread
? '{}'
: stringifyProps(
attributes.map((attribute: Attribute) => `${attribute.name}: ${attribute.value}`)
);
if (attributes.length || bindings.length) {
componentInitProperties.push(`data: ${name_initial_data}`);
}
if ((!usesSpread && attributes.filter(a => a.dynamic).length) || bindings.length) {
updates.push(`var ${name_changes} = {};`);
}
if (attributes.length) {
if (usesSpread) {
const levels = block.getUniqueName(`${this.var}_spread_levels`);
const initialProps = [];
const changes = [];
attributes
.forEach(munged => {
const { spread, name, dynamic, value, dependencies } = munged;
if (spread) {
initialProps.push(value);
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
changes.push(condition ? `${condition} && ${value}` : value);
} else {
const obj = `{ ${quoteIfNecessary(name)}: ${value} }`;
initialProps.push(obj);
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
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 {
attributes
.filter((attribute: Attribute) => attribute.dynamic)
.forEach((attribute: Attribute) => {
if (attribute.dependencies.length) {
updates.push(deindent`
if (${attribute.dependencies
.map(dependency => `changed.${dependency}`)
.join(' || ')}) ${name_changes}.${attribute.name} = ${attribute.value};
`);
}
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.value};`);
}
});
}
}
if (bindings.length) {
generator.hasComplexBindings = true;
name_updating = block.alias(`${name}_updating`);
block.addVariable(name_updating, '{}');
let hasLocalBindings = false;
let hasStoreBindings = false;
const builder = new CodeBuilder();
bindings.forEach((binding: Binding) => {
let { name: key } = getObject(binding.value);
binding.contexts.forEach(context => {
allContexts.add(context);
});
let setFromChild;
if (block.contexts.has(key)) {
const computed = isComputed(binding.value);
const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : '';
const list = block.listNames.get(key);
const index = block.indexNames.get(key);
setFromChild = deindent`
${list}[${index}]${tail} = childState.${binding.name};
${binding.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} = state.${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.type === 'MemberExpression') {
setFromChild = deindent`
${binding.snippet} = childState.${binding.name};
${newState}.${prop} = state.${key};
`;
}
else {
setFromChild = `${newState}.${prop} = childState.${binding.name};`;
}
}
statements.push(deindent`
if (${binding.prop} in ${binding.obj}) {
${name_initial_data}.${binding.name} = ${binding.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.dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
${name_changes}.${binding.name} = ${binding.snippet};
${name_updating}.${binding.name} = true;
}
`);
});
const initialisers = [
'state = #component.get()',
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({ ${bindings.map(b => `${b.name}: 1`).join(', ')} }, ${name}.get());
});
`;
}
if (this.name === 'svelte:component') {
const switch_value = block.getUniqueName('switch_value');
const switch_props = block.getUniqueName('switch_props');
block.contextualise(this.expression);
const { dependencies, snippet } = this.metadata;
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
block.builders.init.addBlock(deindent`
var ${switch_value} = ${snippet};
function ${switch_props}(state) {
${(attributes.length || bindings.length) && deindent`
var ${name_initial_data} = ${attributeObject};`}
${statements}
return {
${componentInitProperties.join(',\n')}
};
}
if (${switch_value}) {
var ${name} = new ${switch_value}(${switch_props}(state));
${beforecreate}
}
${eventHandlers.map(handler => deindent`
function ${handler.var}(event) {
${handler.body}
}
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'});
${ref && `#component.refs.${ref.name} = ${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}(state));
${name}._fragment.c();
${this.children.map(child => child.remount(name))}
${name}._mount(${updateMountNode}, ${anchor});
${eventHandlers.map(handler => deindent`
${name}.on("${handler.name}", ${handler.var});
`)}
${ref && `#component.refs.${ref.name} = ${name};`}
}
${ref && deindent`
else if (#component.refs.${ref.name} === ${name}) {
#component.refs.${ref.name} = null;
}`}
}
`);
if (updates.length) {
block.builders.update.addBlock(deindent`
else {
${updates}
${name}._set(${name_changes});
${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'
? generator.name
: `%components-${this.name}`;
block.builders.init.addBlock(deindent`
${(attributes.length || bindings.length) && deindent`
var ${name_initial_data} = ${attributeObject};`}
${statements}
var ${name} = new ${expression}({
${componentInitProperties.join(',\n')}
});
${beforecreate}
${eventHandlers.map(handler => deindent`
${name}.on("${handler.name}", function(event) {
${handler.body}
});
`)}
${ref && `#component.refs.${ref.name} = ${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});
${bindings.length && `${name_updating} = {};`}
`);
}
if (!parentNode) block.builders.unmount.addLine(`${name}._unmount();`);
block.builders.destroy.addLine(deindent`
${name}.destroy(false);
${ref && `if (#component.refs.${ref.name} === ${name}) #component.refs.${ref.name} = null;`}
`);
}
}
remount(name: string) {
return `${this.var}._mount(${name}._slotted.default, null);`;
}
}
function mungeBinding(binding: Node, block: Block): Binding {
const { name } = getObject(binding.value);
const { contexts } = block.contextualise(binding.value);
const { dependencies, snippet } = binding.metadata;
const contextual = block.contexts.has(name);
let obj;
let prop;
if (contextual) {
obj = `state.${block.listNames.get(name)}`;
prop = `${block.indexNames.get(name)}`;
} else if (binding.value.type === 'MemberExpression') {
prop = `[✂${binding.value.property.start}-${binding.value.property.end}✂]`;
if (!binding.value.computed) prop = `'${prop}'`;
obj = `[✂${binding.value.object.start}-${binding.value.object.end}✂]`;
} else {
obj = 'state';
prop = `'${name}'`;
}
return {
name: binding.name,
value: binding.value,
contexts,
snippet,
obj,
prop,
dependencies
};
}
function mungeEventHandler(generator: DomGenerator, node: Node, handler: Node, block: Block, allContexts: Set<string>) {
let body;
if (handler.expression) {
generator.addSourcemapLocations(handler.expression);
// TODO try out repetition between this and element counterpart
const flattened = flattenReference(handler.expression.callee);
if (!validCalleeObjects.has(flattened.name)) {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.code.prependRight(
handler.expression.start,
`${block.alias('component')}.`
);
}
let usesState = false;
handler.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, null, true);
if (contexts.has('state')) usesState = true;
contexts.forEach(context => {
allContexts.add(context);
});
});
body = deindent`
${usesState && `const state = #component.get();`}
[${handler.expression.start}-${handler.expression.end}];
`;
} else {
body = deindent`
#component.fire('${handler.name}', event);
`;
}
return {
name: handler.name,
var: block.getUniqueName(`${node.var}_${handler.name}`),
body
};
}
function isComputed(node: Node) {
while (node.type === 'MemberExpression') {
if (node.computed) return true;
node = node.object;
}
return false;
}

@ -1,8 +0,0 @@
import Node from './shared/Node';
import Block from '../dom/Block';
export default class ElseBlock extends Node {
type: 'ElseBlock';
children: Node[];
block: Block;
}

@ -1,7 +0,0 @@
import Node from './shared/Node';
export default class EventHandler extends Node {
name: string;
value: Node[]
expression: Node
}

@ -1,7 +0,0 @@
import Node from './shared/Node';
import Block from '../dom/Block';
export default class PendingBlock extends Node {
block: Block;
children: Node[];
}

@ -1,7 +0,0 @@
import Node from './shared/Node';
export default class Ref extends Node {
name: string;
value: Node[]
expression: Node
}

@ -1,7 +0,0 @@
import Node from './shared/Node';
import Block from '../dom/Block';
export default class ThenBlock extends Node {
block: Block;
children: Node[];
}

@ -1,7 +0,0 @@
import Node from './shared/Node';
export default class Transition extends Node {
name: string;
value: Node[]
expression: Node
}

@ -1,54 +0,0 @@
import Node from './shared/Node';
import Attribute from './Attribute';
import AwaitBlock from './AwaitBlock';
import Action from './Action';
import Binding from './Binding';
import CatchBlock from './CatchBlock';
import Comment from './Comment';
import Component from './Component';
import EachBlock from './EachBlock';
import Element from './Element';
import ElseBlock from './ElseBlock';
import EventHandler from './EventHandler';
import Fragment from './Fragment';
import Head from './Head';
import IfBlock from './IfBlock';
import MustacheTag from './MustacheTag';
import PendingBlock from './PendingBlock';
import RawMustacheTag from './RawMustacheTag';
import Ref from './Ref';
import Slot from './Slot';
import Text from './Text';
import ThenBlock from './ThenBlock';
import Title from './Title';
import Transition from './Transition';
import Window from './Window';
const nodes: Record<string, any> = {
Attribute,
AwaitBlock,
Action,
Binding,
CatchBlock,
Comment,
Component,
EachBlock,
Element,
ElseBlock,
EventHandler,
Fragment,
Head,
IfBlock,
MustacheTag,
PendingBlock,
RawMustacheTag,
Ref,
Slot,
Text,
ThenBlock,
Title,
Transition,
Window
};
export default nodes;

@ -1,45 +0,0 @@
import Node from './Node';
import Block from '../../dom/Block';
export default class Tag extends Node {
renameThisMethod(
block: Block,
update: ((value: string) => string)
) {
const { indexes } = block.contextualise(this.expression);
const { dependencies, snippet } = this.metadata;
const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
const shouldCache = (
this.expression.type !== 'Identifier' ||
block.contexts.has(this.expression.name) ||
hasChangeableIndex
);
const value = shouldCache && block.getUniqueName(`${this.var}_value`);
const content = shouldCache ? value : snippet;
if (shouldCache) block.addVariable(value, snippet);
if (dependencies.length || hasChangeableIndex) {
const changedCheck = (
(block.hasOutroMethod ? `#outroing || ` : '') +
dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')
);
const updateCachedValue = `${value} !== (${value} = ${snippet})`;
const condition = shouldCache ?
(dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
changedCheck;
block.builders.update.addConditional(
condition,
update(content)
);
}
return { init: content };
}
}

@ -1,107 +0,0 @@
import { stringify } from '../../../utils/stringify';
import getExpressionPrecedence from '../../../utils/getExpressionPrecedence';
import Node from './Node';
import Attribute from '../Attribute';
import Block from '../../dom/Block';
type MungedAttribute = {
spread: boolean;
name: string;
value: string | true;
dependencies: string[];
dynamic: boolean;
}
export default function mungeAttribute(attribute: Node, block: Block): MungedAttribute {
if (attribute.type === 'Spread') {
block.contextualise(attribute.expression); // TODO remove
const { dependencies, snippet } = attribute.metadata;
return {
spread: true,
name: null,
value: snippet,
dynamic: dependencies.length > 0,
dependencies
};
}
if (attribute.value === true) {
// attributes without values, e.g. <textarea readonly>
return {
spread: false,
name: attribute.name,
value: true,
dynamic: false,
dependencies: []
};
}
if (attribute.value.length === 0) {
return {
spread: false,
name: attribute.name,
value: `''`,
dynamic: false,
dependencies: []
};
}
if (attribute.value.length === 1) {
const value = attribute.value[0];
if (value.type === 'Text') {
// static attributes
return {
spread: false,
name: attribute.name,
value: stringify(value.data),
dynamic: false,
dependencies: []
};
}
// simple dynamic attributes
block.contextualise(value.expression); // TODO remove
const { dependencies, snippet } = value.metadata;
// TODO only update attributes that have changed
return {
spread: false,
name: attribute.name,
value: snippet,
dependencies,
dynamic: true
};
}
// otherwise we're dealing with a complex dynamic attribute
const allDependencies = new Set();
const value =
(attribute.value[0].type === 'Text' ? '' : `"" + `) +
attribute.value
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
block.contextualise(chunk.expression); // TODO remove
const { dependencies, snippet } = chunk.metadata;
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
return {
spread: false,
name: attribute.name,
value,
dependencies: Array.from(allDependencies),
dynamic: true
};
}

@ -1,49 +0,0 @@
import deindent from '../../utils/deindent';
import flattenReference from '../../utils/flattenReference';
import { SsrGenerator } from './index';
import { Node } from '../../interfaces';
import getObject from '../../utils/getObject';
interface BlockOptions {
// TODO
}
export default class Block {
generator: SsrGenerator;
conditions: string[];
contexts: Map<string, string>;
indexes: Map<string, string>;
contextDependencies: Map<string, string[]>;
constructor(options: BlockOptions) {
Object.assign(this, options);
}
addBinding(binding: Node, name: string) {
const conditions = [`!('${binding.name}' in state)`].concat(
// TODO handle contextual bindings...
this.conditions.map(c => `(${c})`)
);
const { name: prop } = getObject(binding.value);
this.generator.bindings.push(deindent`
if (${conditions.join('&&')}) {
tmp = ${name}.data();
if ('${prop}' in tmp) {
state.${binding.name} = tmp.${prop};
settled = false;
}
}
`);
}
child(options: BlockOptions) {
return new Block(Object.assign({}, this, options, { parent: this }));
}
contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler);
}
}

@ -1,14 +0,0 @@
import { SsrGenerator } from './index';
import Block from './Block';
import { Node } from '../../interfaces';
export type Visitor = (
generator: SsrGenerator,
block: Block,
node: Node
) => void;
export interface AppendTarget {
slots: Record<string, string>;
slotStack: string[]
}

@ -1,13 +0,0 @@
import visitors from './visitors/index';
import { SsrGenerator } from './index';
import Block from './Block';
import { Node } from '../../interfaces';
export default function visit(
generator: SsrGenerator,
block: Block,
node: Node
) {
const visitor = visitors[node.type];
visitor(generator, block, node);
}

@ -1,40 +0,0 @@
import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitAwaitBlock(
generator: SsrGenerator,
block: Block,
node: Node
) {
block.contextualise(node.expression);
const { dependencies, snippet } = node.metadata;
// TODO should this be the generator's job? It's duplicated between
// here and the equivalent DOM compiler visitor
const contexts = new Map(block.contexts);
contexts.set(node.value, '__value');
const contextDependencies = new Map(block.contextDependencies);
contextDependencies.set(node.value, dependencies);
const childBlock = block.child({
contextDependencies,
contexts
});
generator.append('${(function(__value) { if(__isPromise(__value)) return `');
node.pending.children.forEach((child: Node) => {
visit(generator, childBlock, child);
});
generator.append('`; return `');
node.then.children.forEach((child: Node) => {
visit(generator, childBlock, child);
});
generator.append(`\`;}(${snippet})) }`);
}

@ -1,14 +0,0 @@
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitComment(
generator: SsrGenerator,
block: Block,
node: Node
) {
// Allow option to preserve comments, otherwise ignore
if (generator && generator.options && generator.options.preserveComments) {
generator.append(`<!--${node.data}-->`);
}
}

@ -1,135 +0,0 @@
import flattenReference from '../../../utils/flattenReference';
import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { AppendTarget } from '../interfaces';
import { Node } from '../../../interfaces';
import getObject from '../../../utils/getObject';
import getTailSnippet from '../../../utils/getTailSnippet';
import { escape, escapeTemplate, stringify } from '../../../utils/stringify';
export default function visitComponent(
generator: SsrGenerator,
block: Block,
node: Node
) {
function stringifyAttribute(chunk: Node) {
if (chunk.type === 'Text') {
return escapeTemplate(escape(chunk.data));
}
if (chunk.type === 'MustacheTag') {
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
return '${__escape( ' + snippet + ')}';
}
}
const attributes: Node[] = [];
const bindings: Node[] = [];
let usesSpread;
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Attribute' || attribute.type === 'Spread') {
if (attribute.type === 'Spread') usesSpread = true;
attributes.push(attribute);
} else if (attribute.type === 'Binding') {
bindings.push(attribute);
}
});
const bindingProps = bindings.map(binding => {
const { name } = getObject(binding.value);
const tail = binding.value.type === 'MemberExpression'
? getTailSnippet(binding.value)
: '';
const keypath = block.contexts.has(name)
? `${name}${tail}`
: `state.${name}${tail}`;
return `${binding.name}: ${keypath}`;
});
function getAttributeValue(attribute) {
if (attribute.value === true) return `true`;
if (attribute.value.length === 0) return `''`;
if (attribute.value.length === 1) {
const chunk = attribute.value[0];
if (chunk.type === 'Text') {
return stringify(chunk.data);
}
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
return snippet;
}
return '`' + attribute.value.map(stringifyAttribute).join('') + '`';
}
const props = usesSpread
? `Object.assign(${
attributes
.map(attribute => {
if (attribute.type === 'Spread') {
block.contextualise(attribute.expression);
return attribute.metadata.snippet;
} else {
return `{ ${attribute.name}: ${getAttributeValue(attribute)} }`;
}
})
.concat(bindingProps.map(p => `{ ${p} }`))
.join(', ')
})`
: `{ ${attributes
.map(attribute => `${attribute.name}: ${getAttributeValue(attribute)}`)
.concat(bindingProps)
.join(', ')} }`;
const isDynamicComponent = node.name === 'svelte:component';
if (isDynamicComponent) block.contextualise(node.expression);
const expression = (
node.name === 'svelte:self' ? generator.name :
isDynamicComponent ? `((${node.metadata.snippet}) || __missingComponent)` :
`%components-${node.name}`
);
bindings.forEach(binding => {
block.addBinding(binding, expression);
});
let open = `\${${expression}._render(__result, ${props}`;
const options = [];
options.push(`store: options.store`);
if (node.children.length) {
const appendTarget: AppendTarget = {
slots: { default: '' },
slotStack: ['default']
};
generator.appendTargets.push(appendTarget);
node.children.forEach((child: Node) => {
visit(generator, block, child);
});
const slotted = Object.keys(appendTarget.slots)
.map(name => `${name}: () => \`${appendTarget.slots[name]}\``)
.join(', ');
options.push(`slotted: { ${slotted} }`);
generator.appendTargets.pop();
}
if (options.length) {
open += `, { ${options.join(', ')} }`;
}
generator.append(open);
generator.append(')}');
}

@ -1,57 +0,0 @@
import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitEachBlock(
generator: SsrGenerator,
block: Block,
node: Node
) {
block.contextualise(node.expression);
const { dependencies, snippet } = node.metadata;
const open = `\${ ${node.else ? `${snippet}.length ? ` : ''}${snippet}.map(${node.index ? `(${node.context}, ${node.index})` : `(${node.context})`} => \``;
generator.append(open);
// TODO should this be the generator's job? It's duplicated between
// here and the equivalent DOM compiler visitor
const contexts = new Map(block.contexts);
contexts.set(node.context, node.context);
const indexes = new Map(block.indexes);
if (node.index) indexes.set(node.index, node.context);
const contextDependencies = new Map(block.contextDependencies);
contextDependencies.set(node.context, dependencies);
if (node.destructuredContexts) {
for (let i = 0; i < node.destructuredContexts.length; i += 1) {
contexts.set(node.destructuredContexts[i], `${node.context}[${i}]`);
contextDependencies.set(node.destructuredContexts[i], dependencies);
}
}
const childBlock = block.child({
contexts,
indexes,
contextDependencies,
});
node.children.forEach((child: Node) => {
visit(generator, childBlock, child);
});
const close = `\`).join("")`;
generator.append(close);
if (node.else) {
generator.append(` : \``);
node.else.children.forEach((child: Node) => {
visit(generator, block, child);
});
generator.append(`\``);
}
generator.append('}');
}

@ -1,106 +0,0 @@
import visitComponent from './Component';
import visitSlot from './Slot';
import isVoidElementName from '../../../utils/isVoidElementName';
import quoteIfNecessary from '../../../utils/quoteIfNecessary';
import visit from '../visit';
import { SsrGenerator } from '../index';
import Element from '../../nodes/Element';
import Block from '../Block';
import { Node } from '../../../interfaces';
import stringifyAttributeValue from './shared/stringifyAttributeValue';
import { escape } from '../../../utils/stringify';
// 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 function visitElement(
generator: SsrGenerator,
block: Block,
node: Element
) {
if (node.name === 'slot') {
visitSlot(generator, block, node);
return;
}
let openingTag = `<${node.name}`;
let textareaContents; // awkward special case
const slot = node.getStaticAttributeValue('slot');
if (slot && node.hasAncestor('Component')) {
const slot = node.attributes.find((attribute: Node) => attribute.name === 'slot');
const slotName = slot.value[0].data;
const appendTarget = generator.appendTargets[generator.appendTargets.length - 1];
appendTarget.slotStack.push(slotName);
appendTarget.slots[slotName] = '';
}
if (node.attributes.find(attr => attr.type === 'Spread')) {
// TODO dry this out
const args = [];
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Spread') {
block.contextualise(attribute.expression);
args.push(attribute.metadata.snippet);
} else if (attribute.type === 'Attribute') {
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = stringifyAttributeValue(block, attribute.value);
} else if (attribute.value === true) {
args.push(`{ ${quoteIfNecessary(attribute.name)}: true }`);
} else if (
booleanAttributes.has(attribute.name) &&
attribute.value.length === 1 &&
attribute.value[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
block.contextualise(attribute.value[0].expression);
args.push(`{ ${quoteIfNecessary(attribute.name)}: ${attribute.value[0].metadata.snippet} }`);
} else {
args.push(`{ ${quoteIfNecessary(attribute.name)}: \`${stringifyAttributeValue(block, attribute.value)}\` }`);
}
}
});
openingTag += "${__spread([" + args.join(', ') + "])}";
} else {
node.attributes.forEach((attribute: Node) => {
if (attribute.type !== 'Attribute') return;
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = stringifyAttributeValue(block, attribute.value);
} else if (attribute.value === true) {
openingTag += ` ${attribute.name}`;
} else if (
booleanAttributes.has(attribute.name) &&
attribute.value.length === 1 &&
attribute.value[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
block.contextualise(attribute.value[0].expression);
openingTag += '${' + attribute.value[0].metadata.snippet + ' ? " ' + attribute.name + '" : "" }';
} else {
openingTag += ` ${attribute.name}="${stringifyAttributeValue(block, attribute.value)}"`;
}
});
}
if (node._cssRefAttribute) {
openingTag += ` svelte-ref-${node._cssRefAttribute}`;
}
openingTag += '>';
generator.append(openingTag);
if (node.name === 'textarea' && textareaContents !== undefined) {
generator.append(textareaContents);
} else {
node.children.forEach((child: Node) => {
visit(generator, block, child);
});
}
if (!isVoidElementName(node.name)) {
generator.append(`</${node.name}>`);
}
}

@ -1,19 +0,0 @@
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
import stringifyAttributeValue from './shared/stringifyAttributeValue';
import visit from '../visit';
export default function visitDocument(
generator: SsrGenerator,
block: Block,
node: Node
) {
generator.append('${(__result.head += `');
node.children.forEach((child: Node) => {
visit(generator, block, child);
});
generator.append('`, "")}');
}

@ -1,33 +0,0 @@
import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitIfBlock(
generator: SsrGenerator,
block: Block,
node: Node
) {
block.contextualise(node.expression);
const { snippet } = node.metadata;
generator.append('${ ' + snippet + ' ? `');
const childBlock = block.child({
conditions: block.conditions.concat(snippet),
});
node.children.forEach((child: Node) => {
visit(generator, childBlock, child);
});
generator.append('` : `');
if (node.else) {
node.else.children.forEach((child: Node) => {
visit(generator, childBlock, child);
});
}
generator.append('` }');
}

@ -1,20 +0,0 @@
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitMustacheTag(
generator: SsrGenerator,
block: Block,
node: Node
) {
block.contextualise(node.expression);
const { snippet } = node.metadata;
generator.append(
node.parent &&
node.parent.type === 'Element' &&
node.parent.name === 'style'
? '${' + snippet + '}'
: '${__escape(' + snippet + ')}'
);
}

@ -1,14 +0,0 @@
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitRawMustacheTag(
generator: SsrGenerator,
block: Block,
node: Node
) {
block.contextualise(node.expression);
const { snippet } = node.metadata;
generator.append('${' + snippet + '}');
}

@ -1,21 +0,0 @@
import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitSlot(
generator: SsrGenerator,
block: Block,
node: Node
) {
const name = node.attributes.find((attribute: Node) => attribute.name);
const slotName = name && name.value[0].data || 'default';
generator.append(`\${options && options.slotted && options.slotted.${slotName} ? options.slotted.${slotName}() : \``);
node.children.forEach((child: Node) => {
visit(generator, block, child);
});
generator.append(`\`}`);
}

@ -1,21 +0,0 @@
import { SsrGenerator } from '../index';
import Block from '../Block';
import { escape, escapeHTML, escapeTemplate } from '../../../utils/stringify';
import { Node } from '../../../interfaces';
export default function visitText(
generator: SsrGenerator,
block: Block,
node: Node
) {
let text = node.data;
if (
!node.parent ||
node.parent.type !== 'Element' ||
(node.parent.name !== 'script' && node.parent.name !== 'style')
) {
// unless this Text node is inside a <script> or <style> element, escape &,<,>
text = escapeHTML(text);
}
generator.append(escape(escapeTemplate(text)));
}

@ -1,19 +0,0 @@
import { SsrGenerator } from '../index';
import Block from '../Block';
import { escape } from '../../../utils/stringify';
import visit from '../visit';
import { Node } from '../../../interfaces';
export default function visitTitle(
generator: SsrGenerator,
block: Block,
node: Node
) {
generator.append(`<title>`);
node.children.forEach((child: Node) => {
visit(generator, block, child);
});
generator.append(`</title>`);
}

@ -1,3 +0,0 @@
export default function visitWindow() {
// noop
}

@ -1,29 +0,0 @@
import AwaitBlock from './AwaitBlock';
import Comment from './Comment';
import Component from './Component';
import EachBlock from './EachBlock';
import Element from './Element';
import Head from './Head';
import IfBlock from './IfBlock';
import MustacheTag from './MustacheTag';
import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot';
import Text from './Text';
import Title from './Title';
import Window from './Window';
export default {
AwaitBlock,
Comment,
Component,
EachBlock,
Element,
Head,
IfBlock,
MustacheTag,
RawMustacheTag,
Slot,
Text,
Title,
Window
};

@ -1,17 +0,0 @@
import Block from '../../Block';
import { escape, escapeTemplate } from '../../../../utils/stringify';
import { Node } from '../../../../interfaces';
export default function stringifyAttributeValue(block: Block, chunks: Node[]) {
return chunks
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return escapeTemplate(escape(chunk.data).replace(/"/g, '&quot;'));
}
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
return '${__escape(' + snippet + ')}';
})
.join('');
}

@ -1,11 +1,11 @@
import parse from './parse/index'; import parse from './parse/index';
import validate from './validate/index'; import validate from './validate/index';
import generate from './generators/dom/index'; import generate from './compile/dom/index';
import generateSSR from './generators/server-side-rendering/index'; import generateSSR from './compile/ssr/index';
import Stats from './Stats'; import Stats from './Stats';
import { assign } from './shared/index.js'; import { assign } from './shared/index.js';
import Stylesheet from './css/Stylesheet'; import Stylesheet from './css/Stylesheet';
import { Parsed, CompileOptions, Warning, PreprocessOptions, Preprocessor } from './interfaces'; import { Ast, CompileOptions, Warning, PreprocessOptions, Preprocessor } from './interfaces';
import { SourceMap } from 'magic-string'; import { SourceMap } from 'magic-string';
const version = '__VERSION__'; const version = '__VERSION__';
@ -108,7 +108,7 @@ export async function preprocess(source: string, options: PreprocessOptions) {
function compile(source: string, _options: CompileOptions) { function compile(source: string, _options: CompileOptions) {
const options = normalizeOptions(_options); const options = normalizeOptions(_options);
let parsed: Parsed; let ast: Ast;
const stats = new Stats({ const stats = new Stats({
onwarn: options.onwarn onwarn: options.onwarn
@ -116,7 +116,7 @@ function compile(source: string, _options: CompileOptions) {
try { try {
stats.start('parse'); stats.start('parse');
parsed = parse(source, options); ast = parse(source, options);
stats.stop('parse'); stats.stop('parse');
} catch (err) { } catch (err) {
options.onerror(err); options.onerror(err);
@ -124,20 +124,20 @@ function compile(source: string, _options: CompileOptions) {
} }
stats.start('stylesheet'); stats.start('stylesheet');
const stylesheet = new Stylesheet(source, parsed, options.filename, options.dev); const stylesheet = new Stylesheet(source, ast, options.filename, options.dev);
stats.stop('stylesheet'); stats.stop('stylesheet');
stats.start('validate'); stats.start('validate');
validate(parsed, source, stylesheet, stats, options); validate(ast, source, stylesheet, stats, options);
stats.stop('validate'); stats.stop('validate');
if (options.generate === false) { if (options.generate === false) {
return { ast: parsed, stats, js: null, css: null }; return { ast: ast, stats, js: null, css: null };
} }
const compiler = options.generate === 'ssr' ? generateSSR : generate; const compiler = options.generate === 'ssr' ? generateSSR : generate;
return compiler(parsed, source, stylesheet, options, stats); return compiler(ast, source, stylesheet, options, stats);
}; };
function create(source: string, _options: CompileOptions = {}) { function create(source: string, _options: CompileOptions = {}) {

@ -20,7 +20,7 @@ export interface Parser {
metaTags: {}; metaTags: {};
} }
export interface Parsed { export interface Ast {
html: Node; html: Node;
css: Node; css: Node;
js: Node; js: Node;
@ -71,7 +71,6 @@ export interface GenerateOptions {
format: ModuleFormat; format: ModuleFormat;
banner?: string; banner?: string;
sharedPath?: string; sharedPath?: string;
helpers?: { name: string, alias: string }[];
} }
export interface ShorthandImport { export interface ShorthandImport {
@ -97,3 +96,8 @@ export interface PreprocessOptions {
} }
export type Preprocessor = (options: {content: string, attributes: Record<string, string | boolean>, filename?: string}) => { code: string, map?: SourceMap | string }; export type Preprocessor = (options: {content: string, attributes: Record<string, string | boolean>, filename?: string}) => { code: string, map?: SourceMap | string };
export interface AppendTarget {
slots: Record<string, string>;
slotStack: string[]
}

@ -5,12 +5,13 @@ import { whitespace } from '../utils/patterns';
import { trimStart, trimEnd } from '../utils/trim'; import { trimStart, trimEnd } from '../utils/trim';
import reservedNames from '../utils/reservedNames'; import reservedNames from '../utils/reservedNames';
import fullCharCodeAt from '../utils/fullCharCodeAt'; import fullCharCodeAt from '../utils/fullCharCodeAt';
import { Node, Parsed } from '../interfaces'; import { Node, Ast } from '../interfaces';
import error from '../utils/error'; import error from '../utils/error';
interface ParserOptions { interface ParserOptions {
filename?: string; filename?: string;
bind?: boolean; bind?: boolean;
customElement?: boolean;
} }
type ParserState = (parser: Parser) => (ParserState | void); type ParserState = (parser: Parser) => (ParserState | void);
@ -18,6 +19,7 @@ type ParserState = (parser: Parser) => (ParserState | void);
export class Parser { export class Parser {
readonly template: string; readonly template: string;
readonly filename?: string; readonly filename?: string;
readonly customElement: boolean;
index: number; index: number;
stack: Array<Node>; stack: Array<Node>;
@ -36,6 +38,7 @@ export class Parser {
this.template = template.replace(/\s+$/, ''); this.template = template.replace(/\s+$/, '');
this.filename = options.filename; this.filename = options.filename;
this.customElement = options.customElement;
this.allowBindings = options.bind !== false; this.allowBindings = options.bind !== false;
@ -220,7 +223,7 @@ export class Parser {
export default function parse( export default function parse(
template: string, template: string,
options: ParserOptions = {} options: ParserOptions = {}
): Parsed { ): Ast {
const parser = new Parser(template, options); const parser = new Parser(template, options);
return { return {
html: parser.html, html: parser.html,

@ -60,6 +60,16 @@ const disallowedContents = new Map([
['th', new Set(['td', 'th', 'tr'])], ['th', new Set(['td', 'th', 'tr'])],
]); ]);
function parentIsHead(stack) {
let i = stack.length;
while (i--) {
const { type } = stack[i];
if (type === 'Head') return true;
if (type === 'Element' || type === 'Component') return false;
}
return false;
}
export default function tag(parser: Parser) { export default function tag(parser: Parser) {
const start = parser.index++; const start = parser.index++;
@ -113,7 +123,9 @@ export default function tag(parser: Parser) {
const type = metaTags.has(name) const type = metaTags.has(name)
? metaTags.get(name) ? metaTags.get(name)
: 'Element'; // TODO in v2, capitalised name means 'Component' : (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'Component'
: name === 'title' && parentIsHead(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
const element: Node = { const element: Node = {
start, start,

@ -32,7 +32,7 @@ fs.readdirSync(__dirname).forEach(file => {
}); });
fs.writeFileSync( fs.writeFileSync(
'src/generators/dom/shared.ts', 'src/compile/shared.ts',
`// this file is auto-generated, do not edit it `// this file is auto-generated, do not edit it
const shared: Record<string, string> = ${JSON.stringify(declarations, null, '\t')}; const shared: Record<string, string> = ${JSON.stringify(declarations, null, '\t')};

@ -3,6 +3,7 @@ import { noop } from './utils.js';
export * from './dom.js'; export * from './dom.js';
export * from './keyed-each.js'; export * from './keyed-each.js';
export * from './spread.js'; export * from './spread.js';
export * from './ssr.js';
export * from './transitions.js'; export * from './transitions.js';
export * from './utils.js'; export * from './utils.js';

@ -0,0 +1,37 @@
export function spread(args) {
const attributes = Object.assign({}, ...args);
let str = '';
Object.keys(attributes).forEach(name => {
const value = attributes[name];
if (value === undefined) return;
if (value === true) str += " " + name;
str += " " + name + "=" + JSON.stringify(value);
});
return str;
}
export const escaped = {
'"': '&quot;',
"'": '&#39;',
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
export function escape(html) {
return String(html).replace(/["'&<>]/g, match => escaped[match]);
}
export function each(items, assign, fn) {
let str = '';
for (let i = 0; i < items.length; i += 1) {
str += fn(assign(items[i], i));
}
return str;
}
export const missingComponent = {
_render: () => ''
};

@ -0,0 +1,5 @@
export default function addToSet(a: Set<any>, b: Set<any>) {
b.forEach(item => {
a.add(item);
});
}

@ -2,6 +2,54 @@ import { walk } from 'estree-walker';
import isReference from 'is-reference'; import isReference from 'is-reference';
import { Node } from '../interfaces'; import { Node } from '../interfaces';
export function createScopes(expression: Node) {
const map = new WeakMap();
const globals = new Set();
let scope = new Scope(null, false);
walk(expression, {
enter(node: Node, parent: Node) {
if (/Function/.test(node.type)) {
if (node.type === 'FunctionDeclaration') {
scope.declarations.add(node.id.name);
} else {
scope = new Scope(scope, false);
map.set(node, scope);
if (node.id) scope.declarations.add(node.id.name);
}
node.params.forEach((param: Node) => {
extractNames(param).forEach(name => {
scope.declarations.add(name);
});
});
} else if (/For(?:In|Of)Statement/.test(node.type)) {
scope = new Scope(scope, true);
map.set(node, scope);
} else if (node.type === 'BlockStatement') {
scope = new Scope(scope, true);
map.set(node, scope);
} else if (/(Function|Class|Variable)Declaration/.test(node.type)) {
scope.addDeclaration(node);
} else if (isReference(node, parent)) {
if (!scope.has(node.name)) {
globals.add(node.name);
}
}
},
leave(node: Node) {
if (map.has(node)) {
scope = scope.parent;
}
},
});
return { map, scope, globals };
}
// TODO remove this in favour of weakmap version
export default function annotateWithScopes(expression: Node) { export default function annotateWithScopes(expression: Node) {
const globals = new Set(); const globals = new Set();
let scope = new Scope(null, false); let scope = new Scope(null, false);

@ -1,18 +0,0 @@
import { Node, Parsed } from '../interfaces';
export default function clone(node: Node|Parsed) {
const cloned: any = {};
for (const key in node) {
const value = node[key];
if (Array.isArray(value)) {
cloned[key] = value.map(clone);
} else if (value && typeof value === 'object') {
cloned[key] = clone(value);
} else {
cloned[key] = value;
}
}
return cloned;
}

@ -1,8 +1,8 @@
import { DomGenerator } from '../generators/dom/index'; import Compiler from '../compile/Compiler';
import { Node } from '../interfaces'; import { Node } from '../interfaces';
export default function createDebuggingComment(node: Node, generator: DomGenerator) { export default function createDebuggingComment(node: Node, compiler: Compiler) {
const { locate, source } = generator; const { locate, source } = compiler;
let c = node.start; let c = node.start;
if (node.type === 'ElseBlock') { if (node.type === 'ElseBlock') {
@ -10,7 +10,7 @@ export default function createDebuggingComment(node: Node, generator: DomGenerat
while (source[c - 1] === '{') c -= 1; while (source[c - 1] === '{') c -= 1;
} }
let d = node.expression ? node.expression.end : c; let d = node.expression ? node.expression.node.end : c;
while (source[d] !== '}') d += 1; while (source[d] !== '}') d += 1;
while (source[d] === '}') d += 1; while (source[d] === '}') d += 1;

@ -1,6 +1,7 @@
import { Node } from '../interfaces'; import { Node } from '../interfaces';
export default function flattenReference(node: Node) { export default function flattenReference(node: Node) {
if (node.type === 'Expression') throw new Error('bad');
const parts = []; const parts = [];
const propEnd = node.end; const propEnd = node.end;

@ -1,53 +0,0 @@
import { Node } from '../interfaces';
const binaryOperators: Record<string, number> = {
'**': 15,
'*': 14,
'/': 14,
'%': 14,
'+': 13,
'-': 13,
'<<': 12,
'>>': 12,
'>>>': 12,
'<': 11,
'<=': 11,
'>': 11,
'>=': 11,
'in': 11,
'instanceof': 11,
'==': 10,
'!=': 10,
'===': 10,
'!==': 10,
'&': 9,
'^': 8,
'|': 7
};
const logicalOperators: Record<string, number> = {
'&&': 6,
'||': 5
};
const precedence: Record<string, (expression?: Node) => number> = {
Literal: () => 21,
Identifier: () => 21,
ParenthesizedExpression: () => 20,
MemberExpression: () => 19,
NewExpression: () => 19, // can be 18 (if no args) but makes no practical difference
CallExpression: () => 19,
UpdateExpression: () => 17,
UnaryExpression: () => 16,
BinaryExpression: (expression: Node) => binaryOperators[expression.operator],
LogicalExpression: (expression: Node) => logicalOperators[expression.operator],
ConditionalExpression: () => 4,
AssignmentExpression: () => 3,
YieldExpression: () => 2,
SpreadElement: () => 1,
SequenceExpression: () => 0
};
export default function getExpressionPrecedence(expression: Node) {
return expression.type in precedence ? precedence[expression.type](expression) : 0;
}

@ -1,6 +1,8 @@
import validateComponent from './validateComponent';
import validateElement from './validateElement'; import validateElement from './validateElement';
import validateWindow from './validateWindow'; import validateWindow from './validateWindow';
import validateHead from './validateHead'; import validateHead from './validateHead';
import validateSlot from './validateSlot';
import a11y from './a11y'; import a11y from './a11y';
import fuzzymatch from '../utils/fuzzymatch' import fuzzymatch from '../utils/fuzzymatch'
import flattenReference from '../../utils/flattenReference'; import flattenReference from '../../utils/flattenReference';
@ -29,25 +31,32 @@ export default function validateHtml(validator: Validator, html: Node) {
validateHead(validator, node, refs, refCallees); validateHead(validator, node, refs, refCallees);
} }
else if (node.type === 'Element') { else if (node.type === 'Slot') {
const isComponent = validateSlot(validator, node);
node.name === 'svelte:self' || }
node.name === 'svelte:component' ||
validator.components.has(node.name); else if (node.type === 'Component' || node.name === 'svelte:self' || node.name === 'svelte:component') {
validateComponent(
validator,
node,
refs,
refCallees,
stack,
elementStack
);
}
else if (node.type === 'Element') {
validateElement( validateElement(
validator, validator,
node, node,
refs, refs,
refCallees, refCallees,
stack, stack,
elementStack, elementStack
isComponent
); );
if (!isComponent) { a11y(validator, node, elementStack);
a11y(validator, node, elementStack);
}
} }
else if (node.type === 'EachBlock') { else if (node.type === 'EachBlock') {

@ -0,0 +1,44 @@
import * as namespaces from '../../utils/namespaces';
import validateEventHandler from './validateEventHandler';
import validate, { Validator } from '../index';
import { Node } from '../../interfaces';
export default function validateComponent(
validator: Validator,
node: Node,
refs: Map<string, Node[]>,
refCallees: Node[],
stack: Node[],
elementStack: Node[]
) {
if (node.name !== 'svelte:self' && node.name !== 'svelte:component' && !validator.components.has(node.name)) {
validator.error(node, {
code: `missing-component`,
message: `${node.name} component is not defined`
});
}
validator.used.components.add(node.name);
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Ref') {
if (!refs.has(attribute.name)) refs.set(attribute.name, []);
refs.get(attribute.name).push(node);
}
if (attribute.type === 'EventHandler') {
validator.used.events.add(attribute.name);
validateEventHandler(validator, attribute, refCallees);
} else if (attribute.type === 'Transition') {
validator.error(attribute, {
code: `invalid-transition`,
message: `Transitions can only be applied to DOM elements, not components`
});
} else if (attribute.type === 'Action') {
validator.error(attribute, {
code: `invalid-action`,
message: `Actions can only be applied to DOM elements, not components`
});
}
});
}

@ -11,20 +11,8 @@ export default function validateElement(
refs: Map<string, Node[]>, refs: Map<string, Node[]>,
refCallees: Node[], refCallees: Node[],
stack: Node[], stack: Node[],
elementStack: Node[], elementStack: Node[]
isComponent: Boolean
) { ) {
if (isComponent) {
validator.used.components.add(node.name);
}
if (!isComponent && /^[A-Z]/.test(node.name[0])) {
validator.error(node, {
code: `missing-component`,
message: `${node.name} component is not defined`
});
}
if (elementStack.length === 0 && validator.namespace !== namespaces.svg && svg.test(node.name)) { if (elementStack.length === 0 && validator.namespace !== namespaces.svg && svg.test(node.name)) {
validator.warn(node, { validator.warn(node, {
code: `missing-namespace`, code: `missing-namespace`,
@ -95,7 +83,7 @@ export default function validateElement(
refs.get(attribute.name).push(node); refs.get(attribute.name).push(node);
} }
if (!isComponent && attribute.type === 'Binding') { if (attribute.type === 'Binding') {
const { name } = attribute; const { name } = attribute;
if (name === 'value') { if (name === 'value') {
@ -179,13 +167,6 @@ export default function validateElement(
validator.used.events.add(attribute.name); validator.used.events.add(attribute.name);
validateEventHandler(validator, attribute, refCallees); validateEventHandler(validator, attribute, refCallees);
} else if (attribute.type === 'Transition') { } else if (attribute.type === 'Transition') {
if (isComponent) {
validator.error(attribute, {
code: `invalid-transition`,
message: `Transitions can only be applied to DOM elements, not components`
});
}
validator.used.transitions.add(attribute.name); validator.used.transitions.add(attribute.name);
const bidi = attribute.intro && attribute.outro; const bidi = attribute.intro && attribute.outro;
@ -238,17 +219,10 @@ export default function validateElement(
} }
} }
if (attribute.name === 'slot' && !isComponent) { if (attribute.name === 'slot') {
checkSlotAttribute(validator, node, attribute, stack); checkSlotAttribute(validator, node, attribute, stack);
} }
} else if (attribute.type === 'Action') { } else if (attribute.type === 'Action') {
if (isComponent) {
validator.error(attribute, {
code: `invalid-action`,
message: `Actions can only be applied to DOM elements, not components`
});
}
validator.used.actions.add(attribute.name); validator.used.actions.add(attribute.name);
if (!validator.actions.has(attribute.name)) { if (!validator.actions.has(attribute.name)) {
@ -295,9 +269,11 @@ function checkSlotAttribute(validator: Validator, node: Node, attribute: Node, s
let i = stack.length; let i = stack.length;
while (i--) { while (i--) {
const parent = stack[i]; const parent = stack[i];
if (parent.type === 'Element') {
if (parent.type === 'Component') {
// if we're inside a component or a custom element, gravy // if we're inside a component or a custom element, gravy
if (parent.name === 'svelte:self' || parent.name === 'svelte:component' || validator.components.has(parent.name)) return; if (parent.name === 'svelte:self' || parent.name === 'svelte:component' || validator.components.has(parent.name)) return;
} else if (parent.type === 'Element') {
if (/-/.test(parent.name)) return; if (/-/.test(parent.name)) return;
} }

@ -13,7 +13,7 @@ export default function validateHead(validator: Validator, node: Node, refs: Map
// TODO ensure only valid elements are included here // TODO ensure only valid elements are included here
node.children.forEach(node => { node.children.forEach(node => {
if (node.type !== 'Element') return; // TODO handle {{#if}} and friends? if (node.type !== 'Element' && node.type !== 'Title') return; // TODO handle {{#if}} and friends?
validateElement(validator, node, refs, refCallees, [], [], false); validateElement(validator, node, refs, refCallees, [], []);
}); });
} }

@ -0,0 +1,56 @@
import * as namespaces from '../../utils/namespaces';
import validateEventHandler from './validateEventHandler';
import validate, { Validator } from '../index';
import { Node } from '../../interfaces';
export default function validateSlot(
validator: Validator,
node: Node
) {
node.attributes.forEach(attr => {
if (attr.type !== 'Attribute') {
validator.error(attr, {
code: `invalid-slot-directive`,
message: `<slot> cannot have directives`
});
}
if (attr.name !== 'name') {
validator.error(attr, {
code: `invalid-slot-attribute`,
message: `"name" is the only attribute permitted on <slot> elements`
});
}
if (attr.value.length !== 1 || attr.value[0].type !== 'Text') {
validator.error(attr, {
code: `dynamic-slot-name`,
message: `<slot> name cannot be dynamic`
});
}
const slotName = attr.value[0].data;
if (slotName === 'default') {
validator.error(attr, {
code: `invalid-slot-name`,
message: `default is a reserved word — it cannot be used as a slot name`
});
}
// TODO should duplicate slots be disallowed? Feels like it's more likely to be a
// bug than anything. Perhaps it should be a warning
// if (validator.slots.has(slotName)) {
// validator.error(`duplicate '${slotName}' <slot> element`, nameAttribute.start);
// }
// validator.slots.add(slotName);
});
// if (node.attributes.length === 0) && validator.slots.has('default')) {
// validator.error(node, {
// code: `duplicate-slot`,
// message: `duplicate default <slot> element`
// });
// }
}

@ -5,8 +5,7 @@ import getCodeFrame from '../utils/getCodeFrame';
import Stats from '../Stats'; import Stats from '../Stats';
import error from '../utils/error'; import error from '../utils/error';
import Stylesheet from '../css/Stylesheet'; import Stylesheet from '../css/Stylesheet';
import Stats from '../Stats'; import { Node, Ast, CompileOptions, Warning } from '../interfaces';
import { Node, Parsed, CompileOptions, Warning } from '../interfaces';
export class Validator { export class Validator {
readonly source: string; readonly source: string;
@ -34,7 +33,7 @@ export class Validator {
actions: Set<string>; actions: Set<string>;
}; };
constructor(parsed: Parsed, source: string, stats: Stats, options: CompileOptions) { constructor(ast: Ast, source: string, stats: Stats, options: CompileOptions) {
this.source = source; this.source = source;
this.stats = stats; this.stats = stats;
@ -93,7 +92,7 @@ export class Validator {
} }
export default function validate( export default function validate(
parsed: Parsed, ast: Ast,
source: string, source: string,
stylesheet: Stylesheet, stylesheet: Stylesheet,
stats: Stats, stats: Stats,
@ -117,27 +116,27 @@ export default function validate(
}); });
} }
const validator = new Validator(parsed, source, stats, { const validator = new Validator(ast, source, stats, {
name, name,
filename, filename,
dev, dev,
parser parser
}); });
if (parsed.js) { if (ast.js) {
validateJs(validator, parsed.js); validateJs(validator, ast.js);
} }
if (parsed.css) { if (ast.css) {
stylesheet.validate(validator); stylesheet.validate(validator);
} }
if (parsed.html) { if (ast.html) {
validateHtml(validator, parsed.html); validateHtml(validator, ast.html);
} }
// need to do a second pass of the JS, now that we've analysed the markup // need to do a second pass of the JS, now that we've analysed the markup
if (parsed.js && validator.defaultExport) { if (ast.js && validator.defaultExport) {
const categories = { const categories = {
components: 'component', components: 'component',
// TODO helpers require a bit more work — need to analyse all expressions // TODO helpers require a bit more work — need to analyse all expressions

@ -136,7 +136,7 @@ var proto = {
/* generated by Svelte vX.Y.Z */ /* generated by Svelte vX.Y.Z */
function link(node) { function link(node) {
function onClick(event) { function onClick(event) {
event.preventDefault(); event.preventDefault();
history.pushState(null, null, event.target.href); history.pushState(null, null, event.target.href);
@ -150,7 +150,7 @@ function link(node) {
} }
} }
} }
function create_main_fragment(component, state) { function create_main_fragment(component, ctx) {
var a, link_action; var a, link_action;
return { return {

@ -2,7 +2,7 @@
import { assign, createElement, detachNode, init, insertNode, noop, proto } from "svelte/shared.js"; import { assign, createElement, detachNode, init, insertNode, noop, proto } from "svelte/shared.js";
function link(node) { function link(node) {
function onClick(event) { function onClick(event) {
event.preventDefault(); event.preventDefault();
history.pushState(null, null, event.target.href); history.pushState(null, null, event.target.href);
@ -17,7 +17,7 @@ function link(node) {
} }
}; };
function create_main_fragment(component, state) { function create_main_fragment(component, ctx) {
var a, link_action; var a, link_action;
return { return {

@ -153,13 +153,13 @@ function add_css() {
appendNode(style, document.head); appendNode(style, document.head);
} }
function create_main_fragment(component, state) { function create_main_fragment(component, ctx) {
var p, text; var p, text;
return { return {
c: function create() { c: function create() {
p = createElement("p"); p = createElement("p");
text = createText(state.foo); text = createText(ctx.foo);
this.h(); this.h();
}, },
@ -172,9 +172,9 @@ function create_main_fragment(component, state) {
appendNode(text, p); appendNode(text, p);
}, },
p: function update(changed, state) { p: function update(changed, ctx) {
if (changed.foo) { if (changed.foo) {
text.data = state.foo; text.data = ctx.foo;
} }
}, },

@ -12,13 +12,13 @@ function add_css() {
appendNode(style, document.head); appendNode(style, document.head);
} }
function create_main_fragment(component, state) { function create_main_fragment(component, ctx) {
var p, text; var p, text;
return { return {
c: function create() { c: function create() {
p = createElement("p"); p = createElement("p");
text = createText(state.foo); text = createText(ctx.foo);
this.h(); this.h();
}, },
@ -31,9 +31,9 @@ function create_main_fragment(component, state) {
appendNode(text, p); appendNode(text, p);
}, },
p: function update(changed, state) { p: function update(changed, ctx) {
if (changed.foo) { if (changed.foo) {
text.data = state.foo; text.data = ctx.foo;
} }
}, },

@ -129,7 +129,7 @@ var proto = {
var Nested = window.Nested; var Nested = window.Nested;
function create_main_fragment(component, state) { function create_main_fragment(component, ctx) {
var nested_initial_data = { foo: "bar" }; var nested_initial_data = { foo: "bar" };
var nested = new Nested({ var nested = new Nested({

@ -3,7 +3,7 @@ import { _differsImmutable, assign, callAll, init, noop, proto } from "svelte/sh
var Nested = window.Nested; var Nested = window.Nested;
function create_main_fragment(component, state) { function create_main_fragment(component, ctx) {
var nested_initial_data = { foo: "bar" }; var nested_initial_data = { foo: "bar" };
var nested = new Nested({ var nested = new Nested({

@ -129,7 +129,7 @@ var proto = {
var Nested = window.Nested; var Nested = window.Nested;
function create_main_fragment(component, state) { function create_main_fragment(component, ctx) {
var nested_initial_data = { foo: "bar" }; var nested_initial_data = { foo: "bar" };
var nested = new Nested({ var nested = new Nested({

@ -3,7 +3,7 @@ import { _differsImmutable, assign, callAll, init, noop, proto } from "svelte/sh
var Nested = window.Nested; var Nested = window.Nested;
function create_main_fragment(component, state) { function create_main_fragment(component, ctx) {
var nested_initial_data = { foo: "bar" }; var nested_initial_data = { foo: "bar" };
var nested = new Nested({ var nested = new Nested({

@ -125,7 +125,7 @@ var proto = {
var Nested = window.Nested; var Nested = window.Nested;
function create_main_fragment(component, state) { function create_main_fragment(component, ctx) {
var nested_initial_data = { foo: "bar" }; var nested_initial_data = { foo: "bar" };
var nested = new Nested({ var nested = new Nested({

@ -3,7 +3,7 @@ import { assign, callAll, init, noop, proto } from "svelte/shared.js";
var Nested = window.Nested; var Nested = window.Nested;
function create_main_fragment(component, state) { function create_main_fragment(component, ctx) {
var nested_initial_data = { foo: "bar" }; var nested_initial_data = { foo: "bar" };
var nested = new Nested({ var nested = new Nested({

@ -131,7 +131,7 @@ function b({ x }) {
return x * 3; return x * 3;
} }
function create_main_fragment(component, state) { function create_main_fragment(component, ctx) {
return { return {
c: noop, c: noop,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save