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
node_modules
compiler
ssr
shared.js
scratch
!test/compiler
!test/ssr
.nyc_output
coverage
coverage.lcov
test/sourcemaps/samples/*/output.js
test/sourcemaps/samples/*/output.js.map
_actual.*
_actual-v2.*
_actual-bundle.*
src/generators/dom/shared.ts
package-lock.json
.idea/
*.iml
store.umd.js
yarn-error.log
node_modules
/compiler/
/ssr/
/shared.js
/scratch/
/coverage/
/coverage.lcov/
/test/sourcemaps/samples/*/output.js
/test/sourcemaps/samples/*/output.js.map
/src/compile/shared.ts
/package-lock.json
/store.umd.js
/yarn-error.log
_actual*.*

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

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

@ -1,3 +1,4 @@
import { parseExpressionAt } from 'acorn';
import MagicString, { Bundle } from 'magic-string';
import isReference from 'is-reference';
import { walk, childKeys } from 'estree-walker';
@ -13,11 +14,13 @@ import nodeToString from '../utils/nodeToString';
import wrapModule from './wrapModule';
import annotateWithScopes, { Scope } from '../utils/annotateWithScopes';
import getName from '../utils/getName';
import clone from '../utils/clone';
import Stylesheet from '../css/Stylesheet';
import { test } from '../config';
import nodes from './nodes/index';
import { Node, GenerateOptions, ShorthandImport, Parsed, CompileOptions, CustomElementOptions } from '../interfaces';
import Fragment from './nodes/Fragment';
import shared from './shared';
import { DomTarget } from './dom/index';
import { SsrTarget } from './ssr/index';
import { Node, GenerateOptions, ShorthandImport, Ast, CompileOptions, CustomElementOptions } from '../interfaces';
interface Computation {
key: string;
@ -75,14 +78,15 @@ function removeIndentation(
childKeys.EachBlock = childKeys.IfBlock = ['children', 'else'];
childKeys.Attribute = ['value'];
export default class Generator {
export default class Compiler {
stats: Stats;
ast: Parsed;
parsed: Parsed;
ast: Ast;
source: string;
name: string;
options: CompileOptions;
fragment: Fragment;
target: DomTarget | SsrTarget;
customElement: CustomElementOptions;
tag: string;
@ -122,22 +126,22 @@ export default class Generator {
usedNames: Set<string>;
constructor(
parsed: Parsed,
ast: Ast,
source: string,
name: string,
stylesheet: Stylesheet,
options: CompileOptions,
stats: Stats,
dom: boolean
dom: boolean,
target: DomTarget | SsrTarget
) {
stats.start('compile');
this.stats = stats;
this.ast = clone(parsed);
this.parsed = parsed;
this.ast = ast;
this.source = source;
this.options = options;
this.target = target;
this.imports = [];
this.shorthandImports = [];
@ -191,8 +195,11 @@ export default class Generator {
throw new Error(`No tag name specified`); // TODO better error
}
this.walkTemplate();
this.fragment = new Fragment(this, ast.html);
// this.walkTemplate();
if (!this.customElement) this.stylesheet.reify();
stylesheet.warnOnUnusedSelectors(options.onwarn);
}
addSourcemapLocations(node: Node) {
@ -212,112 +219,108 @@ export default class Generator {
return this.aliases.get(name);
}
contextualise(
contexts: Map<string, string>,
indexes: Map<string, string>,
expression: Node,
context: string,
isEventHandler: boolean
): {
contexts: Set<string>,
indexes: Set<string>
} {
// this.addSourcemapLocations(expression);
generate(result: string, options: CompileOptions, { banner = '', name, format }: GenerateOptions ) {
const pattern = /\[✂(\d+)-(\d+)$/;
const usedContexts: Set<string> = new Set();
const usedIndexes: Set<string> = new Set();
const helpers = new Set();
const { code, helpers } = this;
// TODO use same regex for both
result = result.replace(options.generate === 'ssr' ? /(@+|#+|%+)(\w*(?:-\w*)?)/g : /(%+|@+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
if (sigil === '@') {
if (name in shared) {
if (options.dev && `${name}Dev` in shared) name = `${name}Dev`;
helpers.add(name);
}
let scope: Scope;
let lexicalDepth = 0;
return this.alias(name);
}
const self = this;
if (sigil === '%') {
return this.templateVars.get(name);
}
walk(expression, {
enter(node: Node, parent: Node, key: string) {
if (/^Function/.test(node.type)) lexicalDepth += 1;
return sigil.slice(1) + name;
});
if (node._scope) {
scope = node._scope;
return;
}
let importedHelpers;
if (node.type === 'ThisExpression') {
if (lexicalDepth === 0 && context)
code.overwrite(node.start, node.end, context, {
storeName: true,
contentOnly: false,
});
} else if (isReference(node, parent)) {
const { name } = flattenReference(node);
if (scope && scope.has(name)) return;
if (name === 'event' && isEventHandler) {
// noop
} else if (contexts.has(name)) {
const contextName = contexts.get(name);
if (contextName !== name) {
// this is true for 'reserved' names like `state` and `component`,
// also destructured contexts
code.overwrite(
node.start,
node.start + name.length,
contextName,
{ storeName: true, contentOnly: false }
);
const destructuredName = contextName.replace(/\[\d+\]/, '');
if (destructuredName !== contextName) {
// so that hoisting the context works correctly
usedContexts.add(destructuredName);
}
}
if (options.shared) {
if (format !== 'es' && format !== 'cjs') {
throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``);
}
usedContexts.add(name);
} else if (helpers.has(name)) {
let object = node;
while (object.type === 'MemberExpression') object = object.object;
const alias = self.templateVars.get(`helpers-${name}`);
if (alias !== name) code.overwrite(object.start, object.end, alias);
} else if (indexes.has(name)) {
const context = indexes.get(name);
usedContexts.add(context); // TODO is this right?
usedIndexes.add(name);
} else {
// handle shorthand properties
if (parent && parent.type === 'Property' && parent.shorthand) {
if (key === 'key') {
code.appendLeft(node.start, `${name}: `);
return;
importedHelpers = Array.from(helpers).sort().map(name => {
const alias = this.alias(name);
return { name, alias };
});
} else {
let inlineHelpers = '';
const compiler = this;
importedHelpers = [];
helpers.forEach(name => {
const str = shared[name];
const code = new MagicString(str);
const expression = parseExpressionAt(str, 0);
let { scope } = annotateWithScopes(expression);
walk(expression, {
enter(node: Node, parent: Node) {
if (node._scope) scope = node._scope;
if (
node.type === 'Identifier' &&
isReference(node, parent) &&
!scope.has(node.name)
) {
if (node.name in shared) {
// this helper function depends on another one
const dependency = node.name;
helpers.add(dependency);
const alias = compiler.alias(dependency);
if (alias !== node.name) {
code.overwrite(node.start, node.end, alias);
}
}
}
},
leave(node: Node) {
if (node._scope) scope = scope.parent;
},
});
if (name === 'transitionManager') {
// special case
const global = `_svelteTransitionManager`;
code.prependRight(node.start, `state.`);
usedContexts.add('state');
inlineHelpers += `\n\nvar ${this.alias('transitionManager')} = window.${global} || (window.${global} = ${code});\n\n`;
} else if (name === 'escaped' || name === 'missingComponent') {
// vars are an awkward special case... would be nice to avoid this
const alias = this.alias(name);
inlineHelpers += `\n\nconst ${alias} = ${code};`
} else {
const alias = this.alias(expression.id.name);
if (alias !== expression.id.name) {
code.overwrite(expression.id.start, expression.id.end, alias);
}
this.skip();
inlineHelpers += `\n\n${code}`;
}
},
leave(node: Node) {
if (/^Function/.test(node.type)) lexicalDepth -= 1;
if (node._scope) scope = scope.parent;
},
});
});
return {
contexts: usedContexts,
indexes: usedIndexes
};
}
result += inlineHelpers;
}
generate(result: string, options: CompileOptions, { banner = '', sharedPath, helpers, name, format }: GenerateOptions ) {
const pattern = /\[✂(\d+)-(\d+)$/;
const sharedPath = options.shared === true
? 'svelte/shared.js'
: options.shared || '';
const module = wrapModule(result, format, name, options, banner, sharedPath, helpers, this.imports, this.shorthandImports, this.source);
const module = wrapModule(result, format, name, options, banner, sharedPath, importedHelpers, this.imports, this.shorthandImports, this.source);
const parts = module.split('✂]');
const finalChunk = parts.pop();
@ -393,7 +396,7 @@ export default class Generator {
return alias;
}
getUniqueNameMaker(names: string[]) {
getUniqueNameMaker() {
const localUsedNames = new Set();
function add(name: string) {
@ -402,7 +405,6 @@ export default class Generator {
reservedNames.forEach(add);
this.userVars.forEach(add);
names.forEach(add);
return (name: string) => {
if (test) name = `${name}$`;
@ -428,7 +430,7 @@ export default class Generator {
imports
} = this;
const { js } = this.parsed;
const { js } = this.ast;
const componentDefinition = new CodeBuilder();
@ -703,213 +705,4 @@ export default class Generator {
}
}
}
walkTemplate() {
const generator = this;
const {
code,
expectedProperties,
helpers
} = this;
const { html } = this.parsed;
const contextualise = (
node: Node, contextDependencies: Map<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 deindent from '../../utils/deindent';
import { escape } from '../../utils/stringify';
import { DomGenerator } from './index';
import Compiler from '../Compiler';
import { Node } from '../../interfaces';
import shared from './shared';
export interface BlockOptions {
name: string;
generator?: DomGenerator;
expression?: Node;
context?: string;
destructuredContexts?: string[];
compiler?: Compiler;
comment?: string;
key?: string;
contexts?: Map<string, string>;
contextTypes?: Map<string, string>;
indexes?: Map<string, string>;
changeableIndexes?: Map<string, boolean>;
indexNames?: Map<string, string>;
listNames?: Map<string, string>;
dependencies?: Set<string>;
}
export default class Block {
generator: DomGenerator;
compiler: Compiler;
name: string;
expression: Node;
context: string;
destructuredContexts?: string[];
comment?: string;
key: string;
first: string;
contexts: Map<string, string>;
contextTypes: Map<string, string>;
indexes: Map<string, string>;
changeableIndexes: Map<string, boolean>;
dependencies: Set<string>;
indexNames: Map<string, string>;
listNames: Map<string, string>;
@ -67,21 +52,14 @@ export default class Block {
autofocus: string;
constructor(options: BlockOptions) {
this.generator = options.generator;
this.compiler = options.compiler;
this.name = options.name;
this.expression = options.expression;
this.context = options.context;
this.destructuredContexts = options.destructuredContexts;
this.comment = options.comment;
// for keyed each blocks
this.key = options.key;
this.first = null;
this.contexts = options.contexts;
this.contextTypes = options.contextTypes;
this.indexes = options.indexes;
this.changeableIndexes = options.changeableIndexes;
this.dependencies = new Set();
this.indexNames = options.indexNames;
@ -105,18 +83,18 @@ export default class Block {
this.hasOutroMethod = false;
this.outros = 0;
this.getUniqueName = this.generator.getUniqueNameMaker([...this.contexts.values()]);
this.getUniqueName = this.compiler.getUniqueNameMaker();
this.variables = new Map();
this.aliases = new Map()
.set('component', this.getUniqueName('component'))
.set('state', this.getUniqueName('state'));
.set('ctx', this.getUniqueName('ctx'));
if (this.key) this.aliases.set('key', this.getUniqueName('key'));
this.hasUpdateMethod = false; // determined later
}
addDependencies(dependencies: string[]) {
addDependencies(dependencies: Set<string>) {
dependencies.forEach(dependency => {
this.dependencies.add(dependency);
});
@ -163,10 +141,6 @@ export default class Block {
return new Block(Object.assign({}, this, { key: null }, options, { parent: this }));
}
contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler);
}
toString() {
let introing;
const hasIntros = !this.builders.intro.isEmpty();
@ -186,23 +160,6 @@ export default class Block {
this.builders.mount.addLine(`${this.autofocus}.focus();`);
}
// TODO `this.contexts` is possibly redundant post-#1122
const initializers = [];
this.contexts.forEach((name, context) => {
// TODO only the ones that are actually used in this block...
const listName = this.listNames.get(context);
const indexName = this.indexNames.get(context);
initializers.push(
`${name} = state.${context}`,
`${listName} = state.${listName}`,
`${indexName} = state.${indexName}`
);
this.hasUpdateMethod = true;
});
// minor hack we need to ensure that any {{{triples}}} are detached first
this.builders.unmount.addBlockAtStart(this.builders.detachRaw.toString());
@ -230,7 +187,7 @@ export default class Block {
`);
}
if (this.generator.hydratable) {
if (this.compiler.options.hydratable) {
if (this.builders.claim.isEmpty() && this.builders.hydrate.isEmpty()) {
properties.addBlock(`l: @noop,`);
} else {
@ -261,13 +218,13 @@ export default class Block {
`);
}
if (this.hasUpdateMethod) {
if (this.builders.update.isEmpty() && initializers.length === 0) {
if (this.hasUpdateMethod || this.maintainContext) {
if (this.builders.update.isEmpty() && !this.maintainContext) {
properties.addBlock(`p: @noop,`);
} else {
properties.addBlock(deindent`
p: function update(changed, state) {
${initializers.map(str => `${str};`)}
p: function update(changed, ${this.maintainContext ? '_ctx' : 'ctx'}) {
${this.maintainContext && `ctx = _ctx;`}
${this.builders.update}
},
`);
@ -338,9 +295,7 @@ export default class Block {
return deindent`
${this.comment && `// ${escape(this.comment)}`}
function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, state) {
${initializers.length > 0 &&
`var ${initializers.join(', ')};`}
function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, ctx) {
${this.variables.size > 0 &&
`var ${Array.from(this.variables.keys())
.map(key => {

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

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

@ -1,21 +1,36 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import { DomGenerator } from '../dom/index';
import Block from '../dom/Block';
import PendingBlock from './PendingBlock';
import ThenBlock from './ThenBlock';
import CatchBlock from './CatchBlock';
import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression';
import { SsrTarget } from '../ssr';
export default class AwaitBlock extends Node {
expression: Expression;
value: string;
error: string;
expression: Node;
pending: PendingBlock;
then: ThenBlock;
catch: CatchBlock;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.expression = new Expression(compiler, this, scope, info.expression);
const deps = this.expression.dependencies;
this.value = info.value;
this.error = info.error;
this.pending = new PendingBlock(compiler, this, scope, info.pending);
this.then = new ThenBlock(compiler, this, scope.add(this.value, deps), info.then);
this.catch = new CatchBlock(compiler, this, scope.add(this.error, deps), info.catch);
}
init(
block: Block,
stripWhitespace: boolean,
@ -24,42 +39,30 @@ export default class AwaitBlock extends Node {
this.cannotUseInnerHTML();
this.var = block.getUniqueName('await_block');
block.addDependencies(this.metadata.dependencies);
block.addDependencies(this.expression.dependencies);
let dynamic = false;
let isDynamic = false;
[
['pending', null],
['then', this.value],
['catch', this.error]
].forEach(([status, arg]) => {
['pending', 'then', 'catch'].forEach(status => {
const child = this[status];
child.block = block.child({
comment: createDebuggingComment(child, this.generator),
name: this.generator.getUniqueName(`create_${status}_block`),
contexts: new Map(block.contexts),
contextTypes: new Map(block.contextTypes)
comment: createDebuggingComment(child, this.compiler),
name: this.compiler.getUniqueName(`create_${status}_block`)
});
if (arg) {
child.block.context = arg;
child.block.contexts.set(arg, arg); // TODO should be using getUniqueName
child.block.contextTypes.set(arg, status);
}
child.initChildren(child.block, stripWhitespace, nextSibling);
this.generator.blocks.push(child.block);
this.compiler.target.blocks.push(child.block);
if (child.block.dependencies.size > 0) {
dynamic = true;
isDynamic = true;
block.addDependencies(child.block.dependencies);
}
});
this.pending.block.hasUpdateMethod = dynamic;
this.then.block.hasUpdateMethod = dynamic;
this.catch.block.hasUpdateMethod = dynamic;
this.pending.block.hasUpdateMethod = isDynamic;
this.then.block.hasUpdateMethod = isDynamic;
this.catch.block.hasUpdateMethod = isDynamic;
}
build(
@ -72,8 +75,7 @@ export default class AwaitBlock extends Node {
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
const updateMountNode = this.getUpdateMountNode(anchor);
block.contextualise(this.expression);
const { snippet } = this.metadata;
const { snippet } = this.expression;
const promise = block.getUniqueName(`promise`);
const resolved = block.getUniqueName(`resolved`);
@ -101,11 +103,11 @@ export default class AwaitBlock extends Node {
// but it's probably not worth it
block.builders.init.addBlock(deindent`
function ${replace_await_block}(${token}, type, state) {
function ${replace_await_block}(${token}, type, ctx) {
if (${token} !== ${await_token}) return;
var ${old_block} = ${await_block};
${await_block} = type && (${await_block_type} = type)(#component, state);
${await_block} = type && (${await_block_type} = type)(#component, ctx);
if (${old_block}) {
${old_block}.u();
@ -117,23 +119,23 @@ export default class AwaitBlock extends Node {
}
}
function ${handle_promise}(${promise}, state) {
function ${handle_promise}(${promise}, ctx) {
var ${token} = ${await_token} = {};
if (@isPromise(${promise})) {
${promise}.then(function(${value}) {
${this.then.block.context ? deindent`
var state = #component.get();
${resolved} = { ${this.then.block.context}: ${value} };
${replace_await_block}(${token}, ${create_then_block}, @assign(@assign({}, state), ${resolved}));
${this.value ? deindent`
var ctx = #component.get();
${resolved} = { ${this.value}: ${value} };
${replace_await_block}(${token}, ${create_then_block}, @assign(@assign({}, ctx), ${resolved}));
` : deindent`
${replace_await_block}(${token}, null, null);
`}
}, function (${error}) {
${this.catch.block.context ? deindent`
var state = #component.get();
${resolved} = { ${this.catch.block.context}: ${error} };
${replace_await_block}(${token}, ${create_catch_block}, @assign(@assign({}, state), ${resolved}));
${this.error ? deindent`
var ctx = #component.get();
${resolved} = { ${this.error}: ${error} };
${replace_await_block}(${token}, ${create_catch_block}, @assign(@assign({}, ctx), ${resolved}));
` : deindent`
${replace_await_block}(${token}, null, null);
`}
@ -141,19 +143,19 @@ export default class AwaitBlock extends Node {
// if we previously had a then/catch block, destroy it
if (${await_block_type} !== ${create_pending_block}) {
${replace_await_block}(${token}, ${create_pending_block}, state);
${replace_await_block}(${token}, ${create_pending_block}, ctx);
return true;
}
} else {
${resolved} = { ${this.then.block.context}: ${promise} };
${resolved} = { ${this.value}: ${promise} };
if (${await_block_type} !== ${create_then_block}) {
${replace_await_block}(${token}, ${create_then_block}, @assign(@assign({}, state), ${resolved}));
${replace_await_block}(${token}, ${create_then_block}, @assign(@assign({}, ctx), ${resolved}));
return true;
}
}
}
${handle_promise}(${promise} = ${snippet}, state);
${handle_promise}(${promise} = ${snippet}, ctx);
`);
block.builders.create.addBlock(deindent`
@ -174,15 +176,15 @@ export default class AwaitBlock extends Node {
`);
const conditions = [];
if (this.metadata.dependencies) {
if (this.expression.dependencies.size > 0) {
conditions.push(
`(${this.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})`
`(${[...this.expression.dependencies].map(dep => `'${dep}' in changed`).join(' || ')})`
);
}
conditions.push(
`${promise} !== (${promise} = ${snippet})`,
`${handle_promise}(${promise}, state)`
`${handle_promise}(${promise}, ctx)`
);
if (this.pending.block.hasUpdateMethod) {
@ -190,7 +192,7 @@ export default class AwaitBlock extends Node {
if (${conditions.join(' && ')}) {
// nothing
} else {
${await_block}.p(changed, @assign(@assign({}, state), ${resolved}));
${await_block}.p(changed, @assign(@assign({}, ctx), ${resolved}));
}
`);
} else {
@ -217,4 +219,23 @@ export default class AwaitBlock extends Node {
});
});
}
ssr() {
const target: SsrTarget = <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 getTailSnippet from '../../utils/getTailSnippet';
import flattenReference from '../../utils/flattenReference';
import { DomGenerator } from '../dom/index';
import Compiler from '../Compiler';
import Block from '../dom/Block';
import Expression from './shared/Expression';
const readOnlyMediaAttributes = new Set([
'duration',
@ -15,12 +16,43 @@ const readOnlyMediaAttributes = new Set([
export default class Binding extends Node {
name: string;
value: Node;
expression: Node;
value: Expression;
isContextual: boolean;
usesContext: boolean;
obj: string;
prop: string;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.name = info.name;
this.value = new Expression(compiler, this, scope, info.value);
let obj;
let prop;
const { name } = getObject(this.value.node);
this.isContextual = scope.names.has(name);
if (this.value.node.type === 'MemberExpression') {
prop = `[✂${this.value.node.property.start}-${this.value.node.property.end}✂]`;
if (!this.value.node.computed) prop = `'${prop}'`;
obj = `[✂${this.value.node.object.start}-${this.value.node.object.end}✂]`;
this.usesContext = true;
} else {
obj = 'ctx';
prop = `'${name}'`;
this.usesContext = scope.names.has(name);
}
this.obj = obj;
this.prop = prop;
}
munge(
block: Block,
allUsedContexts: Set<string>
block: Block
) {
const node: Element = this.parent;
@ -29,32 +61,27 @@ export default class Binding extends Node {
let updateCondition: string;
const { name } = getObject(this.value);
const { contexts } = block.contextualise(this.value);
const { snippet } = this.metadata;
const { name } = getObject(this.value.node);
const { snippet } = this.value;
// special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>`
// 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
// pointing to
// TODO should this happen in preprocess?
const dependencies = this.metadata.dependencies.slice();
this.metadata.dependencies.forEach((prop: string) => {
const indirectDependencies = this.generator.indirectDependencies.get(prop);
const dependencies = new Set(this.value.dependencies);
this.value.dependencies.forEach((prop: string) => {
const indirectDependencies = this.compiler.indirectDependencies.get(prop);
if (indirectDependencies) {
indirectDependencies.forEach(indirectDependency => {
if (!~dependencies.indexOf(indirectDependency)) dependencies.push(indirectDependency);
dependencies.add(indirectDependency);
});
}
});
contexts.forEach(context => {
allUsedContexts.add(context);
});
// view to model
const valueFromDom = getValueFromDom(this.generator, node, this);
const handler = getEventHandler(this.generator, block, name, snippet, this, dependencies, valueFromDom);
const valueFromDom = getValueFromDom(this.compiler, node, this);
const handler = getEventHandler(this, this.compiler, block, name, snippet, dependencies, valueFromDom);
// model to view
let updateDom = getDomUpdater(node, this, snippet);
@ -62,7 +89,7 @@ export default class Binding extends Node {
// special cases
if (this.name === 'group') {
const bindingGroup = getBindingGroup(this.generator, this.value);
const bindingGroup = getBindingGroup(this.compiler, this.value.node);
block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
@ -135,67 +162,68 @@ function getDomUpdater(
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 keypath = parts.join('.');
// TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context
let index = generator.bindingGroups.indexOf(keypath);
let index = compiler.bindingGroups.indexOf(keypath);
if (index === -1) {
index = generator.bindingGroups.length;
generator.bindingGroups.push(keypath);
index = compiler.bindingGroups.length;
compiler.bindingGroups.push(keypath);
}
return index;
}
function getEventHandler(
generator: DomGenerator,
binding: Binding,
compiler: Compiler,
block: Block,
name: string,
snippet: string,
attribute: Node,
dependencies: string[],
value: string,
isContextual: boolean
) {
const storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1));
dependencies = dependencies.filter(prop => prop[0] !== '$');
const storeDependencies = [...dependencies].filter(prop => prop[0] === '$').map(prop => prop.slice(1));
dependencies = [...dependencies].filter(prop => prop[0] !== '$');
if (block.contexts.has(name)) {
const tail = attribute.value.type === 'MemberExpression'
? getTailSnippet(attribute.value)
if (binding.isContextual) {
const tail = binding.value.node.type === 'MemberExpression'
? getTailSnippet(binding.value.node)
: '';
const list = `context.${block.listNames.get(name)}`;
const index = `context.${block.indexNames.get(name)}`;
const list = `ctx.${block.listNames.get(name)}`;
const index = `ctx.${block.indexNames.get(name)}`;
return {
usesContext: true,
usesState: true,
usesStore: storeDependencies.length > 0,
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}`)
};
}
if (attribute.value.type === 'MemberExpression') {
if (binding.value.node.type === 'MemberExpression') {
// This is a little confusing, and should probably be tidied up
// at some point. It addresses a tricky bug (#893), wherein
// Svelte tries to `set()` a computed property, which throws an
// error in dev mode. a) it's possible that we should be
// 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
dependencies = dependencies.filter(prop => !generator.computations.some(computation => computation.key === prop));
dependencies = dependencies.filter(prop => !compiler.computations.some(computation => computation.key === prop));
return {
usesContext: false,
usesState: true,
usesStore: storeDependencies.length > 0,
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}`)
};
}
@ -222,7 +250,7 @@ function getEventHandler(
}
function getValueFromDom(
generator: DomGenerator,
compiler: Compiler,
node: Element,
binding: Node
) {
@ -237,7 +265,7 @@ function getValueFromDom(
// <input type='checkbox' bind:group='foo'>
if (binding.name === 'group') {
const bindingGroup = getBindingGroup(generator, binding.value);
const bindingGroup = getBindingGroup(compiler, binding.value.node);
if (type === 'checkbox') {
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
}

@ -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 Block from '../dom/Block';
import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression';
import mapChildren from './shared/mapChildren';
import TemplateScope from './shared/TemplateScope';
export default class EachBlock extends Node {
type: 'EachBlock';
block: Block;
expression: Node;
expression: Expression;
iterations: string;
index: string;
context: string;
key: string;
scope: TemplateScope;
destructuredContexts: string[];
children: Node[];
else?: ElseBlock;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.expression = new Expression(compiler, this, scope, info.expression);
this.context = info.context;
this.index = info.index;
this.key = info.key;
this.scope = scope.child();
this.scope.add(this.context, this.expression.dependencies);
if (this.index) {
// index can only change if this is a keyed each block
const dependencies = this.key ? this.expression.dependencies : [];
this.scope.add(this.index, dependencies);
}
// TODO more general approach to destructuring
this.destructuredContexts = info.destructuredContexts || [];
this.destructuredContexts.forEach(name => {
this.scope.add(name, this.expression.dependencies);
});
this.children = mapChildren(compiler, this, this.scope, info.children);
this.else = info.else
? new ElseBlock(compiler, this, this.scope, info.else)
: null;
}
init(
block: Block,
stripWhitespace: boolean,
@ -30,45 +65,26 @@ export default class EachBlock extends Node {
this.iterations = block.getUniqueName(`${this.var}_blocks`);
this.each_context = block.getUniqueName(`${this.var}_context`);
const { dependencies } = this.metadata;
const { dependencies } = this.expression;
block.addDependencies(dependencies);
this.block = block.child({
comment: createDebuggingComment(this, this.generator),
name: this.generator.getUniqueName('create_each_block'),
context: this.context,
comment: createDebuggingComment(this, this.compiler),
name: this.compiler.getUniqueName('create_each_block'),
key: this.key,
contexts: new Map(block.contexts),
contextTypes: new Map(block.contextTypes),
indexes: new Map(block.indexes),
changeableIndexes: new Map(block.changeableIndexes),
indexNames: new Map(block.indexNames),
listNames: new Map(block.listNames)
});
const listName = this.generator.getUniqueName('each_value');
const indexName = this.index || this.generator.getUniqueName(`${this.context}_index`);
const listName = this.compiler.getUniqueName('each_value');
const indexName = this.index || this.compiler.getUniqueName(`${this.context}_index`);
this.block.contextTypes.set(this.context, 'each');
this.block.indexNames.set(this.context, indexName);
this.block.listNames.set(this.context, listName);
if (this.index) {
this.block.getUniqueName(this.index); // this prevents name collisions (#1254)
this.block.indexes.set(this.index, this.context);
this.block.changeableIndexes.set(this.index, this.key); // TODO is this right?
}
const context = this.block.getUniqueName(this.context);
this.block.contexts.set(this.context, context); // TODO this is now redundant?
if (this.destructuredContexts) {
for (let i = 0; i < this.destructuredContexts.length; i += 1) {
const context = this.block.getUniqueName(this.destructuredContexts[i]);
this.block.contexts.set(this.destructuredContexts[i], context);
}
}
this.contextProps = [
@ -83,18 +99,18 @@ export default class EachBlock extends Node {
}
}
this.generator.blocks.push(this.block);
this.compiler.target.blocks.push(this.block);
this.initChildren(this.block, stripWhitespace, nextSibling);
block.addDependencies(this.block.dependencies);
this.block.hasUpdateMethod = this.block.dependencies.size > 0;
if (this.else) {
this.else.block = block.child({
comment: createDebuggingComment(this.else, this.generator),
name: this.generator.getUniqueName(`${this.block.name}_else`),
comment: createDebuggingComment(this.else, this.compiler),
name: this.compiler.getUniqueName(`${this.block.name}_else`),
});
this.generator.blocks.push(this.else.block);
this.compiler.target.blocks.push(this.else.block);
this.else.initChildren(
this.else.block,
stripWhitespace,
@ -111,7 +127,7 @@ export default class EachBlock extends Node {
) {
if (this.children.length === 0) return;
const { generator } = this;
const { compiler } = this;
const each = this.var;
@ -127,8 +143,8 @@ export default class EachBlock extends Node {
// hack the sourcemap, so that if data is missing the bug
// is easy to find
let c = this.start + 2;
while (generator.source[c] !== 'e') c += 1;
generator.code.overwrite(c, c + 4, 'length');
while (compiler.source[c] !== 'e') c += 1;
compiler.code.overwrite(c, c + 4, 'length');
const length = `[✂${c}-${c+4}✂]`;
const mountOrIntro = this.block.hasIntroMethod ? 'i' : 'm';
@ -142,8 +158,7 @@ export default class EachBlock extends Node {
mountOrIntro,
};
block.contextualise(this.expression);
const { snippet } = this.metadata;
const { snippet } = this.expression;
block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
@ -163,14 +178,14 @@ export default class EachBlock extends Node {
}
if (this.else) {
const each_block_else = generator.getUniqueName(`${each}_else`);
const each_block_else = compiler.getUniqueName(`${each}_else`);
block.builders.init.addLine(`var ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block
block.builders.init.addBlock(deindent`
if (!${each_block_value}.${length}) {
${each_block_else} = ${this.else.block.name}(#component, state);
${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c();
}
`);
@ -186,9 +201,9 @@ export default class EachBlock extends Node {
if (this.else.block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if (!${each_block_value}.${length} && ${each_block_else}) {
${each_block_else}.p(changed, state);
${each_block_else}.p(changed, ctx);
} else if (!${each_block_value}.${length}) {
${each_block_else} = ${this.else.block.name}(#component, state);
${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
} else if (${each_block_else}) {
@ -206,7 +221,7 @@ export default class EachBlock extends Node {
${each_block_else} = null;
}
} else if (!${each_block_else}) {
${each_block_else} = ${this.else.block.name}(#component, state);
${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
}
@ -269,7 +284,7 @@ export default class EachBlock extends Node {
block.builders.init.addBlock(deindent`
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
var ${key} = ${each_block_value}[#i].${this.key};
${blocks}[#i] = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, @assign(@assign({}, state), {
${blocks}[#i] = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, @assign(@assign({}, ctx), {
${this.contextProps.join(',\n')}
}));
}
@ -299,7 +314,7 @@ export default class EachBlock extends Node {
var ${each_block_value} = ${snippet};
${blocks} = @updateKeyedEach(${blocks}, #component, changed, "${this.key}", ${dynamic ? '1' : '0'}, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, function(#i) {
return @assign(@assign({}, state), {
return @assign(@assign({}, ctx), {
${this.contextProps.join(',\n')}
});
});
@ -334,7 +349,7 @@ export default class EachBlock extends Node {
var ${iterations} = [];
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
${iterations}[#i] = ${create_each_block}(#component, @assign(@assign({}, state), {
${iterations}[#i] = ${create_each_block}(#component, @assign(@assign({}, ctx), {
${this.contextProps.join(',\n')}
}));
}
@ -365,7 +380,7 @@ export default class EachBlock extends Node {
`);
const allDependencies = new Set(this.block.dependencies);
const { dependencies } = this.metadata;
const { dependencies } = this.expression;
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
@ -432,7 +447,7 @@ export default class EachBlock extends Node {
if (${condition}) {
for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) {
var ${this.each_context} = @assign(@assign({}, state), {
var ${this.each_context} = @assign(@assign({}, ctx), {
${this.contextProps.join(',\n')}
});
@ -457,4 +472,36 @@ export default class EachBlock extends Node {
// TODO consider keyed blocks
return `for (var #i = 0; #i < ${this.iterations}.length; #i += 1) ${this.iterations}[#i].m(${name}._slotted.default, null);`;
}
ssr() {
const { compiler } = this;
const { snippet } = this.expression;
const props = [`${this.context}: item`]
.concat(this.destructuredContexts.map((name, i) => `${name}: item[${i}]`));
const getContext = this.index
? `(item, i) => Object.assign({}, ctx, { ${props.join(', ')}, ${this.index}: i })`
: `item => Object.assign({}, ctx, { ${props.join(', ')} })`;
const open = `\${ ${this.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${getContext}, ctx => \``;
compiler.target.append(open);
this.children.forEach((child: Node) => {
child.ssr();
});
const close = `\`)`;
compiler.target.append(close);
if (this.else) {
compiler.target.append(` : \``);
this.else.children.forEach((child: Node) => {
child.ssr();
});
compiler.target.append(`\``);
}
compiler.target.append('}');
}
}

@ -6,24 +6,132 @@ import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames';
import fixAttributeCasing from '../../utils/fixAttributeCasing';
import quoteIfNecessary from '../../utils/quoteIfNecessary';
import mungeAttribute from './shared/mungeAttribute';
import Compiler from '../Compiler';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
import Binding from './Binding';
import EventHandler from './EventHandler';
import Ref from './Ref';
import Transition from './Transition';
import Action from './Action';
import Text from './Text';
import * as namespaces from '../../utils/namespaces';
import mapChildren from './shared/mapChildren';
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
const booleanAttributes = new Set('async autocomplete autofocus autoplay border challenge checked compact contenteditable controls default defer disabled formnovalidate frameborder hidden indeterminate ismap loop multiple muted nohref noresize noshade novalidate nowrap open readonly required reversed scoped scrolling seamless selected sortable spellcheck translate'.split(' '));
export default class Element extends Node {
type: 'Element';
name: string;
attributes: (Attribute | Binding | EventHandler | Ref | Transition | Action)[]; // TODO split these up sooner
scope: any; // TODO
attributes: Attribute[];
actions: Action[];
bindings: Binding[];
handlers: EventHandler[];
intro: Transition;
outro: Transition;
children: Node[];
ref: string;
namespace: string;
constructor(compiler, parent, scope, info: any) {
super(compiler, parent, scope, info);
this.name = info.name;
this.scope = scope;
const parentElement = parent.findNearest(/^Element/);
this.namespace = this.name === 'svg' ?
namespaces.svg :
parentElement ? parentElement.namespace : this.compiler.namespace;
this.attributes = [];
this.actions = [];
this.bindings = [];
this.handlers = [];
this.intro = null;
this.outro = null;
if (this.name === 'textarea') {
// this is an egregious hack, but it's the easiest way to get <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(
block: Block,
stripWhitespace: boolean,
@ -33,103 +141,75 @@ export default class Element extends Node {
this.cannotUseInnerHTML();
}
const parentElement = this.parent && this.parent.findNearest(/^Element/);
this.namespace = this.name === 'svg' ?
namespaces.svg :
parentElement ? parentElement.namespace : this.generator.namespace;
this.attributes.forEach(attribute => {
if (attribute.type === 'Attribute' && attribute.value !== true) {
// special case — xmlns
if (attribute.name === 'xmlns') {
// TODO this attribute must be static enforce at compile time
this.namespace = attribute.value[0].data;
}
attribute.value.forEach((chunk: Node) => {
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;
this.attributes.forEach(attr => {
if (attr.dependencies.size) {
this.parent.cannotUseInnerHTML();
block.addDependencies(attr.dependencies);
// special case — <option value={foo}> — see below
if (this.name === 'option' && attr.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 => {
attr.dependencies.forEach((dependency: string) => {
this.compiler.indirectDependencies.get(prop).add(dependency);
});
});
}
} 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 is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
if (this.children.length > 0) {
this.attributes.push(new Attribute({
generator: this.generator,
name: 'value',
value: this.children,
parent: this
}));
this.bindings.forEach(binding => {
this.parent.cannotUseInnerHTML();
block.addDependencies(binding.value.dependencies);
});
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...
//
// <select bind:value='foo'>
// <option value='{{bar}}'>bar</option>
// <option value='{{baz}}'>baz</option>
// <option value='{bar}'>bar</option>
// <option value='{baz}'>baz</option>
// </option>
//
// ...we need to know that `foo` depends on `bar` and `baz`,
// so that if `foo.qux` changes, we know that we need to
// mark `bar` and `baz` as dirty too
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) {
// 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;
dependencies.forEach((prop: string) => {
this.generator.indirectDependencies.set(prop, new Set());
this.compiler.indirectDependencies.set(prop, new Set());
});
} else {
this.selectBindingDependencies = null;
@ -145,10 +225,6 @@ export default class Element extends Node {
component._slots.add(slot);
}
if (this.spread) {
block.addDependencies(this.spread.metadata.dependencies);
}
this.var = block.getUniqueName(
this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
);
@ -164,11 +240,11 @@ export default class Element extends Node {
parentNode: string,
parentNodes: string
) {
const { generator } = this;
const { compiler } = this;
if (this.name === 'slot') {
const slotName = this.getStaticAttributeValue('name') || 'default';
this.generator.slots.add(slotName);
this.compiler.slots.add(slotName);
}
if (this.name === 'noscript') return;
@ -179,23 +255,22 @@ export default class Element extends Node {
};
const name = this.var;
const allUsedContexts: Set<string> = new Set();
const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot');
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;
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(
`${name} = ${renderStatement};`
);
if (this.generator.hydratable) {
if (this.compiler.options.hydratable) {
if (parentNodes) {
block.builders.claim.addBlock(deindent`
${name} = ${getClaimStatement(generator, this.namespace, parentNodes, this)};
${name} = ${getClaimStatement(compiler, this.namespace, parentNodes, this)};
var ${childState.parentNodes} = @children(${name});
`);
} else {
@ -245,49 +320,52 @@ export default class Element extends Node {
});
}
this.addBindings(block, allUsedContexts);
const eventHandlerUsesComponent = this.addEventHandlers(block, allUsedContexts);
this.addRefs(block);
this.addAttributes(block);
this.addTransitions(block);
this.addActions(block);
let hasHoistedEventHandlerOrBinding = (
//(this.hasAncestor('EachBlock') && this.bindings.length > 0) ||
this.handlers.some(handler => handler.shouldHoist)
);
const eventHandlerOrBindingUsesComponent = (
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 updates: string[] = [];
if (eventHandlerUsesComponent) {
initialProps.push(`component: #component`);
if (eventHandlerOrBindingUsesComponent) {
const component = block.alias('component');
initialProps.push(component === 'component' ? 'component' : `component: ${component}`);
}
allUsedContexts.forEach((contextName: string) => {
if (contextName === 'state') return;
if (block.contextTypes.get(contextName) !== 'each') return;
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 (eventHandlerOrBindingUsesContext) {
initialProps.push(`ctx`);
block.builders.update.addLine(`${name}._svelte.ctx = ctx;`);
}
if (initialProps.length) {
block.builders.hydrate.addBlock(deindent`
${name}._svelte = {
${initialProps.join(',\n')}
};
${name}._svelte = { ${initialProps.join(', ')} };
`);
}
if (updates.length) {
block.builders.update.addBlock(updates.join('\n'));
} else {
if (eventHandlerOrBindingUsesContext) {
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) {
block.builders.mount.addBlock(this.initialUpdate);
}
@ -316,7 +394,7 @@ export default class Element extends 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 + '>';
@ -326,17 +404,16 @@ export default class Element extends Node {
}
addBindings(
block: Block,
allUsedContexts: Set<string>
block: Block
) {
const bindings: Binding[] = this.attributes.filter((a: Binding) => a.type === 'Binding');
if (bindings.length === 0) return;
if (this.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 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) ?
block.getUniqueName(`${this.var}_updating`) :
@ -402,8 +479,6 @@ export default class Element extends Node {
cancelAnimationFrame(${animation_frame});
if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
}
${usesContext && `var context = ${this.var}._svelte;`}
${usesState && `var state = #component.get();`}
${usesStore && `var $ = #component.store.get();`}
${needsLock && `${lock} = true;`}
${mutations.length > 0 && mutations}
@ -424,11 +499,11 @@ export default class Element extends Node {
});
const allInitialStateIsDefined = group.bindings
.map(binding => `'${binding.object}' in state`)
.map(binding => `'${binding.object}' in ctx`)
.join(' && ');
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(
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
@ -445,7 +520,7 @@ export default class Element extends Node {
return;
}
this.attributes.filter((a: Attribute) => a.type === 'Attribute').forEach((attribute: Attribute) => {
this.attributes.forEach((attribute: Attribute) => {
attribute.render(block);
});
}
@ -460,23 +535,20 @@ export default class Element extends Node {
this.attributes
.filter(attr => attr.type === 'Attribute' || attr.type === 'Spread')
.forEach(attr => {
if (attr.type === 'Attribute') {
const { dynamic, value, dependencies } = mungeAttribute(attr, block);
const condition = attr.dependencies.size > 0
? [...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);
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
updates.push(condition ? `${condition} && ${snippet}` : snippet);
}
else {
block.contextualise(attr.expression); // TODO gah
const { snippet, dependencies } = attr.metadata;
} else {
const snippet = `{ ${quoteIfNecessary(attr.name)}: ${attr.getValue()} }`;
initialProps.push(snippet);
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
updates.push(condition ? `${condition} && ${snippet}` : snippet);
}
});
@ -503,90 +575,44 @@ export default class Element extends Node {
`);
}
addEventHandlers(block: Block, allUsedContexts) {
const { generator } = 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!
}
addEventHandlers(block: Block) {
const { compiler } = this;
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, context, true);
this.handlers.forEach(handler => {
const isCustomEvent = compiler.events.has(handler.name);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
allUsedContexts.add(context);
});
});
if (handler.callee) {
handler.render(this.compiler, block, handler.shouldHoist);
}
const ctx = context || 'this';
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);
const target = handler.shouldHoist ? 'this' : this.var;
// get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise
const handlerName = (shouldHoist ? generator : block).getUniqueName(
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
const handlerName = (handler.shouldHoist ? compiler : block).getUniqueName(
`${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
const handlerBody = deindent`
${eventHandlerUsesComponent &&
`var ${block.alias('component')} = ${ctx}._svelte.component;`}
${declarations}
${attribute.expression ?
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
`${block.alias('component')}.fire("${attribute.name}", event);`}
${handler.shouldHoist && (
handler.usesComponent || handler.usesContext
? `const { ${[handler.usesComponent && 'component', handler.usesContext && 'ctx'].filter(Boolean).join(', ')} } = ${target}._svelte;`
: null
)}
${handler.snippet ?
handler.snippet :
`${component}.fire("${handler.name}", event);`}
`;
if (isCustomEvent) {
block.addVariable(handlerName);
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}
});
`);
@ -595,61 +621,52 @@ export default class Element extends Node {
${handlerName}.destroy();
`);
} else {
const handler = deindent`
const handlerFunction = deindent`
function ${handlerName}(event) {
${handlerBody}
}
`;
if (shouldHoist) {
generator.blocks.push(handler);
if (handler.shouldHoist) {
compiler.target.blocks.push(handlerFunction);
} else {
block.builders.init.addBlock(handler);
block.builders.init.addBlock(handlerFunction);
}
block.builders.hydrate.addLine(
`@addListener(${this.var}, "${attribute.name}", ${handlerName});`
`@addListener(${this.var}, "${handler.name}", ${handlerName});`
);
block.builders.destroy.addLine(
`@removeListener(${this.var}, "${attribute.name}", ${handlerName});`
`@removeListener(${this.var}, "${handler.name}", ${handlerName});`
);
}
});
return eventHandlerUsesComponent;
}
addRefs(block: Block) {
// TODO it should surely be an error to have more than one 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};`
);
addRef(block: Block) {
const ref = `#component.refs.${this.ref}`;
block.builders.destroy.addLine(
`if (${ref} === ${this.var}) ${ref} = null;`
);
block.builders.mount.addLine(
`${ref} = ${this.var};`
);
this.generator.usesRefs = true; // so component.refs object is created
});
block.builders.destroy.addLine(
`if (${ref} === ${this.var}) ${ref} = null;`
);
}
addTransitions(
block: Block
) {
const intro = this.attributes.find((a: Transition) => a.type === 'Transition' && a.intro);
const outro = this.attributes.find((a: Transition) => a.type === 'Transition' && a.outro);
const { intro, outro } = this;
if (!intro && !outro) return;
if (intro === outro) {
block.contextualise(intro.expression); // TODO remove all these
const name = block.getUniqueName(`${this.var}_transition`);
const snippet = intro.expression
? intro.metadata.snippet
? intro.expression.snippet
: '{}';
block.addVariable(name);
@ -677,11 +694,9 @@ export default class Element extends Node {
const outroName = outro && block.getUniqueName(`${this.var}_outro`);
if (intro) {
block.contextualise(intro.expression);
block.addVariable(introName);
const snippet = intro.expression
? intro.metadata.snippet
? intro.expression.snippet
: '{}';
const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?
@ -704,11 +719,9 @@ export default class Element extends Node {
}
if (outro) {
block.contextualise(outro.expression);
block.addVariable(outroName);
const snippet = outro.expression
? outro.metadata.snippet
? outro.expression.snippet
: '{}';
const fn = `%transitions-${outro.name}`;
@ -727,31 +740,29 @@ export default class Element extends Node {
}
addActions(block: Block) {
this.attributes.filter((a: Action) => a.type === 'Action').forEach((attribute:Action) => {
const { expression } = attribute;
this.actions.forEach(action => {
const { expression } = action;
let snippet, dependencies;
if (expression) {
this.generator.addSourcemapLocations(expression);
block.contextualise(expression);
snippet = attribute.metadata.snippet;
dependencies = attribute.metadata.dependencies;
snippet = action.expression.snippet;
dependencies = action.expression.dependencies;
}
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);
const fn = `%actions-${attribute.name}`;
const fn = `%actions-${action.name}`;
block.builders.hydrate.addLine(
`${name} = ${fn}.call(#component, ${this.var}${snippet ? `, ${snippet}` : ''}) || {};`
);
if (dependencies && dependencies.length) {
if (dependencies && dependencies.size > 0) {
let conditional = `typeof ${name}.update === 'function' && `;
const deps = dependencies.map(dependency => `changed.${dependency}`).join(' || ');
conditional += dependencies.length > 1 ? `(${deps})` : deps;
const deps = [...dependencies].map(dependency => `changed.${dependency}`).join(' || ');
conditional += dependencies.size > 1 ? `(${deps})` : deps;
block.builders.update.addConditional(
conditional,
@ -772,11 +783,11 @@ export default class Element extends Node {
if (!attribute) return null;
if (attribute.value === true) return true;
if (attribute.value.length === 0) return '';
if (attribute.isTrue) return true;
if (attribute.chunks.length === 0) return '';
if (attribute.value.length === 1 && attribute.value[0].type === 'Text') {
return attribute.value[0].data;
if (attribute.chunks.length === 1 && attribute.chunks[0].type === 'Text') {
return attribute.chunks[0].data;
}
return null;
@ -797,29 +808,115 @@ export default class Element extends Node {
addCssClass() {
const classAttribute = this.attributes.find(a => a.name === 'class');
if (classAttribute && classAttribute.value !== true) {
if (classAttribute.value.length === 1 && classAttribute.value[0].type === 'Text') {
classAttribute.value[0].data += ` ${this.generator.stylesheet.id}`;
if (classAttribute && !classAttribute.isTrue) {
if (classAttribute.chunks.length === 1 && classAttribute.chunks[0].type === 'Text') {
(<Text>classAttribute.chunks[0]).data += ` ${this.compiler.stylesheet.id}`;
} else {
(<Node[]>classAttribute.value).push(
new Node({ type: 'Text', data: ` ${this.generator.stylesheet.id}` })
(<Node[]>classAttribute.chunks).push(
new Text(this.compiler, this, this.scope, {
type: 'Text',
data: ` ${this.compiler.stylesheet.id}`
})
// new Text({ type: 'Text', data: ` ${this.compiler.stylesheet.id}` })
);
}
} else {
this.attributes.push(
new Attribute({
generator: this.generator,
new Attribute(this.compiler, this, this.scope, {
type: 'Attribute',
name: 'class',
value: [new Node({ type: 'Text', data: `${this.generator.stylesheet.id}` })],
parent: this,
value: [{ type: 'Text', data: `${this.compiler.stylesheet.id}` }]
})
);
}
}
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(
generator: DomGenerator,
compiler: Compiler,
namespace: string,
name: string
) {
@ -835,7 +932,7 @@ function getRenderStatement(
}
function getClaimStatement(
generator: DomGenerator,
compiler: Compiler,
namespace: string,
nodes: string,
node: Node
@ -852,7 +949,7 @@ function getClaimStatement(
: `{}`}, ${namespace === namespaces.svg ? true : false})`;
}
function stringifyAttributeValue(value: Node | true) {
function stringifyAttributeValue(value: Node[] | true) {
if (value === true) 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 { DomGenerator } from '../dom/index';
import Compiler from '../Compiler';
import mapChildren from './shared/mapChildren';
import Block from '../dom/Block';
import TemplateScope from './shared/TemplateScope';
export default class Fragment extends Node {
block: Block;
children: Node[];
scope: TemplateScope;
constructor(compiler: Compiler, info: any) {
const scope = new TemplateScope();
super(compiler, null, scope, info);
this.scope = scope;
this.children = mapChildren(compiler, this, scope, info.children);
}
init() {
this.block = new Block({
generator: this.generator,
compiler: this.compiler,
name: '@create_main_fragment',
key: null,
contexts: new Map(),
indexes: new Map(),
changeableIndexes: new Map(),
indexNames: new Map(),
listNames: new Map(),
dependencies: new Set(),
});
this.generator.blocks.push(this.block);
this.compiler.target.blocks.push(this.block);
this.initChildren(this.block, true, null);
this.block.hasUpdateMethod = true;

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

@ -3,12 +3,6 @@ import Tag from './shared/Tag';
import Block from '../dom/Block';
export default class MustacheTag extends Tag {
init(block: Block) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName('text');
block.addDependencies(this.metadata.dependencies);
}
build(
block: Block,
parentNode: string,
@ -30,4 +24,14 @@ export default class MustacheTag extends Tag {
remount(name: string) {
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';
export default class RawMustacheTag extends Tag {
init(block: Block) {
this.cannotUseInnerHTML();
this.var = block.getUniqueName('raw');
block.addDependencies(this.metadata.dependencies);
}
build(
block: Block,
parentNode: string,
@ -93,4 +87,8 @@ export default class RawMustacheTag extends Tag {
remount(name: string) {
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,
parentNodes: string
) {
const { generator } = this;
const { compiler } = this;
const slotName = this.getStaticAttributeValue('name') || 'default';
generator.slots.add(slotName);
compiler.slots.add(slotName);
const content_name = block.getUniqueName(`slot_content_${slotName}`);
const prop = !isValidIdentifier(slotName) ? `["${slotName}"]` : `.${slotName}`;
@ -136,18 +136,31 @@ export default class Slot extends Element {
getStaticAttributeValue(name: string) {
const attribute = this.attributes.find(
(attr: Node) => attr.name.toLowerCase() === name
attr => attr.name.toLowerCase() === name
);
if (!attribute) return null;
if (attribute.value === true) return true;
if (attribute.value.length === 0) return '';
if (attribute.isTrue) return true;
if (attribute.chunks.length === 0) return '';
if (attribute.value.length === 1 && attribute.value[0].type === 'Text') {
return attribute.value[0].data;
if (attribute.chunks.length === 1 && attribute.chunks[0].type === 'Text') {
return attribute.chunks[0].data;
}
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 Block from '../dom/Block';
@ -33,6 +33,11 @@ export default class Text extends Node {
data: string;
shouldSkip: boolean;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.data = info.data;
}
init(block: Block) {
const parentElement = this.findNearest(/(?:Element|Component)/);
@ -62,4 +67,17 @@ export default class Text extends Node {
remount(name: string) {
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 getExpressionPrecedence from '../../utils/getExpressionPrecedence';
import Node from './shared/Node';
import Block from '../dom/Block';
import mapChildren from './shared/mapChildren';
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(
block: Block,
parentNode: string,
@ -15,27 +31,20 @@ export default class Title extends Node {
let value;
const allDependencies = new Set();
let shouldCache;
// TODO some of this code is repeated in Tag.ts — would be good to
// DRY it out if that's possible without introducing crazy indirection
if (this.children.length === 1) {
// single {{tag}} — may be a non-string
const { expression } = this.children[0];
const { indexes } = block.contextualise(expression);
const { dependencies, snippet } = this.children[0].metadata;
const { dependencies, snippet } = this.children[0].expression;
value = snippet;
dependencies.forEach(d => {
allDependencies.add(d);
});
shouldCache = (
expression.type !== 'Identifier' ||
block.contexts.has(expression.name)
);
} else {
// '{{foo}} {{bar}}' — treat as string concatenation
// '{foo} {bar}' — treat as string concatenation
value =
(this.children[0].type === 'Text' ? '' : `"" + `) +
this.children
@ -43,34 +52,31 @@ export default class Title extends Node {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { indexes } = block.contextualise(chunk.expression);
const { dependencies, snippet } = chunk.metadata;
const { dependencies, snippet } = chunk.expression;
dependencies.forEach(d => {
allDependencies.add(d);
});
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet;
return chunk.expression.getPrecedence() <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
shouldCache = true;
}
const last = shouldCache && block.getUniqueName(
const last = this.shouldCache && block.getUniqueName(
`title_value`
);
if (shouldCache) block.addVariable(last);
if (this.shouldCache) block.addVariable(last);
let updater;
const init = shouldCache ? `${last} = ${value}` : value;
const init = this.shouldCache ? `${last} = ${value}` : value;
block.builders.init.addLine(
`document.title = ${init};`
);
updater = `document.title = ${shouldCache ? last : value};`;
updater = `document.title = ${this.shouldCache ? last : value};`;
if (allDependencies.size) {
const dependencies = Array.from(allDependencies);
@ -81,7 +87,7 @@ export default class Title extends Node {
const updateCachedValue = `${last} !== (${last} = ${value})`;
const condition = shouldCache ?
const condition = this.shouldCache ?
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
changedCheck;
@ -95,4 +101,14 @@ export default class Title extends Node {
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 Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
import Binding from './Binding';
import EventHandler from './EventHandler';
const associatedEvents = {
innerWidth: 'resize',
@ -34,99 +35,100 @@ const readonly = new Set([
export default class Window extends Node {
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(
block: Block,
parentNode: string,
parentNodes: string
) {
const { generator } = this;
const { compiler } = this;
const events = {};
const bindings: Record<string, string> = {};
this.attributes.forEach((attribute: Node) => {
if (attribute.type === 'EventHandler') {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.addSourcemapLocations(attribute.expression);
this.handlers.forEach(handler => {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
compiler.addSourcemapLocations(handler.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) => {
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')}.`
);
}
handler.render(compiler, block, false); // TODO hoist?
const handlerName = block.getUniqueName(`onwindow${attribute.name}`);
const handlerBody = deindent`
${usesState && `var state = #component.get();`}
[${attribute.expression.start}-${attribute.expression.end}];
`;
const handlerName = block.getUniqueName(`onwindow${handler.name}`);
const handlerBody = deindent`
${usesState && `var ctx = #component.get();`}
${handler.snippet};
`;
if (isCustomEvent) {
// TODO dry this out
block.addVariable(handlerName);
if (isCustomEvent) {
// TODO dry this out
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`
${handlerName} = %events-${attribute.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("${attribute.name}", ${handlerName});
`);
block.builders.destroy.addBlock(deindent`
window.removeEventListener("${attribute.name}", ${handlerName});
`);
}
block.builders.destroy.addBlock(deindent`
window.removeEventListener("${handler.name}", ${handlerName});
`);
}
});
if (attribute.type === 'Binding') {
// in dev mode, throw if read-only values are written to
if (readonly.has(attribute.name)) {
generator.readonly.add(attribute.value.name);
}
this.bindings.forEach(binding => {
// in dev mode, throw if read-only values are written to
if (readonly.has(binding.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
if (attribute.name === 'online') return;
// bind:online is a special case, we need to listen for two separate events
if (binding.name === 'online') return;
const associatedEvent = associatedEvents[attribute.name];
const property = properties[attribute.name] || attribute.name;
const associatedEvent = associatedEvents[binding.name];
const property = properties[binding.name] || binding.name;
if (!events[associatedEvent]) events[associatedEvent] = [];
events[associatedEvent].push(
`${attribute.value.name}: this.${property}`
);
if (!events[associatedEvent]) events[associatedEvent] = [];
events[associatedEvent].push(
`${binding.value.node.name}: this.${property}`
);
// add initial value
generator.metaBindings.push(
`this._state.${attribute.value.name} = window.${property};`
);
}
// add initial value
compiler.target.metaBindings.push(
`this._state.${binding.value.node.name} = window.${property};`
);
});
const lock = block.getUniqueName(`window_updating`);
@ -149,13 +151,13 @@ export default class Window extends Node {
if (${lock}) return;
${lock} = true;
`}
${generator.options.dev && `component._updatingReadonlyProperty = true;`}
${compiler.options.dev && `component._updatingReadonlyProperty = true;`}
#component.set({
${props}
});
${generator.options.dev && `component._updatingReadonlyProperty = false;`}
${compiler.options.dev && `component._updatingReadonlyProperty = false;`}
${event === 'scroll' && `${lock} = false;`}
`;
@ -205,7 +207,7 @@ export default class Window extends Node {
`);
// add initial value
generator.metaBindings.push(
compiler.target.metaBindings.push(
`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 { trimStart, trimEnd } from '../../../utils/trim';
export default class Node {
type: string;
start: number;
end: number;
[key: string]: any;
readonly start: number;
readonly end: number;
readonly compiler: Compiler;
readonly parent: Node;
readonly type: string;
metadata?: {
dependencies: string[];
snippet: string;
};
parent: Node;
prev?: Node;
next?: Node;
generator: DomGenerator;
canUseInnerHTML: boolean;
var: string;
constructor(data: Record<string, any>) {
Object.assign(this, data);
constructor(compiler: Compiler, parent, scope, info: any) {
this.start = info.start;
this.end = info.end;
this.type = info.type;
// this makes properties non-enumerable, which makes logging
// bearable. might have a performance cost. TODO remove in prod?
Object.defineProperties(this, {
compiler: {
value: compiler
},
parent: {
value: parent
}
});
}
cannotUseInnerHTML() {
if (this.canUseInnerHTML !== false) {
this.canUseInnerHTML = false;
if (this.parent) {
if (!this.parent.cannotUseInnerHTML) console.log(this.parent.type, this.type);
this.parent.cannotUseInnerHTML();
}
if (this.parent) this.parent.cannotUseInnerHTML();
}
}
@ -82,7 +86,7 @@ export default class Node {
lastChild = null;
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);

@ -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 Generator from '../Generator';
import Compiler from '../Compiler';
import Stats from '../../Stats';
import Stylesheet from '../../css/Stylesheet';
import Block from './Block';
import visit from './visit';
import { removeNode, removeObjectKey } from '../../utils/removeNode';
import getName from '../../utils/getName';
import globalWhitelist from '../../utils/globalWhitelist';
import { Parsed, Node, CompileOptions } from '../../interfaces';
import { AppendTarget } from './interfaces';
import { Ast, Node, CompileOptions } from '../../interfaces';
import { AppendTarget } from '../../interfaces';
import { stringify } from '../../utils/stringify';
export class SsrGenerator extends Generator {
export class SsrTarget {
bindings: string[];
renderCode: string;
appendTargets: AppendTarget[];
constructor(
parsed: Parsed,
source: string,
name: string,
stylesheet: Stylesheet,
options: CompileOptions,
stats: Stats
) {
super(parsed, source, name, stylesheet, options, stats, false);
constructor() {
this.bindings = [];
this.renderCode = '';
this.appendTargets = [];
this.stylesheet.warnOnUnusedSelectors(options.onwarn);
}
append(code: string) {
@ -44,7 +32,7 @@ export class SsrGenerator extends Generator {
}
export default function ssr(
parsed: Parsed,
ast: Ast,
source: string,
stylesheet: Stylesheet,
options: CompileOptions,
@ -52,28 +40,22 @@ export default function ssr(
) {
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
const mainBlock = new Block({
generator,
contexts: new Map(),
indexes: new Map(),
conditions: [],
});
trim(parsed.html.children).forEach((node: Node) => {
visit(generator, mainBlock, node);
trim(compiler.fragment.children).forEach((node: Node) => {
node.ssr();
});
const css = generator.customElement ?
const css = compiler.customElement ?
{ code: null, map: null } :
generator.stylesheet.render(options.filename, true);
compiler.stylesheet.render(options.filename, true);
// generate initial state object
const expectedProperties = Array.from(generator.expectedProperties);
const expectedProperties = Array.from(compiler.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = expectedProperties.filter(prop => prop[0] === '$');
@ -93,11 +75,13 @@ export default function ssr(
initialState.push('{}');
}
initialState.push('state');
initialState.push('ctx');
const helpers = new Set();
// TODO concatenate CSS maps
const result = deindent`
${generator.javascript}
${compiler.javascript}
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();`}
__result.addComponent(${name});
state = Object.assign(${initialState.join(', ')});
ctx = Object.assign(${initialState.join(', ')});
${computations.map(
({ key, deps }) =>
`state.${key} = %computed-${key}(state);`
`ctx.${key} = %computed-${key}(ctx);`
)}
${generator.bindings.length &&
${target.bindings.length &&
deindent`
var settled = false;
var tmp;
@ -148,11 +132,11 @@ export default function ssr(
while (!settled) {
settled = true;
${generator.bindings.join('\n\n')}
${target.bindings.join('\n\n')}
}
`}
return \`${generator.renderCode}\`;
return \`${target.renderCode}\`;
};
${name}.css = {
@ -163,64 +147,9 @@ export default function ssr(
var warned = false;
${templateProperties.preload && `${name}.preload = %preload;`}
`;
${
// 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 });
return compiler.generate(result, options, { name, format });
}
function trim(nodes) {

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

@ -178,7 +178,7 @@ function applySelector(blocks: Block[], node: Node, stack: Node[], toEncapsulate
}
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;
toEncapsulate.push({ node, block });
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);
if (!attr) return false;
if (attr.value === true) return operator === null;
if (attr.value.length > 1) return true;
if (attr.isTrue) return operator === null;
if (attr.chunks.length > 1) return true;
if (!expectedValue) return true;
const pattern = operators[operator](expectedValue, caseInsensitive ? 'i' : '');
const value = attr.value[0];
const value = attr.chunks[0];
if (!value) return false;
if (value.type === 'Text') return pattern.test(value.data);
const possibleValues = new Set();
gatherPossibleValues(value.expression, possibleValues);
gatherPossibleValues(value.node, possibleValues);
if (possibleValues.has(UNKNOWN)) return true;
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 getCodeFrame from '../utils/getCodeFrame';
import hash from '../utils/hash';
import Element from '../generators/nodes/Element';
import Element from '../compile/nodes/Element';
import { Validator } from '../validate/index';
import { Node, Parsed, Warning } from '../interfaces';
import { Node, Ast, Warning } from '../interfaces';
class Rule {
selectors: Selector[];
@ -236,7 +236,7 @@ const keys = {};
export default class Stylesheet {
source: string;
parsed: Parsed;
ast: Ast;
filename: string;
dev: boolean;
@ -248,9 +248,9 @@ export default class Stylesheet {
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.parsed = parsed;
this.ast = ast;
this.filename = filename;
this.dev = dev;
@ -259,15 +259,15 @@ export default class Stylesheet {
this.nodesWithCssClass = new Set();
if (parsed.css && parsed.css.children.length) {
this.id = `svelte-${hash(parsed.css.content.styles)}`;
if (ast.css && ast.css.children.length) {
this.id = `svelte-${hash(ast.css.content.styles)}`;
this.hasStyles = true;
const stack: (Rule | Atrule)[] = [];
let currentAtrule: Atrule = null;
walk(this.parsed.css, {
walk(this.ast.css, {
enter: (node: Node) => {
if (node.type === 'Atrule') {
const last = stack[stack.length - 1];
@ -346,7 +346,7 @@ export default class Stylesheet {
const code = new MagicString(this.source);
walk(this.parsed.css, {
walk(this.ast.css, {
enter: (node: Node) => {
code.addSourcemapLocation(node.start);
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 validate from './validate/index';
import generate from './generators/dom/index';
import generateSSR from './generators/server-side-rendering/index';
import generate from './compile/dom/index';
import generateSSR from './compile/ssr/index';
import Stats from './Stats';
import { assign } from './shared/index.js';
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';
const version = '__VERSION__';
@ -108,7 +108,7 @@ export async function preprocess(source: string, options: PreprocessOptions) {
function compile(source: string, _options: CompileOptions) {
const options = normalizeOptions(_options);
let parsed: Parsed;
let ast: Ast;
const stats = new Stats({
onwarn: options.onwarn
@ -116,7 +116,7 @@ function compile(source: string, _options: CompileOptions) {
try {
stats.start('parse');
parsed = parse(source, options);
ast = parse(source, options);
stats.stop('parse');
} catch (err) {
options.onerror(err);
@ -124,20 +124,20 @@ function compile(source: string, _options: CompileOptions) {
}
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.start('validate');
validate(parsed, source, stylesheet, stats, options);
validate(ast, source, stylesheet, stats, options);
stats.stop('validate');
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;
return compiler(parsed, source, stylesheet, options, stats);
return compiler(ast, source, stylesheet, options, stats);
};
function create(source: string, _options: CompileOptions = {}) {

@ -20,7 +20,7 @@ export interface Parser {
metaTags: {};
}
export interface Parsed {
export interface Ast {
html: Node;
css: Node;
js: Node;
@ -71,7 +71,6 @@ export interface GenerateOptions {
format: ModuleFormat;
banner?: string;
sharedPath?: string;
helpers?: { name: string, alias: string }[];
}
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 interface AppendTarget {
slots: Record<string, string>;
slotStack: string[]
}

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

@ -60,6 +60,16 @@ const disallowedContents = new Map([
['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) {
const start = parser.index++;
@ -113,7 +123,9 @@ export default function tag(parser: Parser) {
const type = metaTags.has(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 = {
start,

@ -32,7 +32,7 @@ fs.readdirSync(__dirname).forEach(file => {
});
fs.writeFileSync(
'src/generators/dom/shared.ts',
'src/compile/shared.ts',
`// this file is auto-generated, do not edit it
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 './keyed-each.js';
export * from './spread.js';
export * from './ssr.js';
export * from './transitions.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 { 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) {
const globals = new Set();
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';
export default function createDebuggingComment(node: Node, generator: DomGenerator) {
const { locate, source } = generator;
export default function createDebuggingComment(node: Node, compiler: Compiler) {
const { locate, source } = compiler;
let c = node.start;
if (node.type === 'ElseBlock') {
@ -10,7 +10,7 @@ export default function createDebuggingComment(node: Node, generator: DomGenerat
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;

@ -1,6 +1,7 @@
import { Node } from '../interfaces';
export default function flattenReference(node: Node) {
if (node.type === 'Expression') throw new Error('bad');
const parts = [];
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 validateWindow from './validateWindow';
import validateHead from './validateHead';
import validateSlot from './validateSlot';
import a11y from './a11y';
import fuzzymatch from '../utils/fuzzymatch'
import flattenReference from '../../utils/flattenReference';
@ -29,25 +31,32 @@ export default function validateHtml(validator: Validator, html: Node) {
validateHead(validator, node, refs, refCallees);
}
else if (node.type === 'Element') {
const isComponent =
node.name === 'svelte:self' ||
node.name === 'svelte:component' ||
validator.components.has(node.name);
else if (node.type === 'Slot') {
validateSlot(validator, node);
}
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(
validator,
node,
refs,
refCallees,
stack,
elementStack,
isComponent
elementStack
);
if (!isComponent) {
a11y(validator, node, elementStack);
}
a11y(validator, node, elementStack);
}
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[]>,
refCallees: Node[],
stack: Node[],
elementStack: Node[],
isComponent: Boolean
elementStack: Node[]
) {
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)) {
validator.warn(node, {
code: `missing-namespace`,
@ -95,7 +83,7 @@ export default function validateElement(
refs.get(attribute.name).push(node);
}
if (!isComponent && attribute.type === 'Binding') {
if (attribute.type === 'Binding') {
const { name } = attribute;
if (name === 'value') {
@ -179,13 +167,6 @@ export default function validateElement(
validator.used.events.add(attribute.name);
validateEventHandler(validator, attribute, refCallees);
} 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);
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);
}
} 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);
if (!validator.actions.has(attribute.name)) {
@ -295,9 +269,11 @@ function checkSlotAttribute(validator: Validator, node: Node, attribute: Node, s
let i = stack.length;
while (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 (parent.name === 'svelte:self' || parent.name === 'svelte:component' || validator.components.has(parent.name)) return;
} else if (parent.type === 'Element') {
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
node.children.forEach(node => {
if (node.type !== 'Element') return; // TODO handle {{#if}} and friends?
validateElement(validator, node, refs, refCallees, [], [], false);
if (node.type !== 'Element' && node.type !== 'Title') return; // TODO handle {{#if}} and friends?
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 error from '../utils/error';
import Stylesheet from '../css/Stylesheet';
import Stats from '../Stats';
import { Node, Parsed, CompileOptions, Warning } from '../interfaces';
import { Node, Ast, CompileOptions, Warning } from '../interfaces';
export class Validator {
readonly source: string;
@ -34,7 +33,7 @@ export class Validator {
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.stats = stats;
@ -93,7 +92,7 @@ export class Validator {
}
export default function validate(
parsed: Parsed,
ast: Ast,
source: string,
stylesheet: Stylesheet,
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,
filename,
dev,
parser
});
if (parsed.js) {
validateJs(validator, parsed.js);
if (ast.js) {
validateJs(validator, ast.js);
}
if (parsed.css) {
if (ast.css) {
stylesheet.validate(validator);
}
if (parsed.html) {
validateHtml(validator, parsed.html);
if (ast.html) {
validateHtml(validator, ast.html);
}
// 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 = {
components: 'component',
// TODO helpers require a bit more work — need to analyse all expressions

@ -136,7 +136,7 @@ var proto = {
/* generated by Svelte vX.Y.Z */
function link(node) {
function onClick(event) {
event.preventDefault();
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;
return {

@ -2,7 +2,7 @@
import { assign, createElement, detachNode, init, insertNode, noop, proto } from "svelte/shared.js";
function link(node) {
function onClick(event) {
event.preventDefault();
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;
return {

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save