Implement reactive assignments (#1839)

This also includes elements of RFCs 2 and 3
pull/1878/head
Rich Harris 6 years ago committed by GitHub
parent 85b731c1bc
commit f45e2b70fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

8
.gitignore vendored

@ -1,10 +1,12 @@
.DS_Store
.nyc_output
node_modules
*.map
/cli/
/compiler/
/ssr/
/shared.js
/internal.js
/compiler.js
/scratch/
/coverage/
/coverage.lcov/
@ -13,7 +15,7 @@ node_modules
/test/sourcemaps/samples/*/output.js.map
/test/sourcemaps/samples/*/output.css
/test/sourcemaps/samples/*/output.css.map
/src/compile/shared.ts
/store.umd.js
/yarn-error.log
_actual*.*
_actual*.*
_*/

@ -1,6 +1,5 @@
language: node_js
node_js:
- "6"
- "node"
env:

@ -10,7 +10,7 @@ init:
environment:
matrix:
# node.js
- nodejs_version: 6
- nodejs_version: 10
install:
- ps: Install-Product node $env:nodejs_version

@ -0,0 +1,7 @@
export {
onMount,
onDestroy,
beforeUpdate,
afterUpdate,
createEventDispatcher
} from './internal.js';

3021
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,19 +1,17 @@
{
"name": "svelte",
"version": "2.15.4",
"version": "3.0.0-alpha2",
"description": "The magical disappearing UI framework",
"main": "compiler/svelte.js",
"main": "index.js",
"bin": {
"svelte": "svelte"
},
"files": [
"cli",
"compiler/*.js",
"ssr/*.js",
"shared.js",
"store.js",
"store.umd.js",
"store.d.ts",
"compiler.js",
"register.js",
"index.js",
"internal.js",
"svelte",
"README.md"
],
@ -25,11 +23,11 @@
"codecov": "codecov",
"precodecov": "npm run coverage",
"lint": "eslint src test/*.js",
"build": "node src/shared/_build.js && rollup -c && rollup -c rollup.store.config.js",
"build": "rollup -c",
"prepare": "npm run build",
"dev": "node src/shared/_build.js && rollup -c -w",
"dev": "rollup -c -w",
"pretest": "npm run build",
"posttest": "agadoo shared.js",
"posttest": "agadoo src/internal/index.js",
"prepublishOnly": "npm run lint && npm test",
"prettier": "prettier --write \"src/**/*.ts\""
},
@ -55,17 +53,15 @@
"acorn": "^5.4.1",
"acorn-dynamic-import": "^3.0.0",
"agadoo": "^1.0.1",
"chalk": "^2.4.0",
"codecov": "^3.0.0",
"console-group": "^0.3.2",
"css-tree": "1.0.0-alpha22",
"eslint": "^5.3.0",
"eslint-plugin-html": "^4.0.3",
"eslint-plugin-import": "^2.11.0",
"estree-walker": "^0.5.1",
"estree-walker": "^0.6.0",
"is-reference": "^1.1.0",
"jsdom": "^11.8.0",
"kleur": "^2.0.1",
"kleur": "^3.0.0",
"locate-character": "^2.0.5",
"magic-string": "^0.25.0",
"mocha": "^5.2.0",

@ -1,6 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { compile } from '../index.ts';
const fs = require('fs');
const path = require('path');
const { compile } = require('./compiler.js');
let compileOptions = {
extensions: ['.html']
@ -10,7 +10,7 @@ function capitalise(name) {
return name[0].toUpperCase() + name.slice(1);
}
export default function register(options) {
function register(options) {
if (options.extensions) {
compileOptions.extensions.forEach(deregisterExtension);
options.extensions.forEach(registerExtension);
@ -33,7 +33,8 @@ function registerExtension(extension) {
const options = Object.assign({}, compileOptions, {
filename,
name: capitalise(name),
generate: 'ssr'
generate: 'ssr',
format: 'cjs'
});
const { js } = compile(fs.readFileSync(filename, 'utf-8'), options);
@ -42,4 +43,6 @@ function registerExtension(extension) {
};
}
registerExtension('.html');
registerExtension('.html');
module.exports = register;

@ -20,43 +20,18 @@ export default [
json(),
typescript({
include: 'src/**',
exclude: 'src/shared/**',
exclude: 'src/internal/**',
typescript: require('typescript')
})
],
output: {
file: 'compiler/svelte.js',
file: 'compiler.js',
format: 'umd',
name: 'svelte',
sourcemap: true
}
},
/* ssr/register.js */
{
input: 'src/ssr/register.js',
plugins: [
resolve(),
commonjs(),
buble({
include: 'src/**',
exclude: 'src/shared/**',
target: {
node: 4
}
})
],
external: [path.resolve('src/index.ts'), 'fs', 'path'],
output: {
file: 'ssr/register.js',
format: 'cjs',
paths: {
[path.resolve('src/index.ts')]: '../compiler/svelte.js'
},
sourcemap: true
}
},
/* cli/*.js */
{
input: ['src/cli/index.ts'],
@ -64,7 +39,7 @@ export default [
dir: 'cli',
format: 'cjs',
paths: {
svelte: '../compiler/svelte.js'
svelte: '../compiler.js'
}
},
external: ['fs', 'path', 'os', 'svelte'],
@ -79,11 +54,11 @@ export default [
experimentalCodeSplitting: true
},
/* shared.js */
/* internal.js */
{
input: 'src/shared/index.js',
input: 'src/internal/index.js',
output: {
file: 'shared.js',
file: 'internal.js',
format: 'es'
}
}

@ -1,9 +0,0 @@
export default {
input: 'store.js',
output: {
file: 'store.umd.js',
format: 'umd',
name: 'svelte',
extend: true
}
};

@ -1,4 +1,4 @@
import { Node, Warning } from './interfaces';
import { Warning } from './interfaces';
import Component from './compile/Component';
const now = (typeof process !== 'undefined' && process.hrtime)
@ -96,21 +96,12 @@ export default class Stats {
}
});
const hooks: Record<string, boolean> = component && {
oncreate: !!component.templateProperties.oncreate,
ondestroy: !!component.templateProperties.ondestroy,
onstate: !!component.templateProperties.onstate,
onupdate: !!component.templateProperties.onupdate
};
const computed = new Set(component.computations.map(c => c.key));
return {
props: Array.from(component.expectedProperties).filter(key => !computed.has(key)),
props: component.props.map(prop => prop.as),
timings,
warnings: this.warnings,
imports,
hooks
templateReferences: component && component.template_references
};
}
@ -118,4 +109,4 @@ export default class Stats {
this.warnings.push(warning);
this.onwarn(warning);
}
}
}

@ -54,8 +54,7 @@ export function compile(input, opts) {
immutable: opts.immutable,
generate: opts.generate || 'dom',
customElement: opts.customElement,
store: opts.store,
shared: opts.shared
sveltePath: opts.sveltePath
};
if (isDir) {

@ -7,7 +7,7 @@ prog
.command('compile <input>')
.option('-o, --output', 'Output (if absent, prints to stdout)')
.option('-f, --format', 'Type of output (amd, cjs, es, iife, umd)')
.option('-f, --format', 'Type of output (cjs or esm)', 'esm')
.option('-g, --globals', 'Comma-separate list of `module ID:Global` pairs')
.option('-n, --name', 'Name for IIFE/UMD export (inferred from filename by default)')
.option('-m, --sourcemap', 'Generate sourcemap (`-m inline` for inline map)')

File diff suppressed because it is too large Load Diff

@ -116,9 +116,9 @@ class Declaration {
if (!this.node.property) return; // @apply, and possibly other weird cases?
const c = this.node.start + this.node.property.length;
const first = this.node.value.children ?
this.node.value.children[0] :
this.node.value;
const first = this.node.value.children
? this.node.value.children[0]
: this.node.value;
let start = first.start;
while (/\s/.test(code.original[start])) start += 1;
@ -264,15 +264,15 @@ export default class Stylesheet {
this.nodesWithCssClass = new Set();
this.nodesWithRefCssClass = new Map();
if (ast.css && ast.css.children.length) {
this.id = `svelte-${hash(ast.css.content.styles)}`;
if (ast.css[0] && ast.css[0].children.length) {
this.id = `svelte-${hash(ast.css[0].content.styles)}`;
this.hasStyles = true;
const stack: (Rule | Atrule)[] = [];
let currentAtrule: Atrule = null;
walk(this.ast.css, {
walk(ast.css[0], {
enter: (node: Node) => {
if (node.type === 'Atrule') {
const last = stack[stack.length - 1];

@ -1,11 +1,10 @@
import { assign } from '../shared';
import { assign } from '../internal';
import Stats from '../Stats';
import parse from '../parse/index';
import renderDOM from './render-dom/index';
import renderSSR from './render-ssr/index';
import { CompileOptions, Warning, Ast } from '../interfaces';
import Component from './Component';
import deprecate from '../utils/deprecate';
function normalize_options(options: CompileOptions): CompileOptions {
let normalized = assign({ generate: 'dom', dev: false }, options);
@ -46,16 +45,6 @@ function validate_options(options: CompileOptions, stats: Stats) {
}
export default function compile(source: string, options: CompileOptions = {}) {
const onerror = options.onerror || (err => {
throw err;
});
if (options.onerror) {
// TODO remove in v3
deprecate(`Instead of using options.onerror, wrap svelte.compile in a try-catch block`);
delete options.onerror;
}
options = normalize_options(options);
const stats = new Stats({
@ -64,33 +53,29 @@ export default function compile(source: string, options: CompileOptions = {}) {
let ast: Ast;
try {
validate_options(options, stats);
stats.start('parse');
ast = parse(source, options);
stats.stop('parse');
stats.start('create component');
const component = new Component(
ast,
source,
options.name || 'SvelteComponent',
options,
stats
);
stats.stop('create component');
if (options.generate === false) {
return { ast, stats: stats.render(component), js: null, css: null };
}
if (options.generate === 'ssr') {
return renderSSR(component, options);
}
return renderDOM(component, options);
} catch (err) {
onerror(err);
validate_options(options, stats);
stats.start('parse');
ast = parse(source, options);
stats.stop('parse');
stats.start('create component');
const component = new Component(
ast,
source,
options.name || 'SvelteComponent',
options,
stats
);
stats.stop('create component');
if (options.generate === false) {
return { ast, stats: stats.render(component), js: null, css: null };
}
const js = options.generate === 'ssr'
? renderSSR(component, options)
: renderDOM(component, options);
return component.generate(js);
}

@ -1,27 +1,24 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
export default class Action extends Node {
type: 'Action';
name: string;
expression: Expression;
usesContext: boolean;
constructor(component, parent, scope, info) {
constructor(component: Component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
component.used.actions.add(this.name);
component.warn_if_undefined(info, scope);
if (!component.actions.has(this.name)) {
component.error(this, {
code: `missing-action`,
message: `Missing action '${this.name}'`
});
}
this.name = info.name;
this.expression = info.expression
? new Expression(component, this, scope, info.expression)
: null;
this.usesContext = this.expression && this.expression.usesContext;
}
}

@ -9,9 +9,9 @@ export default class Animation extends Node {
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
component.warn_if_undefined(info, scope);
component.used.animations.add(this.name);
this.name = info.name;
if (parent.animation) {
component.error(this, {
@ -20,13 +20,6 @@ export default class Animation extends Node {
});
}
if (!component.animations.has(this.name)) {
component.error(this, {
code: `missing-animation`,
message: `Missing animation '${this.name}'`
});
}
const block = parent.parent;
if (!block || block.type !== 'EachBlock' || !block.key) {
// TODO can we relax the 'immediate child' rule?

@ -17,6 +17,7 @@ export default class Attribute extends Node {
isSpread: boolean;
isTrue: boolean;
isDynamic: boolean;
isStatic: boolean;
isSynthetic: boolean;
shouldCache: boolean;
expression?: Expression;
@ -33,16 +34,18 @@ export default class Attribute extends Node {
this.isSynthetic = false;
this.expression = new Expression(component, this, scope, info.expression);
this.dependencies = this.expression.dependencies;
this.dependencies = this.expression.dynamic_dependencies;
this.chunks = null;
this.isDynamic = true; // TODO not necessarily
this.isStatic = false;
this.shouldCache = false; // TODO does this mean anything here?
}
else {
this.name = info.name;
this.isTrue = info.value === true;
this.isStatic = true;
this.isSynthetic = info.synthetic;
this.dependencies = new Set();
@ -52,9 +55,11 @@ export default class Attribute extends Node {
: info.value.map(node => {
if (node.type === 'Text') return node;
this.isStatic = false;
const expression = new Expression(component, this, scope, node.expression);
addToSet(this.dependencies, expression.dependencies);
addToSet(this.dependencies, expression.dynamic_dependencies);
return expression;
});
@ -75,7 +80,7 @@ export default class Attribute extends Node {
if (this.chunks.length === 1) {
return this.chunks[0].type === 'Text'
? stringify(this.chunks[0].data)
: this.chunks[0].snippet;
: this.chunks[0].render();
}
return (this.chunks[0].type === 'Text' ? '' : `"" + `) +
@ -84,7 +89,7 @@ export default class Attribute extends Node {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
return chunk.getPrecedence() <= 13 ? `(${chunk.snippet})` : chunk.snippet;
return chunk.getPrecedence() <= 13 ? `(${chunk.render()})` : chunk.render();
}
})
.join(' + ');

@ -17,13 +17,12 @@ export default class AwaitBlock extends Node {
super(component, parent, scope, info);
this.expression = new Expression(component, this, scope, info.expression);
const deps = this.expression.dependencies;
this.value = info.value;
this.error = info.error;
this.pending = new PendingBlock(component, this, scope, info.pending);
this.then = new ThenBlock(component, this, scope.add(this.value, deps), info.then);
this.catch = new CatchBlock(component, this, scope.add(this.error, deps), info.catch);
this.then = new ThenBlock(component, this, scope, info.then);
this.catch = new CatchBlock(component, this, scope, info.catch);
}
}

@ -1,31 +1,39 @@
import Node from './shared/Node';
import getObject from '../../utils/getObject';
import Expression from './shared/Expression';
import Component from '../Component';
export default class Binding extends Node {
name: string;
value: Expression;
expression: Expression;
isContextual: boolean;
usesContext: boolean;
obj: string;
prop: string;
constructor(component, parent, scope, info) {
constructor(component: Component, parent, scope, info) {
super(component, parent, scope, info);
if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') {
component.error(info, {
code: 'invalid-directive-value',
message: 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
});
}
this.name = info.name;
this.value = new Expression(component, this, scope, info.value);
this.expression = new Expression(component, this, scope, info.expression);
let obj;
let prop;
const { name } = getObject(this.value.node);
const { name } = getObject(this.expression.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}✂]`;
if (this.expression.node.type === 'MemberExpression') {
prop = `[✂${this.expression.node.property.start}-${this.expression.node.property.end}✂]`;
if (!this.expression.node.computed) prop = `'${prop}'`;
obj = `[✂${this.expression.node.object.start}-${this.expression.node.object.end}✂]`;
this.usesContext = true;
} else {

@ -1,14 +1,19 @@
import Node from './shared/Node';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
import TemplateScope from './shared/TemplateScope';
export default class CatchBlock extends Node {
block: Block;
scope: TemplateScope;
children: Node[];
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, parent, scope, info.children);
this.scope = scope.child();
this.scope.add(parent.error, parent.expression.dependencies);
this.children = mapChildren(component, parent, this.scope, info.children);
this.warnIfEmptyBlock();
}

@ -11,6 +11,7 @@ export default class EachBlock extends Node {
block: Block;
expression: Expression;
context_node: Node;
iterations: string;
index: string;
@ -28,6 +29,7 @@ export default class EachBlock extends Node {
this.expression = new Expression(component, this, scope, info.expression);
this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring
this.context_node = info.context;
this.index = info.index;
this.scope = scope.child();
@ -36,13 +38,6 @@ export default class EachBlock extends Node {
unpackDestructuring(this.contexts, info.context, '');
this.contexts.forEach(context => {
if (component.helpers.has(context.key.name)) {
component.warn(context.key, {
code: `each-context-clash`,
message: `Context clashes with a helper. Rename one or the other to eliminate any ambiguity`
});
}
this.scope.add(context.key.name, this.expression.dependencies);
});

@ -9,10 +9,10 @@ import Animation from './Animation';
import Action from './Action';
import Class from './Class';
import Text from './Text';
import * as namespaces from '../../utils/namespaces';
import { namespaces } from '../../utils/namespaces';
import mapChildren from './shared/mapChildren';
import { dimensions } from '../../utils/patterns';
import fuzzymatch from '../validate/utils/fuzzymatch';
import fuzzymatch from '../../utils/fuzzymatch';
import Ref from './Ref';
import list from '../../utils/list';
@ -77,14 +77,14 @@ export default class Element extends Node {
type: 'Element';
name: string;
scope: any; // TODO
attributes: Attribute[];
actions: Action[];
bindings: Binding[];
classes: Class[];
handlers: EventHandler[];
intro?: Transition;
outro?: Transition;
animation?: Animation;
attributes: Attribute[] = [];
actions: Action[] = [];
bindings: Binding[] = [];
classes: Class[] = [];
handlers: EventHandler[] = [];
intro?: Transition = null;
outro?: Transition = null;
animation?: Animation = null;
children: Node[];
ref: Ref;
@ -107,16 +107,6 @@ export default class Element extends Node {
});
}
this.attributes = [];
this.actions = [];
this.bindings = [];
this.classes = [];
this.handlers = [];
this.intro = null;
this.outro = null;
this.animation = null;
if (this.name === 'textarea') {
if (info.children.length > 0) {
const valueAttribute = info.attributes.find(node => node.name === 'value');
@ -347,7 +337,7 @@ export default class Element extends Node {
}
if (name === 'slot') {
if (attribute.isDynamic) {
if (!attribute.isStatic) {
component.error(attribute, {
code: `invalid-slot-attribute`,
message: `slot attribute cannot have a dynamic value`
@ -435,7 +425,7 @@ export default class Element extends Node {
if (!attribute) return null;
if (attribute.isDynamic) {
if (!attribute.isStatic) {
component.error(attribute, {
code: `invalid-type`,
message: `'type' attribute cannot be dynamic if input uses two-way binding`
@ -474,7 +464,7 @@ export default class Element extends Node {
(attribute: Attribute) => attribute.name === 'multiple'
);
if (attribute && attribute.isDynamic) {
if (attribute && !attribute.isStatic) {
component.error(attribute, {
code: `dynamic-multiple-attribute`,
message: `'multiple' attribute cannot be dynamic if select uses two-way binding`
@ -658,10 +648,10 @@ export default class Element extends Node {
const slot = this.attributes.find(attribute => attribute.name === 'slot');
if (slot) {
const prop = quotePropIfNecessary(slot.chunks[0].data);
return `@append(${name}._slotted${prop}, ${this.var});`;
return `@append(${name}.$$.slotted${prop}, ${this.var});`;
}
return `@append(${name}._slotted.default, ${this.var});`;
return `@append(${name}.$$.slotted.default, ${this.var});`;
}
addCssClass(className = this.component.stylesheet.id) {

@ -1,164 +1,42 @@
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';
import list from '../../utils/list';
const validBuiltins = new Set(['set', 'fire', 'destroy']);
import Component from '../Component';
import deindent from '../../utils/deindent';
export default class EventHandler extends Node {
name: string;
modifiers: Set<string>;
dependencies: Set<string>;
expression: Node;
callee: any; // TODO
usesComponent: boolean;
expression: Expression;
handler_name: string;
usesContext: boolean;
usesEventObject: boolean;
isCustomEvent: boolean;
shouldHoist: boolean;
insertionPoint: number;
args: Expression[];
snippet: string;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
constructor(component: Component, parent, template_scope, info) {
super(component, parent, template_scope, info);
this.name = info.name;
this.modifiers = new Set(info.modifiers);
component.used.events.add(this.name);
this.dependencies = new Set();
if (info.expression) {
this.validateExpression(info.expression);
this.callee = flattenReference(info.expression.callee);
this.insertionPoint = info.expression.start;
this.usesComponent = !validCalleeObjects.has(this.callee.name);
this.usesContext = false;
this.usesEventObject = this.callee.name === 'event';
this.args = info.expression.arguments.map(param => {
const expression = new Expression(component, this, scope, param);
addToSet(this.dependencies, expression.dependencies);
if (expression.usesContext) this.usesContext = true;
if (expression.usesEvent) this.usesEventObject = true;
return expression;
});
this.snippet = `[✂${info.expression.start}-${info.expression.end}✂];`;
this.expression = new Expression(component, this, template_scope, info.expression);
this.usesContext = this.expression.usesContext;
} else {
this.callee = null;
this.insertionPoint = null;
const name = component.getUniqueName(`${this.name}_handler`);
component.declarations.push(name);
this.args = null;
this.usesComponent = true;
this.usesContext = false;
this.usesEventObject = true;
component.partly_hoisted.push(deindent`
function ${name}(event) {
@bubble($$self, event);
}
`);
this.snippet = null; // TODO handle shorthand events here?
this.handler_name = name;
}
this.isCustomEvent = component.events.has(this.name);
this.shouldHoist = !this.isCustomEvent && parent.hasAncestor('EachBlock');
}
render(component, block, context, hoisted) { // TODO hoist more event handlers
if (this.insertionPoint === null) return; // TODO handle shorthand events here?
if (!validCalleeObjects.has(this.callee.name)) {
const component_name = 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] === '$' && !component.methods.has(this.callee.name)) {
component.code.overwrite(
this.insertionPoint,
this.insertionPoint + 1,
`${component_name}.store.`
);
} else {
component.code.prependRight(
this.insertionPoint,
`${component_name}.`
);
}
}
if (this.isCustomEvent) {
this.args.forEach(arg => {
arg.overwriteThis(context);
});
if (this.callee && this.callee.name === 'this') {
const node = this.callee.nodes[0];
component.code.overwrite(node.start, node.end, context, {
storeName: true,
contentOnly: true
});
}
}
}
validateExpression(expression) {
const { callee, type } = expression;
if (type !== 'CallExpression') {
this.component.error(expression, {
code: `invalid-event-handler`,
message: `Expected a call expression`
});
}
const { component } = this;
const { name } = flattenReference(callee);
if (validCalleeObjects.has(name) || name === 'options') return;
if (name === 'refs') {
this.component.refCallees.push(callee);
return;
}
if (
(callee.type === 'Identifier' && validBuiltins.has(name)) ||
this.component.methods.has(name)
) {
return;
}
if (name[0] === '$') {
// assume it's a store method
return;
}
const validCallees = ['this.*', 'refs.*', 'event.*', 'options.*', 'console.*'].concat(
Array.from(validBuiltins),
Array.from(this.component.methods.keys())
);
let message = `'${component.source.slice(callee.start, callee.end)}' is an invalid callee ` ;
if (name === 'store') {
message += `(did you mean '$${component.source.slice(callee.start + 6, callee.end)}(...)'?)`;
} else {
message += `(should be one of ${list(validCallees)})`;
if (callee.type === 'Identifier' && component.helpers.has(callee.name)) {
message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`;
}
}
render() {
if (this.expression) return this.expression.render();
component.warn(expression, {
code: `invalid-callee`,
message
});
this.component.template_references.add(this.handler_name);
return `ctx.${this.handler_name}`;
}
}

@ -20,21 +20,13 @@ export default class InlineComponent extends Node {
constructor(component: Component, parent, scope, info) {
super(component, parent, scope, info);
component.hasComponents = true;
if (info.name !== 'svelte:component' && info.name !== 'svelte:self') {
component.warn_if_undefined(info, scope);
component.template_references.add(info.name);
}
this.name = info.name;
if (this.name !== 'svelte:self' && this.name !== 'svelte:component') {
if (!component.components.has(this.name)) {
component.error(this, {
code: `missing-component`,
message: `${this.name} component is not defined`
});
}
component.used.components.add(this.name);
}
this.expression = this.name === 'svelte:component'
? new Expression(component, this, scope, info.expression)
: null;

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

@ -1,14 +1,19 @@
import Node from './shared/Node';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
import TemplateScope from './shared/TemplateScope';
export default class ThenBlock extends Node {
block: Block;
scope: TemplateScope;
children: Node[];
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, parent, scope, info.children);
this.scope = scope.child();
this.scope.add(parent.value, parent.expression.dependencies);
this.children = mapChildren(component, parent, this.scope, info.children);
this.warnIfEmptyBlock();
}

@ -1,5 +1,6 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
export default class Transition extends Node {
type: 'Transition';
@ -7,15 +8,10 @@ export default class Transition extends Node {
directive: string;
expression: Expression;
constructor(component, parent, scope, info) {
constructor(component: Component, parent, scope, info) {
super(component, parent, scope, info);
if (!component.transitions.has(info.name)) {
component.error(info, {
code: `missing-transition`,
message: `Missing transition '${info.name}'`
});
}
component.warn_if_undefined(info, scope);
this.name = info.name;
this.directive = info.intro && info.outro ? 'transition' : info.intro ? 'in' : 'out';
@ -33,8 +29,6 @@ export default class Transition extends Node {
});
}
this.component.used.transitions.add(this.name);
this.expression = info.expression
? new Expression(component, this, scope, info.expression)
: null;

@ -2,8 +2,9 @@ import Node from './shared/Node';
import Binding from './Binding';
import EventHandler from './EventHandler';
import flattenReference from '../../utils/flattenReference';
import fuzzymatch from '../validate/utils/fuzzymatch';
import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list';
import Action from './Action';
const validBindings = [
'innerWidth',
@ -17,36 +18,35 @@ const validBindings = [
export default class Window extends Node {
type: 'Window';
handlers: EventHandler[];
bindings: Binding[];
handlers: EventHandler[] = [];
bindings: Binding[] = [];
actions: Action[] = [];
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.handlers = [];
this.bindings = [];
info.attributes.forEach(node => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(component, this, scope, node));
}
else if (node.type === 'Binding') {
if (node.value.type !== 'Identifier') {
const { parts } = flattenReference(node.value);
if (node.expression.type !== 'Identifier') {
const { parts } = flattenReference(node.expression);
component.error(node.value, {
// TODO is this constraint necessary?
component.error(node.expression, {
code: `invalid-binding`,
message: `Bindings on <svelte:window> must be to top-level properties, e.g. '${parts[parts.length - 1]}' rather than '${parts.join('.')}'`
});
}
if (!~validBindings.indexOf(node.name)) {
const match = node.name === 'width'
? 'innerWidth'
: node.name === 'height'
? 'innerHeight'
: fuzzymatch(node.name, validBindings);
const match = (
node.name === 'width' ? 'innerWidth' :
node.name === 'height' ? 'innerHeight' :
fuzzymatch(node.name, validBindings)
);
const message = `'${node.name}' is not a valid binding on <svelte:window>`;
@ -66,6 +66,10 @@ export default class Window extends Node {
this.bindings.push(new Binding(component, this, scope, node));
}
else if (node.type === 'Action') {
this.actions.push(new Action(component, this, scope, node));
}
else {
// TODO there shouldn't be anything else here...
}

@ -2,8 +2,15 @@ import Component from '../../Component';
import { walk } from 'estree-walker';
import isReference from 'is-reference';
import flattenReference from '../../../utils/flattenReference';
import { createScopes } from '../../../utils/annotateWithScopes';
import { createScopes, Scope, extractNames } from '../../../utils/annotateWithScopes';
import { Node } from '../../../interfaces';
import globalWhitelist from '../../../utils/globalWhitelist';
import deindent from '../../../utils/deindent';
import Wrapper from '../../render-dom/wrappers/shared/Wrapper';
import sanitize from '../../../utils/sanitize';
import TemplateScope from './TemplateScope';
import getObject from '../../../utils/getObject';
import { nodes_match } from '../../../utils/nodes_match';
const binaryOperators: Record<string, number> = {
'**': 15,
@ -55,95 +62,182 @@ const precedence: Record<string, (node?: Node) => number> = {
export default class Expression {
component: Component;
owner: Wrapper;
node: any;
snippet: string;
references: Set<string>;
dependencies: Set<string>;
dependencies: Set<string> = new Set();
contextual_dependencies: Set<string> = new Set();
dynamic_dependencies: Set<string> = new Set();
template_scope: TemplateScope;
scope: Scope;
scope_map: WeakMap<Node, Scope>;
is_synthetic: boolean;
declarations: string[] = [];
usesContext = false;
usesEvent = false;
thisReferences: Array<{ start: number, end: number }>;
rendered: string;
constructor(component, parent, scope, info) {
constructor(component: Component, owner: Wrapper, template_scope: TemplateScope, info) {
// TODO revert to direct property access in prod?
Object.defineProperties(this, {
component: {
value: component
},
// TODO remove this, is just for debugging
snippet: {
get: () => {
throw new Error(`cannot access expression.snippet, use expression.render() instead`)
}
}
});
this.node = info;
this.thisReferences = [];
this.snippet = `[✂${info.start}-${info.end}✂]`;
const dependencies = new Set();
this.template_scope = template_scope;
this.owner = owner;
this.is_synthetic = owner.isSynthetic;
const { code, helpers } = component;
const { dependencies, contextual_dependencies, dynamic_dependencies } = this;
let { map, scope: currentScope } = createScopes(info);
let { map, scope } = createScopes(info);
this.scope = scope;
this.scope_map = map;
const isEventHandler = parent.type === 'EventHandler';
const expression = this;
const isSynthetic = parent.isSynthetic;
let function_expression;
function add_dependency(name) {
dependencies.add(name);
if (!function_expression) {
// dynamic_dependencies is used to create `if (changed.foo || ...)`
// conditions — it doesn't apply if the dependency is inside a
// function, and it only applies if the dependency is writable
if (component.instance_script) {
if (component.writable_declarations.has(name)) {
dynamic_dependencies.add(name);
}
} else {
dynamic_dependencies.add(name);
}
}
}
// discover dependencies, but don't change the code yet
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;
scope = map.get(node);
}
if (node.type === 'ThisExpression') {
expression.thisReferences.push(node);
if (!function_expression && /FunctionExpression/.test(node.type)) {
function_expression = node;
}
if (isReference(node, parent)) {
const { name, nodes } = flattenReference(node);
if (name === 'event' && isEventHandler) {
expression.usesEvent = true;
return;
}
if (scope.has(name)) return;
if (globalWhitelist.has(name) && component.declarations.indexOf(name) === -1) return;
if (currentScope.has(name)) return;
if (template_scope.names.has(name)) {
expression.usesContext = true;
if (component.helpers.has(name)) {
let object = node;
while (object.type === 'MemberExpression') object = object.object;
contextual_dependencies.add(name);
component.used.helpers.add(name);
template_scope.dependenciesForName.get(name).forEach(add_dependency);
} else {
add_dependency(name);
component.template_references.add(name);
const alias = component.templateVars.get(`helpers-${name}`);
if (alias !== name) code.overwrite(object.start, object.end, alias);
return;
component.warn_if_undefined(nodes[0], template_scope, true);
}
expression.usesContext = true;
this.skip();
}
},
leave(node) {
if (map.has(node)) {
scope = scope.parent;
}
if (node === function_expression) {
function_expression = null;
}
}
});
}
getPrecedence() {
return this.node.type in precedence ? precedence[this.node.type](this.node) : 0;
}
// TODO move this into a render-dom wrapper?
render() {
if (this.rendered) return this.rendered;
const {
component,
declarations,
scope_map: map,
template_scope,
owner,
is_synthetic
} = this;
let scope = this.scope;
const { code } = component;
if (!isSynthetic) {
// <option> value attribute could be synthetic — avoid double editing
let function_expression;
let pending_assignments = new Set();
let dependencies: Set<string>;
let contextual_dependencies: Set<string>;
// rewrite code as appropriate
walk(this.node, {
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)) {
scope = map.get(node);
}
if (isReference(node, parent)) {
const { name, nodes } = flattenReference(node);
if (scope.has(name)) return;
if (globalWhitelist.has(name) && component.declarations.indexOf(name) === -1) return;
if (function_expression) {
if (template_scope.names.has(name)) {
contextual_dependencies.add(name);
template_scope.dependenciesForName.get(name).forEach(dependency => {
dependencies.add(dependency);
});
} else {
dependencies.add(name);
component.template_references.add(name);
}
} else if (!is_synthetic && !component.hoistable_names.has(name) && !component.imported_declarations.has(name)) {
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);
component.expectedProperties.add(name);
}
if (node.type === 'MemberExpression') {
nodes.forEach(node => {
code.addSourcemapLocation(node.start);
@ -153,25 +247,177 @@ export default class Expression {
this.skip();
}
if (function_expression) {
if (node.type === 'AssignmentExpression') {
const names = node.left.type === 'MemberExpression'
? [getObject(node.left).name]
: extractNames(node.left);
if (node.operator === '=' && nodes_match(node.left, node.right)) {
const dirty = names.filter(name => {
return !scope.declarations.has(name);
});
if (dirty.length) component.has_reactive_assignments = true;
code.overwrite(node.start, node.end, dirty.map(n => `$$make_dirty('${n}')`).join('; '));
} else {
names.forEach(name => {
if (!scope.declarations.has(name)) {
pending_assignments.add(name);
}
});
}
} else if (node.type === 'UpdateExpression') {
const { name } = getObject(node.argument);
if (!scope.declarations.has(name)) {
pending_assignments.add(name);
}
}
} else {
if (node.type === 'AssignmentExpression') {
// TODO should this be a warning/error? `<p>{foo = 1}</p>`
}
if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
function_expression = node;
dependencies = new Set();
contextual_dependencies = new Set();
}
}
},
leave(node: Node, parent: Node) {
if (map.has(node)) currentScope = currentScope.parent;
if (map.has(node)) scope = scope.parent;
if (node === function_expression) {
if (pending_assignments.size > 0) {
if (node.type !== 'ArrowFunctionExpression') {
// this should never happen!
throw new Error(`Well that's odd`);
}
// TOOD optimisation — if this is an event handler,
// the return value doesn't matter
}
const name = component.getUniqueName(
sanitize(get_function_name(node, owner))
);
const args = contextual_dependencies.size > 0
? [`{ ${[...contextual_dependencies].join(', ')} }`]
: [];
let original_params;
if (node.params.length > 0) {
original_params = code.slice(node.params[0].start, node.params[node.params.length - 1].end);
args.push(original_params);
}
let body = code.slice(node.body.start, node.body.end).trim();
if (node.body.type !== 'BlockStatement') {
if (pending_assignments.size > 0) {
const insert = [...pending_assignments].map(name => `$$make_dirty('${name}')`).join('; ');
pending_assignments = new Set();
component.has_reactive_assignments = true;
body = deindent`
{
const $$result = ${body};
${insert}
return $$result;
}
`;
} else {
body = `{\n\treturn ${body};\n}`;
}
}
const fn = deindent`
function ${name}(${args.join(', ')}) ${body}
`;
if (dependencies.size === 0 && contextual_dependencies.size === 0) {
// we can hoist this out of the component completely
component.fully_hoisted.push(fn);
code.overwrite(node.start, node.end, name);
}
else if (contextual_dependencies.size === 0) {
// function can be hoisted inside the component init
component.partly_hoisted.push(fn);
component.declarations.push(name);
component.template_references.add(name);
code.overwrite(node.start, node.end, `ctx.${name}`);
}
else {
// we need a combo block/init recipe
component.partly_hoisted.push(fn);
component.declarations.push(name);
component.template_references.add(name);
code.overwrite(node.start, node.end, name);
declarations.push(deindent`
function ${name}(${original_params ? '...args' : ''}) {
return ctx.${name}(ctx${original_params ? ', ...args' : ''});
}
`);
}
function_expression = null;
dependencies = null;
contextual_dependencies = null;
}
if (/Statement/.test(node.type)) {
if (pending_assignments.size > 0) {
const has_semi = code.original[node.end - 1] === ';';
const insert = (
(has_semi ? ' ' : '; ') +
[...pending_assignments].map(name => `$$make_dirty('${name}')`).join('; ')
);
if (/^(Break|Continue|Return)Statement/.test(node.type)) {
if (node.argument) {
code.overwrite(node.start, node.argument.start, `var $$result = `);
code.appendLeft(node.argument.end, `${insert}; return $$result`);
} else {
code.prependRight(node.start, `${insert}; `);
}
} else if (parent && /(If|For(In|Of)?|While)Statement/.test(parent.type) && node.type !== 'BlockStatement') {
code.prependRight(node.start, '{ ');
code.appendLeft(node.end, `${insert}; }`);
} else {
code.appendLeft(node.end, `${insert};`);
}
component.has_reactive_assignments = true;
pending_assignments = new Set();
}
}
}
});
this.dependencies = dependencies;
return this.rendered = `[✂${this.node.start}-${this.node.end}✂]`;
}
}
getPrecedence() {
return this.node.type in precedence ? precedence[this.node.type](this.node) : 0;
function get_function_name(node, parent) {
if (parent.type === 'EventHandler') {
return `${parent.name}_handler`;
}
overwriteThis(name) {
this.thisReferences.forEach(ref => {
this.component.code.overwrite(ref.start, ref.end, name, {
storeName: true
});
});
if (parent.type === 'Action') {
return `${parent.name}_function`;
}
return 'func';
}

@ -1,6 +1,4 @@
import Component from './../../Component';
import Block from '../../render-dom/Block';
import { trimStart, trimEnd } from '../../../utils/trim';
export default class Node {
readonly start: number;
@ -51,7 +49,7 @@ export default class Node {
}
remount(name: string) {
return `${this.var}.m(${name}._slotted.default, null);`;
return `${this.var}.m(${name}.$$.slotted.default, null);`;
}
warnIfEmptyBlock() {

@ -6,6 +6,7 @@ import Element from '../Element';
import Head from '../Head';
import IfBlock from '../IfBlock';
import InlineComponent from '../InlineComponent';
import Meta from '../Meta';
import MustacheTag from '../MustacheTag';
import RawMustacheTag from '../RawMustacheTag';
import DebugTag from '../DebugTag';
@ -25,6 +26,7 @@ function getConstructor(type): typeof Node {
case 'Head': return Head;
case 'IfBlock': return IfBlock;
case 'InlineComponent': return InlineComponent;
case 'Meta': return Meta;
case 'MustacheTag': return MustacheTag;
case 'RawMustacheTag': return RawMustacheTag;
case 'DebugTag': return DebugTag;

@ -11,7 +11,7 @@ export interface BlockOptions {
renderer?: Renderer;
comment?: string;
key?: string;
bindings?: Map<string, () => string>;
bindings?: Map<string, () => { object: string, property: string, snippet: string }>;
contextOwners?: Map<string, EachBlockWrapper>;
dependencies?: Set<string>;
}
@ -29,7 +29,7 @@ export default class Block {
dependencies: Set<string>;
bindings: Map<string, () => string>;
bindings: Map<string, () => { object: string, property: string, snippet: string }>;
contextOwners: Map<string, EachBlockWrapper>;
builders: {
@ -47,6 +47,8 @@ export default class Block {
destroy: CodeBuilder;
};
event_listeners: string[] = [];
maintainContext: boolean;
hasAnimation: boolean;
hasIntros: boolean;
@ -162,7 +164,10 @@ export default class Block {
) {
this.addVariable(name);
this.builders.create.addLine(`${name} = ${renderStatement};`);
this.builders.claim.addLine(`${name} = ${claimStatement || renderStatement};`);
if (this.renderer.options.hydratable) {
this.builders.claim.addLine(`${name} = ${claimStatement || renderStatement};`);
}
if (parentNode) {
this.builders.mount.addLine(`@append(${parentNode}, ${name});`);
@ -212,7 +217,7 @@ export default class Block {
return new Block(Object.assign({}, this, { key: null }, options, { parent: this }));
}
toString() {
getContents(localKey?: string) {
const { dev } = this.renderer.options;
if (this.hasIntroMethod || this.hasOutroMethod) {
@ -231,11 +236,33 @@ export default class Block {
this.builders.mount.addLine(`${this.autofocus}.focus();`);
}
if (this.event_listeners.length > 0) {
this.addVariable('#dispose');
if (this.event_listeners.length === 1) {
this.builders.hydrate.addLine(
`#dispose = ${this.event_listeners[0]};`
);
this.builders.destroy.addLine(
`#dispose();`
)
} else {
this.builders.hydrate.addBlock(deindent`
#dispose = [
${this.event_listeners.join(',\n')}
];
`);
this.builders.destroy.addLine(
`@run_all(#dispose);`
);
}
}
const properties = new CodeBuilder();
let localKey;
if (this.key) {
localKey = this.getUniqueName('key');
if (localKey) {
properties.addBlock(`key: ${localKey},`);
}
@ -245,7 +272,7 @@ export default class Block {
}
if (this.builders.create.isEmpty() && this.builders.hydrate.isEmpty()) {
properties.addBlock(`c: @noop,`);
properties.addLine(`c: @noop,`);
} else {
const hydrate = !this.builders.hydrate.isEmpty() && (
this.renderer.options.hydratable
@ -261,14 +288,14 @@ export default class Block {
`);
}
if (this.renderer.options.hydratable) {
if (this.renderer.options.hydratable || !this.builders.claim.isEmpty()) {
if (this.builders.claim.isEmpty() && this.builders.hydrate.isEmpty()) {
properties.addBlock(`l: @noop,`);
properties.addLine(`l: @noop,`);
} else {
properties.addBlock(deindent`
${dev ? 'l: function claim' : 'l'}(nodes) {
${this.builders.claim}
${!this.builders.hydrate.isEmpty() && `this.h();`}
${this.renderer.options.hydratable && !this.builders.hydrate.isEmpty() && `this.h();`}
},
`);
}
@ -283,7 +310,7 @@ export default class Block {
}
if (this.builders.mount.isEmpty()) {
properties.addBlock(`m: @noop,`);
properties.addLine(`m: @noop,`);
} else {
properties.addBlock(deindent`
${dev ? 'm: function mount' : 'm'}(#target, anchor) {
@ -294,11 +321,11 @@ export default class Block {
if (this.hasUpdateMethod || this.maintainContext) {
if (this.builders.update.isEmpty() && !this.maintainContext) {
properties.addBlock(`p: @noop,`);
properties.addLine(`p: @noop,`);
} else {
properties.addBlock(deindent`
${dev ? 'p: function update' : 'p'}(changed, ${this.maintainContext ? '_ctx' : 'ctx'}) {
${this.maintainContext && `ctx = _ctx;`}
${dev ? 'p: function update' : 'p'}(changed, ${this.maintainContext ? 'new_ctx' : 'ctx'}) {
${this.maintainContext && `ctx = new_ctx;`}
${this.builders.update}
},
`);
@ -323,7 +350,7 @@ export default class Block {
if (this.hasIntroMethod || this.hasOutroMethod) {
if (this.builders.mount.isEmpty()) {
properties.addBlock(`i: @noop,`);
properties.addLine(`i: @noop,`);
} else {
properties.addBlock(deindent`
${dev ? 'i: function intro' : 'i'}(#target, anchor) {
@ -335,7 +362,7 @@ export default class Block {
}
if (this.builders.outro.isEmpty()) {
properties.addBlock(`o: @run,`);
properties.addLine(`o: @run,`);
} else {
properties.addBlock(deindent`
${dev ? 'o: function outro' : 'o'}(#outrocallback) {
@ -350,7 +377,7 @@ export default class Block {
}
if (this.builders.destroy.isEmpty()) {
properties.addBlock(`d: @noop`);
properties.addLine(`d: @noop`);
} else {
properties.addBlock(deindent`
${dev ? 'd: function destroy' : 'd'}(detach) {
@ -359,22 +386,32 @@ export default class Block {
`);
}
return deindent`
${this.variables.size > 0 &&
`var ${Array.from(this.variables.keys())
.map(key => {
const init = this.variables.get(key);
return init !== undefined ? `${key} = ${init}` : key;
})
.join(', ')};`}
${!this.builders.init.isEmpty() && this.builders.init}
return {
${properties}
};
`.replace(/(#+)(\w*)/g, (match: string, sigil: string, name: string) => {
return sigil === '#' ? this.alias(name) : sigil.slice(1) + name;
});
}
toString() {
const localKey = this.key && this.getUniqueName('key');
return deindent`
${this.comment && `// ${escape(this.comment)}`}
function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, ctx) {
${this.variables.size > 0 &&
`var ${Array.from(this.variables.keys())
.map(key => {
const init = this.variables.get(key);
return init !== undefined ? `${key} = ${init}` : key;
})
.join(', ')};`}
${!this.builders.init.isEmpty() && this.builders.init}
return {
${properties}
};
function ${this.name}(#component, ${this.key ? `${localKey}, ` : ''}ctx) {
${this.getContents(localKey)}
}
`.replace(/(#+)(\w*)/g, (match: string, sigil: string, name: string) => {
return sigil === '#' ? this.alias(name) : sigil.slice(1) + name;

@ -17,12 +17,10 @@ export default class Renderer {
block: Block;
fragment: FragmentWrapper;
usedNames: Set<string>;
fileVar: string;
hasIntroTransitions: boolean;
hasOutroTransitions: boolean;
hasComplexBindings: boolean;
constructor(component: Component, options: CompileOptions) {
this.component = component;
@ -32,7 +30,6 @@ export default class Renderer {
this.readonly = new Set();
this.slots = new Set();
this.usedNames = new Set();
this.fileVar = options.dev && this.component.getUniqueName('file');
// initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
@ -43,7 +40,7 @@ export default class Renderer {
// main block
this.block = new Block({
renderer: this,
name: '@create_main_fragment',
name: null,
key: null,
bindings: new Map(),
@ -64,14 +61,14 @@ export default class Renderer {
null
);
this.blocks.push(this.block);
this.blocks.forEach(block => {
if (typeof block !== 'string') {
block.assignVariableNames();
}
});
this.block.assignVariableNames();
this.fragment.render(this.block, null, 'nodes');
}
}

@ -1,93 +1,42 @@
import deindent from '../../utils/deindent';
import { stringify, escape } from '../../utils/stringify';
import CodeBuilder from '../../utils/CodeBuilder';
import globalWhitelist from '../../utils/globalWhitelist';
import Component from '../Component';
import Renderer from './Renderer';
import { CompileOptions } from '../../interfaces';
import { walk } from 'estree-walker';
import stringifyProps from '../../utils/stringifyProps';
import addToSet from '../../utils/addToSet';
import getObject from '../../utils/getObject';
import { extractNames } from '../../utils/annotateWithScopes';
import { nodes_match } from '../../utils/nodes_match';
export default function dom(
component: Component,
options: CompileOptions
) {
const format = options.format || 'es';
const {
computations,
name,
templateProperties
} = component;
const { name, code } = component;
const renderer = new Renderer(component, options);
const { block } = renderer;
if (component.options.nestedTransitions) {
block.hasOutroMethod = true;
}
block.hasOutroMethod = true;
// prevent fragment being created twice (#1063)
if (options.customElement) block.builders.create.addLine(`this.c = @noop;`);
const builder = new CodeBuilder();
const computationBuilder = new CodeBuilder();
const computationDeps = new Set();
if (computations.length) {
computations.forEach(({ key, deps, hasRestParam }) => {
if (renderer.readonly.has(key)) {
// <svelte:window> bindings
throw new Error(
`Cannot have a computed value '${key}' that clashes with a read-only property`
);
}
renderer.readonly.add(key);
if (deps) {
deps.forEach(dep => {
computationDeps.add(dep);
});
const condition = `${deps.map(dep => `changed.${dep}`).join(' || ')}`;
const statement = `if (this._differs(state.${key}, (state.${key} = %computed-${key}(state)))) changed.${key} = true;`;
computationBuilder.addConditional(condition, statement);
} else {
// computed property depends on entire state object —
// these must go at the end
computationBuilder.addLine(
`if (this._differs(state.${key}, (state.${key} = %computed-${key}(@exclude(state, "${key}"))))) changed.${key} = true;`
);
}
});
}
if (component.javascript) {
const componentDefinition = new CodeBuilder();
component.declarations.forEach(declaration => {
componentDefinition.addBlock(declaration.block);
});
const js = (
component.javascript[0] +
componentDefinition +
component.javascript[1]
);
builder.addBlock(js);
}
if (component.options.dev) {
builder.addLine(`const ${renderer.fileVar} = ${JSON.stringify(component.file)};`);
}
const css = component.stylesheet.render(options.filename, !component.customElement);
const css = component.stylesheet.render(options.filename, !options.customElement);
const styles = component.stylesheet.hasStyles && stringify(options.dev ?
`${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` :
css.code, { onlyEscapeAtSymbol: true });
if (styles && component.options.css !== false && !component.customElement) {
if (styles && component.options.css !== false && !options.customElement) {
builder.addBlock(deindent`
function @add_css() {
var style = @createElement("style");
@ -106,234 +55,315 @@ export default function dom(
builder.addBlock(block.toString());
});
const sharedPath: string = options.shared === true
? 'svelte/shared.js'
: options.shared || '';
const proto = sharedPath
? `@proto`
: deindent`
{
${['destroy', 'get', 'fire', 'on', 'set', '_set', '_stage', '_mount', '_differs']
.map(n => `${n}: @${n}`)
.join(',\n')}
}`;
const debugName = `<${component.customElement ? component.tag : name}>`;
// generate initial state object
const expectedProperties = Array.from(component.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = expectedProperties.filter(prop => prop[0] === '$');
const initialState = [];
if (globals.length > 0) {
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
}
const refs = Array.from(component.refs);
if (storeProps.length > 0) {
initialState.push(`this.store._init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`);
if (options.dev && !options.hydratable) {
block.builders.claim.addLine(
'throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option");'
);
}
if (templateProperties.data) {
initialState.push(`%data()`);
} else if (globals.length === 0 && storeProps.length === 0) {
initialState.push('{}');
}
// TODO injecting CSS this way is kinda dirty. Maybe it should be an
// explicit opt-in, or something?
const should_add_css = (
!options.customElement &&
component.stylesheet.hasStyles &&
options.css !== false
);
const props = component.props.filter(x => component.writable_declarations.has(x.name));
const set = component.meta.props || props.length > 0
? deindent`
$$props => {
${component.meta.props && deindent`
if (!${component.meta.props}) ${component.meta.props} = {};
@assign(${component.meta.props}, $$props);
$$make_dirty('${component.meta.props_object}');
`}
${props.map(prop =>
`if ('${prop.as}' in $$props) ${prop.name} = $$props.${prop.as};`)}
}
`
: null;
const inject_refs = refs.length > 0
? deindent`
$$refs => {
${refs.map(name => `${name} = $$refs.${name};`)}
}
`
: null;
initialState.push(`options.data`);
const hasInitHooks = !!(templateProperties.oncreate || templateProperties.onstate || templateProperties.onupdate);
const constructorBody = deindent`
${options.dev && deindent`
this._debugName = '${debugName}';
${!component.customElement && deindent`
if (!options || (!options.target && !options.root)) {
throw new Error("'target' is a required option");
}`}
${storeProps.length > 0 && !templateProperties.store && deindent`
if (!options.store) {
throw new Error("${debugName} references store properties, but no store was provided");
}`}
`}
@init(this, options);
${templateProperties.store && `this.store = %store();`}
${component.refs.size > 0 && `this.refs = {};`}
this._state = ${initialState.reduce((state, piece) => `@assign(${state}, ${piece})`)};
${storeProps.length > 0 && `this.store._add(this, [${storeProps.map(prop => `"${prop.slice(1)}"`)}]);`}
${renderer.metaBindings}
${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`}
${options.dev &&
Array.from(component.expectedProperties).map(prop => {
if (globalWhitelist.has(prop)) return;
if (computations.find(c => c.key === prop)) return;
const message = component.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 (component.customElement) conditions.push(`!('${prop}' in this.attributes)`);
return `if (${conditions.join(' && ')}) console.warn("${message}");`
})}
${renderer.bindingGroups.length &&
`this._bindingGroups = [${Array(renderer.bindingGroups.length).fill('[]').join(', ')}];`}
this._intro = ${component.options.skipIntroByDefault ? '!!options.intro' : 'true'};
${templateProperties.onstate && `this._handlers.state = [%onstate];`}
${templateProperties.onupdate && `this._handlers.update = [%onupdate];`}
${(templateProperties.ondestroy || storeProps.length) && (
`this._handlers.destroy = [${
[templateProperties.ondestroy && `%ondestroy`, storeProps.length && `@removeFromStore`].filter(Boolean).join(', ')
}];`
)}
${renderer.slots.size && `this._slotted = options.slots || {};`}
${component.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>\`;`}
` :
(component.stylesheet.hasStyles && options.css !== false &&
`if (!document.getElementById("${component.stylesheet.id}-style")) @add_css();`)
const body = [];
const not_equal = component.options.immutable ? `@not_equal` : `@safe_not_equal`;
let dev_props_check;
component.props.forEach(x => {
if (component.imported_declarations.has(x.name) || component.hoistable_names.has(x.name)) {
body.push(deindent`
get ${x.as}() {
return ${x.name};
}
`);
} else {
body.push(deindent`
get ${x.as}() {
return this.$$.get().${x.name};
}
`);
}
${templateProperties.onstate && `%onstate.call(this, { changed: @assignTrue({}, this._state), current: this._state });`}
this._fragment = @create_main_fragment(this, this._state);
${hasInitHooks && deindent`
this.root._oncreate.push(() => {
${templateProperties.oncreate && `%oncreate.call(this);`}
this.fire("update", { changed: @assignTrue({}, this._state), current: this._state });
});
`}
${component.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) {
${component.options.hydratable
? deindent`
var nodes = @children(options.target);
options.hydrate ? this._fragment.l(nodes) : this._fragment.c();
nodes.forEach(@detachNode);` :
deindent`
${options.dev &&
`if (options.hydrate) throw new Error("options.hydrate only works if the component was compiled with the \`hydratable: true\` option");`}
this._fragment.c();`}
this._mount(options.target, options.anchor);
${(component.hasComponents || renderer.hasComplexBindings || hasInitHooks || renderer.hasIntroTransitions) &&
`@flush(this);`}
}
`}
if (component.writable_declarations.has(x.as) && !renderer.readonly.has(x.as)) {
body.push(deindent`
set ${x.as}(value) {
this.$set({ ${x.name}: value });
@flush();
}
`);
} else if (component.options.dev) {
body.push(deindent`
set ${x.as}(value) {
throw new Error("<${component.tag}>: Cannot set read-only property '${x.as}'");
}
`);
}
});
${component.options.skipIntroByDefault && `this._intro = true;`}
`;
if (component.options.dev) {
// TODO check no uunexpected props were passed, as well as
// checking that expected ones were passed
const expected = component.props
.map(x => x.name)
.filter(name => !component.initialised_declarations.has(name));
if (expected.length) {
dev_props_check = deindent`
const state = this.$$.get();
${expected.map(name => deindent`
if (state.${name} === undefined${options.customElement && ` && !('${name}' in this.attributes)`}) {
console.warn("<${component.tag}> was created without expected data property '${name}'");
}`)}
`;
}
}
if (component.customElement) {
const props = component.props || Array.from(component.expectedProperties);
// instrument assignments
if (component.instance_script) {
let scope = component.instance_scope;
let map = component.instance_scope_map;
builder.addBlock(deindent`
class ${name} extends HTMLElement {
constructor(options = {}) {
super();
${constructorBody}
let pending_assignments = new Set();
walk(component.instance_script.content, {
enter: (node, parent) => {
if (map.has(node)) {
scope = map.get(node);
}
},
static get observedAttributes() {
return ${JSON.stringify(props)};
leave(node, parent) {
if (map.has(node)) {
scope = scope.parent;
}
${props.map(prop => deindent`
get ${prop}() {
return this.get().${prop};
}
if (node.type === 'AssignmentExpression') {
const names = node.left.type === 'MemberExpression'
? [getObject(node.left).name]
: extractNames(node.left);
set ${prop}(value) {
this.set({ ${prop}: value });
}
`).join('\n\n')}
if (node.operator === '=' && nodes_match(node.left, node.right)) {
const dirty = names.filter(name => {
return scope.findOwner(name) === component.instance_scope;
});
if (dirty.length) component.has_reactive_assignments = true;
${renderer.slots.size && deindent`
connectedCallback() {
Object.keys(this._slotted).forEach(key => {
this.appendChild(this._slotted[key]);
code.overwrite(node.start, node.end, dirty.map(n => `$$make_dirty('${n}')`).join('; '));
} else {
names.forEach(name => {
if (scope.findOwner(name) === component.instance_scope) {
pending_assignments.add(name);
component.has_reactive_assignments = true;
}
});
}`}
}
}
else if (node.type === 'UpdateExpression') {
const { name } = getObject(node.argument);
attributeChangedCallback(attr, oldValue, newValue) {
this.set({ [attr]: newValue });
if (scope.findOwner(name) === component.instance_scope) {
pending_assignments.add(name);
component.has_reactive_assignments = true;
}
}
${(component.hasComponents || renderer.hasComplexBindings || templateProperties.oncreate || renderer.hasIntroTransitions) && deindent`
connectedCallback() {
@flush(this);
if (pending_assignments.size > 0) {
if (node.type === 'ArrowFunctionExpression') {
const insert = [...pending_assignments].map(name => `$$make_dirty('${name}')`).join(';');
pending_assignments = new Set();
code.prependRight(node.body.start, `{ const $$result = `);
code.appendLeft(node.body.end, `; ${insert}; return $$result; }`);
pending_assignments = new Set();
}
`}
}
@assign(${name}.prototype, ${proto});
${templateProperties.methods && `@assign(${name}.prototype, %methods);`}
@assign(${name}.prototype, {
_mount(target, anchor) {
target.insertBefore(this, anchor);
else if (/Statement/.test(node.type)) {
const insert = [...pending_assignments].map(name => `$$make_dirty('${name}')`).join('; ');
if (/^(Break|Continue|Return)Statement/.test(node.type)) {
if (node.argument) {
code.overwrite(node.start, node.argument.start, `var $$result = `);
code.appendLeft(node.argument.end, `; ${insert}; return $$result`);
} else {
code.prependRight(node.start, `${insert}; `);
}
} else if (parent && /(If|For(In|Of)?|While)Statement/.test(parent.type) && node.type !== 'BlockStatement') {
code.prependRight(node.start, '{ ');
code.appendLeft(node.end, `${code.original[node.end - 1] === ';' ? '' : ';'} ${insert}; }`);
} else {
code.appendLeft(node.end, `${code.original[node.end - 1] === ';' ? '' : ';'} ${insert};`);
}
pending_assignments = new Set();
}
}
});
}
});
customElements.define("${component.tag}", ${name});
`);
} else {
if (pending_assignments.size > 0) {
throw new Error(`TODO this should not happen!`);
}
}
const args = ['$$self'];
if (component.props.length > 0 || component.has_reactive_assignments) args.push('$$props');
if (component.has_reactive_assignments) args.push('$$make_dirty');
builder.addBlock(deindent`
function create_fragment(${component.alias('component')}, ctx) {
${block.getContents()}
}
${component.module_javascript}
${component.fully_hoisted.length > 0 && component.fully_hoisted.join('\n\n')}
`);
const filtered_declarations = component.declarations.filter(name => {
if (component.hoistable_names.has(name)) return false;
if (component.imported_declarations.has(name)) return false;
if (component.props.find(p => p.as === name)) return true;
return component.template_references.has(name);
});
const filtered_props = component.props.filter(prop => {
if (component.hoistable_names.has(prop.name)) return false;
if (component.imported_declarations.has(prop.name)) return false;
return true;
});
const has_definition = (
component.javascript ||
filtered_props.length > 0 ||
component.partly_hoisted.length > 0 ||
filtered_declarations.length > 0 ||
component.reactive_declarations.length > 0
);
const definition = has_definition
? component.alias('define')
: '@noop';
const all_reactive_dependencies = new Set();
component.reactive_declarations.forEach(d => {
addToSet(all_reactive_dependencies, d.dependencies);
});
const user_code = component.javascript || (
component.ast.js.length === 0 && filtered_props.length > 0
? `let { ${filtered_props.map(x => x.name === x.as ? x.as : `${x.as}: ${x.name}`).join(', ')} } = $$props;`
: null
);
if (has_definition) {
builder.addBlock(deindent`
function ${name}(options) {
${constructorBody}
}
function ${definition}(${args.join(', ')}) {
${user_code}
${component.partly_hoisted.length > 0 && component.partly_hoisted.join('\n\n')}
${filtered_declarations.length > 0 && `$$self.$$.get = () => (${stringifyProps(filtered_declarations)});`}
${set && `$$self.$$.set = ${set};`}
@assign(${name}.prototype, ${proto});
${templateProperties.methods && `@assign(${name}.prototype, %methods);`}
${component.reactive_declarations.length > 0 && deindent`
$$self.$$.update = ($$dirty = { ${Array.from(all_reactive_dependencies).map(n => `${n}: 1`).join(', ')} }) => {
${component.reactive_declarations.map(d => deindent`
if (${Array.from(d.dependencies).map(n => `$$dirty.${n}`).join(' || ')}) ${d.snippet}`)}
};
`}
${inject_refs && `$$self.$$.inject_refs = ${inject_refs};`}
}
`);
}
const immutable = templateProperties.immutable ? templateProperties.immutable.value.value : options.immutable;
if (options.customElement) {
builder.addBlock(deindent`
class ${name} extends @SvelteElement {
constructor(options) {
super();
builder.addBlock(deindent`
${options.dev && deindent`
${name}.prototype._checkReadOnly = function _checkReadOnly(newState) {
${Array.from(renderer.readonly).map(
prop =>
`if ('${prop}' in newState && !this._updatingReadonlyProperty) throw new Error("${debugName}: Cannot set read-only property '${prop}'");`
)}
};
`}
${computations.length ? deindent`
${name}.prototype._recompute = function _recompute(changed, state) {
${computationBuilder}
${css.code && `this.shadowRoot.innerHTML = \`<style>${escape(css.code, { onlyEscapeAtSymbol: true }).replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
@init(this, { target: this.shadowRoot }, ${definition}, create_fragment, ${not_equal});
${dev_props_check}
if (options) {
if (options.target) {
@insert(options.target, this, options.anchor);
}
${(component.props.length > 0 || component.meta.props) && deindent`
if (options.props) {
this.$set(options.props);
@flush();
}`}
}
}
static get observedAttributes() {
return ${JSON.stringify(component.props.map(x => x.as))};
}
${body.length > 0 && body.join('\n\n')}
}
` : (!sharedPath && `${name}.prototype._recompute = @noop;`)}
${templateProperties.setup && `%setup(${name});`}
customElements.define("${component.tag}", ${name});
`);
} else {
const superclass = options.dev ? 'SvelteComponentDev' : 'SvelteComponent';
${templateProperties.preload && `${name}.preload = %preload;`}
builder.addBlock(deindent`
class ${name} extends @${superclass} {
constructor(options) {
super(${options.dev && `options`});
${should_add_css && `if (!document.getElementById("${component.stylesheet.id}-style")) @add_css();`}
@init(this, options, ${definition}, create_fragment, ${not_equal});
${immutable && `${name}.prototype._differs = @_differsImmutable;`}
`);
${dev_props_check}
}
let result = builder.toString();
${body.length > 0 && body.join('\n\n')}
}
`);
}
return component.generate(result, options, {
banner: `/* ${component.file ? `${component.file} ` : ``}generated by Svelte v${"__VERSION__"} */`,
sharedPath,
name,
format,
});
return builder.toString();
}

@ -67,7 +67,7 @@ export default class AwaitBlockWrapper extends Wrapper {
this.cannotUseInnerHTML();
block.addDependencies(this.node.expression.dependencies);
block.addDependencies(this.node.expression.dynamic_dependencies);
let isDynamic = false;
let hasIntros = false;
@ -112,7 +112,7 @@ export default class AwaitBlockWrapper extends Wrapper {
this.then.block.hasOutroMethod = hasOutros;
this.catch.block.hasOutroMethod = hasOutros;
if (hasOutros && this.renderer.options.nestedTransitions) {
if (hasOutros) {
block.addOutro();
}
}
@ -125,7 +125,7 @@ export default class AwaitBlockWrapper extends Wrapper {
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
const updateMountNode = this.getUpdateMountNode(anchor);
const { snippet } = this.node.expression;
const snippet = this.node.expression.render();
const info = block.getUniqueName(`info`);
const promise = block.getUniqueName(`promise`);
@ -160,7 +160,7 @@ export default class AwaitBlockWrapper extends Wrapper {
${info}.block.c();
`);
if (parentNodes) {
if (parentNodes && this.renderer.options.hydratable) {
block.builders.claim.addBlock(deindent`
${info}.block.l(${parentNodes});
`);
@ -207,7 +207,7 @@ export default class AwaitBlockWrapper extends Wrapper {
`);
}
if (this.pending.block.hasOutroMethod && this.renderer.options.nestedTransitions) {
if (this.pending.block.hasOutroMethod) {
const countdown = block.getUniqueName('countdown');
block.builders.outro.addBlock(deindent`
const ${countdown} = @callAfter(#outrocallback, 3);

@ -45,7 +45,7 @@ export default class DebugTagWrapper extends Wrapper {
const dependencies = new Set();
this.node.expressions.forEach(expression => {
addToSet(dependencies, expression.dependencies);
addToSet(dependencies, expression.dynamic_dependencies);
});
const condition = [...dependencies].map(d => `changed.${d}`).join(' || ');

@ -1,6 +1,4 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Node from '../../nodes/shared/Node';
import Wrapper from './shared/Wrapper';
import deindent from '../../../utils/deindent';
import Document from '../../nodes/Document';
@ -8,55 +6,17 @@ import Document from '../../nodes/Document';
export default class DocumentWrapper extends Wrapper {
node: Document;
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: Node) {
super(renderer, block, parent, node);
}
render(block: Block, parentNode: string, parentNodes: string) {
const { renderer } = this;
const { component } = renderer;
this.node.handlers.forEach(handler => {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
component.addSourcemapLocations(handler.expression);
const isCustomEvent = component.events.has(handler.name);
let usesState = handler.dependencies.size > 0;
handler.render(component, block, 'document', false); // TODO hoist?
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);
block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${handler.name}.call(#component, document, function(event) {
${handlerBody}
});
`);
const snippet = handler.render();
block.builders.destroy.addLine(deindent`
${handlerName}.destroy();
`);
} else {
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
${handlerBody}
}
document.addEventListener("${handler.name}", ${handlerName});
`);
block.builders.init.addBlock(deindent`
document.addEventListener("${handler.name}", ${snippet});
`);
block.builders.destroy.addBlock(deindent`
document.removeEventListener("${handler.name}", ${handlerName});
`);
}
block.builders.destroy.addBlock(deindent`
document.removeEventListener("${handler.name}", ${snippet});
`);
});
}
}

@ -80,8 +80,8 @@ export default class EachBlockWrapper extends Wrapper {
super(renderer, block, parent, node);
this.cannotUseInnerHTML();
const { dependencies } = node.expression;
block.addDependencies(dependencies);
const { dynamic_dependencies } = node.expression;
block.addDependencies(dynamic_dependencies);
this.block = block.child({
comment: createDebuggingComment(this.node, this.renderer.component),
@ -101,7 +101,11 @@ export default class EachBlockWrapper extends Wrapper {
this.block.contextOwners.set(prop.key.name, this);
// TODO this doesn't feel great
this.block.bindings.set(prop.key.name, () => `ctx.${this.vars.each_block_value}[ctx.${this.indexName}]${prop.tail}`);
this.block.bindings.set(prop.key.name, () => ({
object: this.vars.each_block_value,
property: this.indexName,
snippet: `${this.vars.each_block_value}[${this.indexName}]${prop.tail}`
}));
});
if (this.node.index) {
@ -173,7 +177,7 @@ export default class EachBlockWrapper extends Wrapper {
if (this.hasBinding) this.contextProps.push(`child_ctx.${this.vars.each_block_value} = list;`);
if (this.hasBinding || this.node.index) this.contextProps.push(`child_ctx.${this.indexName} = i;`);
const { snippet } = this.node.expression;
const snippet = this.node.expression.render();
block.builders.init.addLine(`var ${this.vars.each_block_value} = ${snippet};`);
@ -295,7 +299,7 @@ export default class EachBlockWrapper extends Wrapper {
}
block.builders.init.addBlock(deindent`
const ${get_key} = ctx => ${this.node.key.snippet};
const ${get_key} = ctx => ${this.node.key.render()};
for (var #i = 0; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
let child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i);
@ -312,7 +316,7 @@ export default class EachBlockWrapper extends Wrapper {
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].c();
`);
if (parentNodes) {
if (parentNodes && this.renderer.options.hydratable) {
block.builders.claim.addBlock(deindent`
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].l(${parentNodes});
`);
@ -340,7 +344,7 @@ export default class EachBlockWrapper extends Wrapper {
${this.node.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].a();`}
`);
if (this.block.hasOutros && this.renderer.component.options.nestedTransitions) {
if (this.block.hasOutros) {
const countdown = block.getUniqueName('countdown');
block.builders.outro.addBlock(deindent`
const ${countdown} = @callAfter(#outrocallback, ${blocks}.length);
@ -385,7 +389,7 @@ export default class EachBlockWrapper extends Wrapper {
}
`);
if (parentNodes) {
if (parentNodes && this.renderer.options.hydratable) {
block.builders.claim.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].l(${parentNodes});
@ -422,7 +426,6 @@ export default class EachBlockWrapper extends Wrapper {
`);
}
// TODO do this for keyed blocks as well
const condition = Array.from(allDependencies)
.map(dependency => `changed.${dependency}`)
.join(' || ');
@ -472,22 +475,26 @@ export default class EachBlockWrapper extends Wrapper {
`;
}
block.builders.update.addBlock(deindent`
if (${condition}) {
${this.vars.each_block_value} = ${snippet};
const update = deindent`
${this.vars.each_block_value} = ${snippet};
for (var #i = ${start}; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
const child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i);
for (var #i = ${start}; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
const child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i);
${forLoopBody}
}
${forLoopBody}
}
${destroy}
${destroy}
`;
block.builders.update.addBlock(deindent`
if (${condition}) {
${update}
}
`);
}
if (outroBlock && this.renderer.component.options.nestedTransitions) {
if (outroBlock) {
const countdown = block.getUniqueName('countdown');
block.builders.outro.addBlock(deindent`
${iterations} = ${iterations}.filter(Boolean);
@ -501,6 +508,6 @@ export default class EachBlockWrapper extends Wrapper {
remount(name: string) {
// TODO consider keyed blocks
return `for (var #i = 0; #i < ${this.vars.iterations}.length; #i += 1) ${this.vars.iterations}[#i].m(${name}._slotted.default, null);`;
return `for (var #i = 0; #i < ${this.vars.iterations}.length; #i += 1) ${this.vars.iterations}[#i].m(${name}.$$.slotted.default, null);`;
}
}

@ -78,7 +78,7 @@ export default class AttributeWrapper {
// DRY it out if that's possible without introducing crazy indirection
if (this.node.chunks.length === 1) {
// single {tag} — may be a non-string
value = this.node.chunks[0].snippet;
value = this.node.chunks[0].render();
} else {
// '{foo} {bar}' — treat as string concatenation
value =
@ -89,8 +89,8 @@ export default class AttributeWrapper {
return stringify(chunk.data);
} else {
return chunk.getPrecedence() <= 13
? `(${chunk.snippet})`
: chunk.snippet;
? `(${chunk.render()})`
: chunk.render();
}
})
.join(' + ');
@ -99,7 +99,7 @@ export default class AttributeWrapper {
const isSelectValueAttribute =
name === 'value' && element.node.name === 'select';
const shouldCache = this.node.shouldCache || isSelectValueAttribute;
const shouldCache = (this.node.shouldCache || isSelectValueAttribute);
const last = shouldCache && block.getUniqueName(
`${element.var}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
@ -168,9 +168,9 @@ export default class AttributeWrapper {
const updateCachedValue = `${last} !== (${last} = ${value})`;
const condition = shouldCache ?
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
changedCheck;
const condition = shouldCache
? (dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue)
: changedCheck;
block.builders.update.addConditional(
condition,
@ -215,7 +215,7 @@ export default class AttributeWrapper {
return `="${value.map(chunk => {
return chunk.type === 'Text'
? chunk.data.replace(/"/g, '\\"')
: `\${${chunk.snippet}}`
: `\${${chunk.render()}}`
})}"`;
}
}

@ -6,7 +6,7 @@ import Block from '../../Block';
import Node from '../../../nodes/shared/Node';
import Renderer from '../../Renderer';
import flattenReference from '../../../../utils/flattenReference';
import getTailSnippet from '../../../../utils/getTailSnippet';
import { get_tail } from '../../../../utils/get_tail_snippet';
// TODO this should live in a specific binding
const readOnlyMediaAttributes = new Set([
@ -31,14 +31,14 @@ export default class BindingWrapper {
this.node = node;
this.parent = parent;
const { dependencies } = this.node.value;
const { dynamic_dependencies } = this.node.expression;
block.addDependencies(dependencies);
block.addDependencies(dynamic_dependencies);
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
if (parent.node.name === 'select') {
parent.selectBindingDependencies = dependencies;
dependencies.forEach((prop: string) => {
parent.selectBindingDependencies = dynamic_dependencies;
dynamic_dependencies.forEach((prop: string) => {
parent.renderer.component.indirectDependencies.set(prop, new Set());
});
}
@ -46,7 +46,7 @@ export default class BindingWrapper {
if (node.isContextual) {
// we need to ensure that the each block creates a context including
// the list and the index, if they're not otherwise referenced
const { name } = getObject(this.node.value.node);
const { name } = getObject(this.node.expression.node);
const eachBlock = block.contextOwners.get(name);
eachBlock.hasBinding = true;
@ -73,17 +73,22 @@ export default class BindingWrapper {
let updateConditions: string[] = [];
const { name } = getObject(this.node.value.node);
const { snippet } = this.node.value;
const { name } = getObject(this.node.expression.node);
const snippet = this.node.expression.render();
// TODO unfortunate code is necessary because we need to use `ctx`
// inside the fragment, but not inside the <script>
const contextless_snippet = this.parent.renderer.component.source.slice(this.node.expression.node.start, this.node.expression.node.end);
// 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 = new Set(this.node.value.dependencies);
const dependencies = new Set(this.node.expression.dependencies);
this.node.value.dependencies.forEach((prop: string) => {
this.node.expression.dependencies.forEach((prop: string) => {
const indirectDependencies = renderer.component.indirectDependencies.get(prop);
if (indirectDependencies) {
indirectDependencies.forEach(indirectDependency => {
@ -94,7 +99,7 @@ export default class BindingWrapper {
// view to model
const valueFromDom = getValueFromDom(renderer, this.parent, this);
const handler = getEventHandler(this, renderer, block, name, snippet, dependencies, valueFromDom);
const handler = getEventHandler(this, renderer, block, name, contextless_snippet, valueFromDom);
// model to view
let updateDom = getDomUpdater(parent, this, snippet);
@ -102,14 +107,14 @@ export default class BindingWrapper {
// special cases
if (this.node.name === 'group') {
const bindingGroup = getBindingGroup(renderer, this.node.value.node);
const bindingGroup = getBindingGroup(renderer, this.node.expression.node);
block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${parent.var});`
`(#component.$$.binding_groups[${bindingGroup}] || (#component.$$.binding_groups[${bindingGroup}] = [])).push(${parent.var});`
);
block.builders.destroy.addLine(
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${parent.var}), 1);`
`#component.$$.binding_groups[${bindingGroup}].splice(#component.$$.binding_groups[${bindingGroup}].indexOf(${parent.var}), 1);`
);
}
@ -135,7 +140,7 @@ export default class BindingWrapper {
updateDom = null;
}
const dependencyArray = [...this.node.value.dependencies]
const dependencyArray = [...this.node.expression.dynamic_dependencies]
if (dependencyArray.length === 1) {
updateConditions.push(`changed.${dependencyArray[0]}`)
@ -148,13 +153,16 @@ export default class BindingWrapper {
return {
name: this.node.name,
object: name,
handler: handler,
handler,
snippet,
usesContext: handler.usesContext,
updateDom: updateDom,
initialUpdate: initialUpdate,
needsLock: !isReadOnly && needsLock,
updateCondition: updateConditions.length ? updateConditions.join(' && ') : undefined,
isReadOnlyMediaAttribute: this.isReadOnlyMediaAttribute()
isReadOnlyMediaAttribute: this.isReadOnlyMediaAttribute(),
dependencies,
contextual_dependencies: this.node.expression.contextual_dependencies
};
}
}
@ -210,67 +218,36 @@ function getEventHandler(
block: Block,
name: string,
snippet: string,
dependencies: Set<string>,
value: string
) {
const storeDependencies = [...dependencies].filter(prop => prop[0] === '$').map(prop => prop.slice(1));
let dependenciesArray = [...dependencies].filter(prop => prop[0] !== '$');
if (binding.node.isContextual) {
const tail = binding.node.value.node.type === 'MemberExpression'
? getTailSnippet(binding.node.value.node)
: '';
let tail = '';
if (binding.node.expression.node.type === 'MemberExpression') {
const { start, end } = get_tail(binding.node.expression.node);
tail = renderer.component.source.slice(start, end);
}
const head = block.bindings.get(name);
const { object, property, snippet } = block.bindings.get(name)();
return {
usesContext: true,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${head()}${tail} = ${value};`,
props: dependenciesArray.map(prop => `${prop}: ctx.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
mutation: `${snippet}${tail} = ${value};`,
contextual_dependencies: new Set([object, property])
};
}
if (binding.node.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 `component.target.readonly` sooner so
// that we don't have to do the `.some()` here
dependenciesArray = dependenciesArray.filter(prop => !renderer.component.computations.some(computation => computation.key === prop));
if (binding.node.expression.node.type === 'MemberExpression') {
return {
usesContext: false,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${snippet} = ${value}`,
props: dependenciesArray.map((prop: string) => `${prop}: ctx.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
mutation: `${snippet} = ${value};`,
contextual_dependencies: new Set()
};
}
let props;
let storeProps;
if (name[0] === '$') {
props = [];
storeProps = [`${name.slice(1)}: ${value}`];
} else {
props = [`${name}: ${value}`];
storeProps = [];
}
return {
usesContext: false,
usesState: false,
usesStore: false,
mutation: null,
props,
storeProps
mutation: `${snippet} = ${value};`,
contextual_dependencies: new Set()
};
}
@ -285,31 +262,31 @@ function getValueFromDom(
// <select bind:value='selected>
if (node.name === 'select') {
return node.getStaticAttributeValue('multiple') === true ?
`@selectMultipleValue(${element.var})` :
`@selectValue(${element.var})`;
`@selectMultipleValue(this)` :
`@selectValue(this)`;
}
const type = node.getStaticAttributeValue('type');
// <input type='checkbox' bind:group='foo'>
if (name === 'group') {
const bindingGroup = getBindingGroup(renderer, binding.node.value.node);
const bindingGroup = getBindingGroup(renderer, binding.node.expression.node);
if (type === 'checkbox') {
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
return `@getBindingGroupValue($$self.$$.binding_groups[${bindingGroup}])`;
}
return `${element.var}.__value`;
return `this.__value`;
}
// <input type='range|number' bind:value>
if (type === 'range' || type === 'number') {
return `@toNumber(${element.var}.${name})`;
return `@toNumber(this.${name})`;
}
if ((name === 'buffered' || name === 'seekable' || name === 'played')) {
return `@timeRangesToArray(${element.var}.${name})`
return `@timeRangesToArray(this.${name})`
}
// everything else
return `${element.var}.${name}`;
return `this.${name}`;
}

@ -4,6 +4,7 @@ import AttributeWrapper from './Attribute';
import Node from '../../../nodes/shared/Node';
import ElementWrapper from '.';
import { stringify } from '../../../../utils/stringify';
import addToSet from '../../../../utils/addToSet';
export interface StyleProp {
key: string;
@ -32,11 +33,9 @@ export default class StyleAttributeWrapper extends AttributeWrapper {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { dependencies, snippet } = chunk;
const snippet = chunk.render();
dependencies.forEach(d => {
propDependencies.add(d);
});
addToSet(propDependencies, chunk.dynamic_dependencies);
return chunk.getPrecedence() <= 13 ? `(${snippet})` : snippet;
}

@ -10,12 +10,15 @@ import { stringify, escapeHTML, escape } from '../../../../utils/stringify';
import TextWrapper from '../Text';
import fixAttributeCasing from '../../../../utils/fixAttributeCasing';
import deindent from '../../../../utils/deindent';
import namespaces from '../../../../utils/namespaces';
import { namespaces } from '../../../../utils/namespaces';
import AttributeWrapper from './Attribute';
import StyleAttributeWrapper from './StyleAttribute';
import { dimensions } from '../../../../utils/patterns';
import Binding from './Binding';
import InlineComponentWrapper from '../InlineComponent';
import addToSet from '../../../../utils/addToSet';
import addEventHandlers from '../shared/addEventHandlers';
import addActions from '../shared/addActions';
const events = [
{
@ -165,12 +168,14 @@ export default class ElementWrapper extends Wrapper {
// add directive and handler dependencies
[node.animation, node.outro, ...node.actions, ...node.classes].forEach(directive => {
if (directive && directive.expression) {
block.addDependencies(directive.expression.dependencies);
block.addDependencies(directive.expression.dynamic_dependencies);
}
});
node.handlers.forEach(handler => {
block.addDependencies(handler.dependencies);
if (handler.expression) {
block.addDependencies(handler.expression.dynamic_dependencies);
}
});
if (this.parent) {
@ -211,7 +216,7 @@ export default class ElementWrapper extends Wrapper {
let initialMountNode;
if (this.slotOwner) {
initialMountNode = `${this.slotOwner.var}._slotted${prop}`;
initialMountNode = `${this.slotOwner.var}.$$.slotted${prop}`;
} else {
initialMountNode = parentNode;
}
@ -278,10 +283,6 @@ export default class ElementWrapper extends Wrapper {
});
}
let hasHoistedEventHandlerOrBinding = (
//(this.hasAncestor('EachBlock') && this.bindings.length > 0) ||
this.node.handlers.some(handler => handler.shouldHoist)
);
const eventHandlerOrBindingUsesComponent = (
this.bindings.length > 0 ||
this.node.handlers.some(handler => handler.usesComponent)
@ -289,33 +290,12 @@ export default class ElementWrapper extends Wrapper {
const eventHandlerOrBindingUsesContext = (
this.bindings.some(binding => binding.node.usesContext) ||
this.node.handlers.some(handler => handler.usesContext)
this.node.handlers.some(handler => handler.usesContext) ||
this.node.actions.some(action => action.usesContext)
);
if (hasHoistedEventHandlerOrBinding) {
const initialProps: string[] = [];
const updates: string[] = [];
if (eventHandlerOrBindingUsesComponent) {
const component = block.alias('component');
initialProps.push(component === 'component' ? 'component' : `component: ${component}`);
}
if (eventHandlerOrBindingUsesContext) {
initialProps.push(`ctx`);
block.builders.update.addLine(`${node}._svelte.ctx = ctx;`);
block.maintainContext = true;
}
if (initialProps.length) {
block.builders.hydrate.addBlock(deindent`
${node}._svelte = { ${initialProps.join(', ')} };
`);
}
} else {
if (eventHandlerOrBindingUsesContext) {
block.maintainContext = true;
}
if (eventHandlerOrBindingUsesContext) {
block.maintainContext = true;
}
this.addBindings(block);
@ -331,7 +311,7 @@ export default class ElementWrapper extends Wrapper {
block.builders.mount.addBlock(this.initialUpdate);
}
if (nodes) {
if (nodes && this.renderer.options.hydratable) {
block.builders.claim.addLine(
`${nodes}.forEach(@detachNode);`
);
@ -409,9 +389,7 @@ export default class ElementWrapper extends Wrapper {
if (this.bindings.length === 0) return;
if (this.node.name === 'select' || this.isMediaNode()) {
this.renderer.hasComplexBindings = true;
}
renderer.component.has_reactive_assignments = true;
const needsLock = this.node.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type'));
@ -425,20 +403,28 @@ export default class ElementWrapper extends Wrapper {
if (lock) block.addVariable(lock, 'false');
const groups = events
.map(event => {
return {
events: event.eventNames,
bindings: mungedBindings.filter(binding => event.filter(this.node, binding.name))
};
})
.map(event => ({
events: event.eventNames,
bindings: mungedBindings.filter(binding => event.filter(this.node, binding.name))
}))
.filter(group => group.bindings.length);
groups.forEach(group => {
const handler = block.getUniqueName(`${this.var}_${group.events.join('_')}_handler`);
renderer.component.declarations.push(handler);
renderer.component.template_references.add(handler);
const needsLock = group.bindings.some(binding => binding.needsLock);
const dependencies = new Set();
const contextual_dependencies = new Set();
group.bindings.forEach(binding => {
// TODO this is a mess
addToSet(dependencies, binding.dependencies);
addToSet(contextual_dependencies, binding.contextual_dependencies);
addToSet(contextual_dependencies, binding.handler.contextual_dependencies);
if (!binding.updateDom) return;
const updateConditions = needsLock ? [`!${lock}`] : [];
@ -449,21 +435,8 @@ export default class ElementWrapper extends Wrapper {
);
});
const usesStore = group.bindings.some(binding => binding.handler.usesStore);
const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');
const props = new Set();
const storeProps = new Set();
group.bindings.forEach(binding => {
binding.handler.props.forEach(prop => {
props.add(prop);
});
binding.handler.storeProps.forEach(prop => {
storeProps.add(prop);
});
}); // TODO use stringifyProps here, once indenting is fixed
// media bindings — awkward special case. The native timeupdate events
// fire too infrequently, so we need to take matters into our
// own hands
@ -473,21 +446,48 @@ export default class ElementWrapper extends Wrapper {
block.addVariable(animation_frame);
}
block.builders.init.addBlock(deindent`
function ${handler}() {
${
animation_frame && deindent`
cancelAnimationFrame(${animation_frame});
if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
// TODO figure out how to handle locks
let callee;
// TODO dry this out — similar code for event handlers and component bindings
if (contextual_dependencies.size > 0) {
const deps = Array.from(contextual_dependencies);
block.builders.init.addBlock(deindent`
function ${handler}() {
ctx.${handler}.call(this, ctx);
}
${usesStore && `var $ = #component.store.get();`}
${needsLock && `${lock} = true;`}
${mutations.length > 0 && mutations}
${props.size > 0 && `#component.set({ ${Array.from(props).join(', ')} });`}
${storeProps.size > 0 && `#component.store.set({ ${Array.from(storeProps).join(', ')} });`}
${needsLock && `${lock} = false;`}
}
`);
`);
this.renderer.component.partly_hoisted.push(deindent`
function ${handler}({ ${deps.join(', ')} }) {
${
animation_frame && deindent`
cancelAnimationFrame(${animation_frame});
if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
}
${mutations.length > 0 && mutations}
${Array.from(dependencies).map(dep => `$$make_dirty('${dep}');`)}
}
`);
callee = handler;
} else {
this.renderer.component.partly_hoisted.push(deindent`
function ${handler}() {
${
animation_frame && deindent`
cancelAnimationFrame(${animation_frame});
if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
}
${mutations.length > 0 && mutations}
${Array.from(dependencies).map(dep => `$$make_dirty('${dep}');`)}
}
`);
callee = `ctx.${handler}`;
}
group.events.forEach(name => {
if (name === 'resize') {
@ -496,40 +496,32 @@ export default class ElementWrapper extends Wrapper {
block.addVariable(resize_listener);
block.builders.mount.addLine(
`${resize_listener} = @addResizeListener(${this.var}, ${handler});`
`${resize_listener} = @addResizeListener(${this.var}, ${callee});`
);
block.builders.destroy.addLine(
`${resize_listener}.cancel();`
);
} else {
block.builders.hydrate.addLine(
`@addListener(${this.var}, "${name}", ${handler});`
);
block.builders.destroy.addLine(
`@removeListener(${this.var}, "${name}", ${handler});`
block.event_listeners.push(
`@addListener(${this.var}, "${name}", ${callee})`
);
}
});
const allInitialStateIsDefined = group.bindings
.map(binding => `'${binding.object}' in ctx`)
.join(' && ');
const someInitialStateIsUndefined = group.bindings
.map(binding => `${binding.snippet} === void 0`)
.join(' || ');
if (this.node.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
renderer.hasComplexBindings = true;
block.builders.hydrate.addLine(
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
`if (${someInitialStateIsUndefined}) @add_render_callback(() => ${callee}.call(${this.var}));`
);
}
if (group.events[0] === 'resize') {
renderer.hasComplexBindings = true;
block.builders.hydrate.addLine(
`#component.root._aftercreate.push(${handler});`
`@add_render_callback(() => ${callee}.call(${this.var}));`
);
}
});
@ -566,7 +558,7 @@ export default class ElementWrapper extends Wrapper {
: null;
if (attr.isSpread) {
const { snippet, dependencies } = attr.expression;
const snippet = attr.expression.render();
initialProps.push(snippet);
@ -602,105 +594,21 @@ export default class ElementWrapper extends Wrapper {
}
addEventHandlers(block: Block) {
const { renderer } = this;
const { component } = renderer;
this.node.handlers.forEach(handler => {
const isCustomEvent = component.events.has(handler.name);
if (handler.callee) {
// TODO move handler render method into a wrapper
handler.render(this.renderer.component, block, this.var, handler.shouldHoist);
}
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 = (handler.shouldHoist ? component : block).getUniqueName(
`${handler.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
);
const component_name = block.alias('component'); // can't use #component, might be hoisted
// create the handler body
const handlerBody = deindent`
${handler.shouldHoist && (
handler.usesComponent || handler.usesContext
? `const { ${[handler.usesComponent && 'component', handler.usesContext && 'ctx'].filter(Boolean).join(', ')} } = ${target}._svelte;`
: null
)}
${handler.snippet ?
handler.snippet :
`${component_name}.fire("${handler.name}", event);`}
`;
if (isCustomEvent) {
block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${handler.name}.call(${component_name}, ${this.var}, function(event) {
${handlerBody}
});
`);
block.builders.destroy.addLine(deindent`
${handlerName}.destroy();
`);
} else {
const modifiers = [];
if (handler.modifiers.has('preventDefault')) modifiers.push('event.preventDefault();');
if (handler.modifiers.has('stopPropagation')) modifiers.push('event.stopPropagation();');
const handlerFunction = deindent`
function ${handlerName}(event) {
${modifiers}
${handlerBody}
}
`;
if (handler.shouldHoist) {
renderer.blocks.push(handlerFunction);
} else {
block.builders.init.addBlock(handlerFunction);
}
const opts = ['passive', 'once', 'capture'].filter(mod => handler.modifiers.has(mod));
if (opts.length) {
const optString = (opts.length === 1 && opts[0] === 'capture')
? 'true'
: `{ ${opts.map(opt => `${opt}: true`).join(', ')} }`;
block.builders.hydrate.addLine(
`@addListener(${this.var}, "${handler.name}", ${handlerName}, ${optString});`
);
block.builders.destroy.addLine(
`@removeListener(${this.var}, "${handler.name}", ${handlerName}, ${optString});`
);
} else {
block.builders.hydrate.addLine(
`@addListener(${this.var}, "${handler.name}", ${handlerName});`
);
block.builders.destroy.addLine(
`@removeListener(${this.var}, "${handler.name}", ${handlerName});`
);
}
}
});
addEventHandlers(block, this.var, this.node.handlers);
}
addRef(block: Block) {
const ref = `#component.refs.${this.node.ref.name}`;
const ref = `#component.$$.refs.${this.node.ref.name}`;
block.builders.mount.addLine(
`${ref} = ${this.var};`
);
block.builders.destroy.addLine(
`if (${ref} === ${this.var}) ${ref} = null;`
`if (${ref} === ${this.var}) {
${ref} = null;
#component.$$.inject_refs(#component.$$.refs);
}`
);
}
@ -708,23 +616,24 @@ export default class ElementWrapper extends Wrapper {
block: Block
) {
const { intro, outro } = this.node;
if (!intro && !outro) return;
const { component } = this.renderer;
if (intro === outro) {
const name = block.getUniqueName(`${this.var}_transition`);
const snippet = intro.expression
? intro.expression.snippet
? intro.expression.render()
: '{}';
block.addVariable(name);
const fn = `%transitions-${intro.name}`;
const fn = component.qualify(intro.name);
block.builders.intro.addConditional(`#component.root._intro`, deindent`
block.builders.intro.addConditional(`@intro.enabled`, deindent`
if (${name}) ${name}.invalidate();
#component.root._aftercreate.push(() => {
@add_render_callback(() => {
if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true);
${name}.run(1);
});
@ -746,10 +655,10 @@ export default class ElementWrapper extends Wrapper {
if (intro) {
block.addVariable(introName);
const snippet = intro.expression
? intro.expression.snippet
? intro.expression.render()
: '{}';
const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?
const fn = component.qualify(intro.name); // TODO add built-in transitions?
if (outro) {
block.builders.intro.addBlock(deindent`
@ -758,8 +667,8 @@ export default class ElementWrapper extends Wrapper {
`);
}
block.builders.intro.addConditional(`#component.root._intro`, deindent`
#component.root._aftercreate.push(() => {
block.builders.intro.addConditional(`@intro.enabled`, deindent`
@add_render_callback(() => {
${introName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true);
${introName}.run(1);
});
@ -769,10 +678,10 @@ export default class ElementWrapper extends Wrapper {
if (outro) {
block.addVariable(outroName);
const snippet = outro.expression
? outro.expression.snippet
? outro.expression.render()
: '{}';
const fn = `%transitions-${outro.name}`;
const fn = component.qualify(outro.name);
block.builders.intro.addBlock(deindent`
if (${outroName}) ${outroName}.abort(1);
@ -793,6 +702,8 @@ export default class ElementWrapper extends Wrapper {
addAnimation(block: Block) {
if (!this.node.animation) return;
const { component } = this.renderer;
const rect = block.getUniqueName('rect');
const animation = block.getUniqueName('animation');
@ -808,48 +719,21 @@ export default class ElementWrapper extends Wrapper {
if (${animation}) ${animation}.stop();
`);
const params = this.node.animation.expression ? this.node.animation.expression.snippet : '{}';
const params = this.node.animation.expression ? this.node.animation.expression.render() : '{}';
let { name } = this.node.animation;
if (!component.hoistable_names.has(name) && !component.imported_declarations.has(name)) {
name = `ctx.${name}`;
}
block.builders.animate.addBlock(deindent`
if (${animation}) ${animation}.stop();
${animation} = @wrapAnimation(${this.var}, ${rect}, %animations-${this.node.animation.name}, ${params});
${animation} = @wrapAnimation(${this.var}, ${rect}, ${name}, ${params});
`);
}
addActions(block: Block) {
this.node.actions.forEach(action => {
const { expression } = action;
let snippet, dependencies;
if (expression) {
snippet = expression.snippet;
dependencies = expression.dependencies;
}
const name = block.getUniqueName(
`${action.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action`
);
block.addVariable(name);
const fn = `%actions-${action.name}`;
block.builders.mount.addLine(
`${name} = ${fn}.call(#component, ${this.var}${snippet ? `, ${snippet}` : ''}) || {};`
);
if (dependencies && dependencies.size > 0) {
let conditional = `typeof ${name}.update === 'function' && `;
const deps = [...dependencies].map(dependency => `changed.${dependency}`).join(' || ');
conditional += dependencies.size > 1 ? `(${deps})` : deps;
block.builders.update.addConditional(
conditional,
`${name}.update.call(#component, ${snippet});`
);
}
block.builders.destroy.addLine(
`if (${name} && typeof ${name}.destroy === 'function') ${name}.destroy.call(#component);`
);
});
addActions(this.renderer.component, block, this.var, this.node.actions);
}
addClasses(block: Block) {
@ -857,10 +741,10 @@ export default class ElementWrapper extends Wrapper {
const { expression, name } = classDir;
let snippet, dependencies;
if (expression) {
snippet = expression.snippet;
snippet = expression.render();
dependencies = expression.dependencies;
} else {
snippet = `ctx${quotePropIfNecessary(name)}`;
snippet = `${quotePropIfNecessary(name)}`;
dependencies = new Set([name]);
}
const updater = `@toggleClass(${this.var}, "${name}", ${snippet});`;
@ -902,13 +786,13 @@ export default class ElementWrapper extends Wrapper {
}
remount(name: string) {
const slot = this.attributes.find(attribute => attribute.name === 'slot');
const slot = this.attributes.find(attribute => attribute.node.name === 'slot');
if (slot) {
const prop = quotePropIfNecessary(slot.chunks[0].data);
return `@append(${name}._slotted${prop}, ${this.var});`;
const prop = quotePropIfNecessary(slot.node.chunks[0].data);
return `@append(${name}.$$.slotted${prop}, ${this.var});`;
}
return `@append(${name}._slotted.default, ${this.var});`;
return `@append(${name}.$$.slotted.default, ${this.var});`;
}
addCssClass(className = this.component.stylesheet.id) {

@ -29,6 +29,7 @@ const wrappers = {
Head,
IfBlock,
InlineComponent,
Meta: null,
MustacheTag,
RawMustacheTag,
Slot,

@ -32,7 +32,7 @@ class IfBlockBranch extends Wrapper {
) {
super(renderer, block, parent, node);
this.condition = (<IfBlock>node).expression && (<IfBlock>node).expression.snippet;
this.condition = (<IfBlock>node).expression && (<IfBlock>node).expression.render();
this.block = block.child({
comment: createDebuggingComment(node, parent.renderer.component),
@ -87,7 +87,7 @@ export default class IfBlockWrapper extends Wrapper {
this.branches.push(branch);
blocks.push(branch.block);
block.addDependencies(node.expression.dependencies);
block.addDependencies(node.expression.dynamic_dependencies);
if (branch.block.dependencies.size > 0) {
isDynamic = true;
@ -125,10 +125,8 @@ export default class IfBlockWrapper extends Wrapper {
createBranches(this.node);
if (component.options.nestedTransitions) {
if (hasIntros) block.addIntro();
if (hasOutros) block.addOutro();
}
if (hasIntros) block.addIntro();
if (hasOutros) block.addOutro();
blocks.forEach(block => {
block.hasUpdateMethod = isDynamic;
@ -163,19 +161,17 @@ export default class IfBlockWrapper extends Wrapper {
if (hasOutros) {
this.renderCompoundWithOutros(block, parentNode, parentNodes, dynamic, vars);
if (this.renderer.options.nestedTransitions) {
block.builders.outro.addBlock(deindent`
if (${name}) ${name}.o(#outrocallback);
else #outrocallback();
`);
}
block.builders.outro.addBlock(deindent`
if (${name}) ${name}.o(#outrocallback);
else #outrocallback();
`);
} else {
this.renderCompound(block, parentNode, parentNodes, dynamic, vars);
}
} else {
this.renderSimple(block, parentNode, parentNodes, dynamic, vars);
if (hasOutros && this.renderer.options.nestedTransitions) {
if (hasOutros) {
block.builders.outro.addBlock(deindent`
if (${name}) ${name}.o(#outrocallback);
else #outrocallback();
@ -185,7 +181,7 @@ export default class IfBlockWrapper extends Wrapper {
block.builders.create.addLine(`${if_name}${name}.c();`);
if (parentNodes) {
if (parentNodes && this.renderer.options.hydratable) {
block.builders.claim.addLine(
`${if_name}${name}.l(${parentNodes});`
);

@ -9,10 +9,8 @@ import stringifyProps from '../../../../utils/stringifyProps';
import addToSet from '../../../../utils/addToSet';
import deindent from '../../../../utils/deindent';
import Attribute from '../../../nodes/Attribute';
import CodeBuilder from '../../../../utils/CodeBuilder';
import getObject from '../../../../utils/getObject';
import Binding from '../../../nodes/Binding';
import getTailSnippet from '../../../../utils/getTailSnippet';
export default class InlineComponentWrapper extends Wrapper {
var: string;
@ -33,7 +31,7 @@ export default class InlineComponentWrapper extends Wrapper {
this.cannotUseInnerHTML();
if (this.node.expression) {
block.addDependencies(this.node.expression.dependencies);
block.addDependencies(this.node.expression.dynamic_dependencies);
}
this.node.attributes.forEach(attr => {
@ -44,17 +42,19 @@ export default class InlineComponentWrapper extends Wrapper {
if (binding.isContextual) {
// we need to ensure that the each block creates a context including
// the list and the index, if they're not otherwise referenced
const { name } = getObject(binding.value.node);
const { name } = getObject(binding.expression.node);
const eachBlock = block.contextOwners.get(name);
eachBlock.hasBinding = true;
}
block.addDependencies(binding.value.dependencies);
block.addDependencies(binding.expression.dynamic_dependencies);
});
this.node.handlers.forEach(handler => {
block.addDependencies(handler.dependencies);
if (handler.expression) {
block.addDependencies(handler.expression.dynamic_dependencies);
}
});
this.var = (
@ -68,9 +68,7 @@ export default class InlineComponentWrapper extends Wrapper {
this.fragment = new FragmentWrapper(renderer, block, node.children, this, stripWhitespace, nextSibling);
}
if (renderer.component.options.nestedTransitions) {
block.addOutro();
}
block.addOutro();
}
render(
@ -83,28 +81,23 @@ export default class InlineComponentWrapper extends Wrapper {
const name = this.var;
const componentInitProperties = [
`root: #component.root`,
`store: #component.store`
];
const component_opts = [];
if (this.fragment) {
const slots = Array.from(this._slots).map(name => `${quoteNameIfNecessary(name)}: @createFragment()`);
componentInitProperties.push(`slots: { ${slots.join(', ')} }`);
component_opts.push(`slots: { ${slots.join(', ')} }`);
this.fragment.nodes.forEach((child: Wrapper) => {
child.render(block, `${this.var}._slotted.default`, 'nodes');
child.render(block, `${this.var}.$$.slotted.default`, 'nodes');
});
}
const statements: string[] = [];
const updates: string[] = [];
const postupdates: string[] = [];
const name_initial_data = block.getUniqueName(`${name}_initial_data`);
let props;
const name_changes = block.getUniqueName(`${name}_changes`);
let name_updating: string;
let beforecreate: string = null;
const updates: string[] = [];
const usesSpread = !!this.node.attributes.find(a => a.isSpread);
@ -115,7 +108,20 @@ export default class InlineComponentWrapper extends Wrapper {
);
if (this.node.attributes.length || this.node.bindings.length) {
componentInitProperties.push(`data: ${name_initial_data}`);
if (!usesSpread && this.node.bindings.length === 0) {
component_opts.push(`props: ${attributeObject}`);
} else {
props = block.getUniqueName(`${name}_props`);
component_opts.push(`props: ${props}`);
}
}
if (component.options.dev) {
// TODO this is a terrible hack, but without it the component
// will complain that options.target is missing. This would
// work better if components had separate public and private
// APIs
component_opts.push(`$$inline: true`);
}
if (!usesSpread && (this.node.attributes.filter(a => a.isDynamic).length || this.node.bindings.length)) {
@ -143,7 +149,7 @@ export default class InlineComponentWrapper extends Wrapper {
: null;
if (attr.isSpread) {
const value = attr.expression.snippet;
const value = attr.expression.render();
initialProps.push(value);
changes.push(condition ? `${condition} && ${value}` : value);
@ -163,7 +169,7 @@ export default class InlineComponentWrapper extends Wrapper {
statements.push(deindent`
for (var #i = 0; #i < ${levels}.length; #i += 1) {
${name_initial_data} = @assign(${name_initial_data}, ${levels}[#i]);
${props} = @assign(${props}, ${levels}[#i]);
}
`);
@ -189,167 +195,130 @@ export default class InlineComponentWrapper extends Wrapper {
}
}
if (this.node.bindings.length) {
renderer.hasComplexBindings = true;
name_updating = block.alias(`${name}_updating`);
block.addVariable(name_updating, '{}');
let hasLocalBindings = false;
let hasStoreBindings = false;
const builder = new CodeBuilder();
this.node.bindings.forEach((binding: Binding) => {
let { name: key } = getObject(binding.value.node);
const munged_bindings = this.node.bindings.map(binding => {
component.has_reactive_assignments = true;
let setFromChild;
const name = component.getUniqueName(`${this.var}_${binding.name}_binding`);
component.declarations.push(name);
component.template_references.add(name);
if (binding.isContextual) {
const computed = isComputed(binding.value.node);
const tail = binding.value.node.type === 'MemberExpression' ? getTailSnippet(binding.value.node) : '';
const updating = block.getUniqueName(`updating_${binding.name}`);
block.addVariable(updating);
const head = block.bindings.get(key);
const snippet = binding.expression.render();
const lhs = binding.value.node.type === 'MemberExpression'
? binding.value.snippet
: `${head()}${tail} = childState${quotePropIfNecessary(binding.name)}`;
statements.push(deindent`
if (${snippet} !== void 0) {
${props}${quotePropIfNecessary(binding.name)} = ${snippet};
}`
);
setFromChild = deindent`
${lhs} = childState${quotePropIfNecessary(binding.name)};
updates.push(deindent`
if (!${updating} && ${[...binding.expression.dynamic_dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
${name_changes}${quotePropIfNecessary(binding.name)} = ${snippet};
}
`);
${[...binding.value.dependencies]
.map((name: string) => {
const isStoreProp = name[0] === '$';
const prop = isStoreProp ? name.slice(1) : name;
const newState = isStoreProp ? 'newStoreState' : 'newState';
postupdates.push(updating);
if (isStoreProp) hasStoreBindings = true;
else hasLocalBindings = true;
const contextual_dependencies = Array.from(binding.expression.contextual_dependencies);
const dependencies = Array.from(binding.expression.dependencies);
return `${newState}${quotePropIfNecessary(prop)} = ctx${quotePropIfNecessary(name)};`;
})}
`;
}
let lhs = component.source.slice(binding.expression.node.start, binding.expression.node.end).trim();
else {
const isStoreProp = key[0] === '$';
const prop = isStoreProp ? key.slice(1) : key;
const newState = isStoreProp ? 'newStoreState' : 'newState';
if (binding.isContextual && binding.expression.node.type === 'Identifier') {
// bind:x={y} — we can't just do `y = x`, we need to
// to `array[index] = x;
const { name } = binding.expression.node;
const { object, property, snippet } = block.bindings.get(name)();
lhs = snippet;
contextual_dependencies.push(object, property);
}
if (isStoreProp) hasStoreBindings = true;
else hasLocalBindings = true;
const args = ['value'];
if (contextual_dependencies.length > 0) {
args.push(`{ ${contextual_dependencies.join(', ')} }`);
if (binding.value.node.type === 'MemberExpression') {
setFromChild = deindent`
${binding.value.snippet} = childState${quotePropIfNecessary(binding.name)};
${newState}${quotePropIfNecessary(prop)} = ctx${quotePropIfNecessary(key)};
`;
block.builders.init.addBlock(deindent`
function ${name}(value) {
${updating} = true;
ctx.${name}.call(null, value, ctx);
}
`);
else {
setFromChild = `${newState}${quotePropIfNecessary(prop)} = childState${quotePropIfNecessary(binding.name)};`;
block.maintainContext = true; // TODO put this somewhere more logical
} else {
block.builders.init.addBlock(deindent`
function ${name}(value) {
${updating} = true;
ctx.${name}.call(null, value);
}
}
`);
}
statements.push(deindent`
if (${binding.value.snippet} !== void 0) {
${name_initial_data}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet};
${name_updating}${quotePropIfNecessary(binding.name)} = true;
}`
);
const body = deindent`
function ${name}(${args.join(', ')}) {
${lhs} = value;
${dependencies.map(dep => `$$make_dirty('${dep}');`)}
}
`;
builder.addConditional(
`!${name_updating}${quotePropIfNecessary(binding.name)} && changed${quotePropIfNecessary(binding.name)}`,
setFromChild
);
component.partly_hoisted.push(body);
updates.push(deindent`
if (!${name_updating}${quotePropIfNecessary(binding.name)} && ${[...binding.value.dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
${name_changes}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet};
${name_updating}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet} !== void 0;
}
`);
});
return `@add_binding_callback(() => @bind(${this.var}, '${binding.name}', ${name}));`;
});
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(changed, childState) {
var ${initialisers};
${builder}
${hasStoreBindings && `#component.store.set(newStoreState);`}
${hasLocalBindings && `#component._set(newState);`}
${name_updating} = {};
}
`);
const munged_handlers = this.node.handlers.map(handler => {
// TODO return declarations from handler.render()?
const snippet = handler.render();
beforecreate = deindent`
#component.root._beforecreate.push(() => {
${name}._bind({ ${this.node.bindings.map(b => `${quoteNameIfNecessary(b.name)}: 1`).join(', ')} }, ${name}.get());
if (handler.expression) {
handler.expression.declarations.forEach(declaration => {
block.builders.init.addBlock(declaration);
});
`;
}
}
this.node.handlers.forEach(handler => {
handler.var = block.getUniqueName(`${this.var}_${handler.name}`); // TODO this is hacky
handler.render(component, block, this.var, false); // TODO hoist when possible
if (handler.usesContext) block.maintainContext = true; // TODO is there a better place to put this?
return `${name}.$on("${handler.name}", ${snippet});`;
});
if (this.node.name === 'svelte:component') {
const switch_value = block.getUniqueName('switch_value');
const switch_props = block.getUniqueName('switch_props');
const { snippet } = this.node.expression;
const snippet = this.node.expression.render();
block.builders.init.addBlock(deindent`
var ${switch_value} = ${snippet};
function ${switch_props}(ctx) {
${(this.node.attributes.length || this.node.bindings.length) && deindent`
var ${name_initial_data} = ${attributeObject};`}
${props && `let ${props} = ${attributeObject};`}`}
${statements}
return {
${componentInitProperties.join(',\n')}
};
return ${stringifyProps(component_opts)};
}
if (${switch_value}) {
var ${name} = new ${switch_value}(${switch_props}(ctx));
${beforecreate}
${munged_bindings}
${munged_handlers}
}
${this.node.handlers.map(handler => deindent`
function ${handler.var}(event) {
${handler.snippet || `#component.fire("${handler.name}", event);`}
}
if (${name}) ${name}.on("${handler.name}", ${handler.var});
`)}
`);
block.builders.create.addLine(
`if (${name}) ${name}._fragment.c();`
`if (${name}) ${name}.$$.fragment.c();`
);
if (parentNodes) {
if (parentNodes && this.renderer.options.hydratable) {
block.builders.claim.addLine(
`if (${name}) ${name}._fragment.l(${parentNodes});`
`if (${name}) ${name}.$$.fragment.l(${parentNodes});`
);
}
block.builders.mount.addBlock(deindent`
if (${name}) {
${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});
${this.node.ref && `#component.refs.${this.node.ref.name} = ${name};`}
@mount_component(${name}, ${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});
${this.node.ref && `#component.$$.refs.${this.node.ref.name} = ${name};`}
}
`);
@ -365,41 +334,34 @@ export default class InlineComponentWrapper extends Wrapper {
block.builders.update.addBlock(deindent`
if (${switch_value} !== (${switch_value} = ${snippet})) {
if (${name}) {
${component.options.nestedTransitions
? deindent`
@groupOutros();
const old_component = ${name};
old_component._fragment.o(() => {
old_component.destroy();
});`
: `${name}.destroy();`}
old_component.$$.fragment.o(() => {
old_component.$destroy();
});
}
if (${switch_value}) {
${name} = new ${switch_value}(${switch_props}(ctx));
${this.node.bindings.length > 0 && deindent`
#component.root._beforecreate.push(() => {
const changed = {};
${this.node.bindings.map(binding => deindent`
if (${binding.value.snippet} === void 0) changed.${binding.name} = 1;`)}
${name}._bind(changed, ${name}.get());
});`}
${name}._fragment.c();
${munged_bindings}
${munged_handlers}
${this.fragment && this.fragment.nodes.map(child => child.remount(name))}
${name}._mount(${updateMountNode}, ${anchor});
${name}.$$.fragment.c();
@mount_component(${name}, ${updateMountNode}, ${anchor});
${this.node.handlers.map(handler => deindent`
${name}.on("${handler.name}", ${handler.var});
${name}.$on("${handler.name}", ${handler.var});
`)}
${this.node.ref && `#component.refs.${this.node.ref.name} = ${name};`}
${this.node.ref && `#component.$$.refs.${this.node.ref.name} = ${name};`}
} else {
${name} = null;
${this.node.ref && deindent`
if (#component.refs.${this.node.ref.name} === ${name}) {
#component.refs.${this.node.ref.name} = null;
if (#component.$$.refs.${this.node.ref.name} === ${name}) {
#component.$$.refs.${this.node.ref.name} = null;
#component.$$.inject_refs(#component.$$.refs);
}`}
}
}
@ -408,72 +370,69 @@ export default class InlineComponentWrapper extends Wrapper {
if (updates.length) {
block.builders.update.addBlock(deindent`
else if (${switch_value}) {
${name}._set(${name_changes});
${this.node.bindings.length && `${name_updating} = {};`}
${name}.$set(${name_changes});
}
${postupdates.length > 0 && `${postupdates.join(' = ')} = false;`}
`);
}
block.builders.destroy.addLine(`if (${name}) ${name}.destroy(${parentNode ? '' : 'detach'});`);
block.builders.destroy.addLine(`if (${name}) ${name}.$destroy(${parentNode ? '' : 'detach'});`);
} else {
const expression = this.node.name === 'svelte:self'
? component.name
: `%components-${this.node.name}`;
: component.qualify(this.node.name);
block.builders.init.addBlock(deindent`
${(this.node.attributes.length || this.node.bindings.length) && deindent`
var ${name_initial_data} = ${attributeObject};`}
${props && `let ${props} = ${attributeObject};`}`}
${statements}
var ${name} = new ${expression}({
${componentInitProperties.join(',\n')}
});
${beforecreate}
var ${name} = new ${expression}(${stringifyProps(component_opts)});
${this.node.handlers.map(handler => deindent`
${name}.on("${handler.name}", function(event) {
${handler.snippet || `#component.fire("${handler.name}", event);`}
});
`)}
${munged_bindings}
${munged_handlers}
${this.node.ref && `#component.refs.${this.node.ref.name} = ${name};`}
${this.node.ref && `#component.$$.refs.${this.node.ref.name} = ${name};`}
`);
block.builders.create.addLine(`${name}._fragment.c();`);
block.builders.create.addLine(`${name}.$$.fragment.c();`);
if (parentNodes) {
if (parentNodes && this.renderer.options.hydratable) {
block.builders.claim.addLine(
`${name}._fragment.l(${parentNodes});`
`${name}.$$.fragment.l(${parentNodes});`
);
}
block.builders.mount.addLine(
`${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});`
`@mount_component(${name}, ${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});`
);
if (updates.length) {
block.builders.update.addBlock(deindent`
${updates}
${name}._set(${name_changes});
${this.node.bindings.length && `${name_updating} = {};`}
${name}.$set(${name_changes});
${postupdates.length > 0 && `${postupdates.join(' = ')} = false;`}
`);
}
block.builders.destroy.addLine(deindent`
${name}.destroy(${parentNode ? '' : 'detach'});
${this.node.ref && `if (#component.refs.${this.node.ref.name} === ${name}) #component.refs.${this.node.ref.name} = null;`}
block.builders.destroy.addBlock(deindent`
${name}.$destroy(${parentNode ? '' : 'detach'});
${this.node.ref && deindent`
if (#component.$$.refs.${this.node.ref.name} === ${name}) {
#component.$$.refs.${this.node.ref.name} = null;
#component.$$.inject_refs(#component.$$.refs);
}
`}
`);
}
if (component.options.nestedTransitions) {
block.builders.outro.addLine(
`if (${name}) ${name}._fragment.o(#outrocallback);`
);
}
block.builders.outro.addLine(
`if (${name}) ${name}.$$.fragment.o(#outrocallback);`
);
}
remount(name: string) {
return `${this.var}._mount(${name}._slotted.default, null);`;
return `${this.var}.$$.fragment.m(${name}.$$.slotted.default, null);`;
}
}

@ -5,10 +5,7 @@ import Slot from '../../nodes/Slot';
import { quotePropIfNecessary } from '../../../utils/quoteIfNecessary';
import FragmentWrapper from './Fragment';
import deindent from '../../../utils/deindent';
function sanitize(name) {
return name.replace(/[^a-zA-Z]+/g, '_').replace(/^_/, '').replace(/_$/, '');
}
import sanitize from '../../../utils/sanitize';
export default class SlotWrapper extends Wrapper {
node: Slot;
@ -49,7 +46,7 @@ export default class SlotWrapper extends Wrapper {
const content_name = block.getUniqueName(`slot_content_${sanitize(slotName)}`);
const prop = quotePropIfNecessary(slotName);
block.addVariable(content_name, `#component._slotted${prop}`);
block.addVariable(content_name, `#component.$$.slotted${prop}`);
// TODO can we use isDomNode instead of type === 'Element'?
const needsAnchorBefore = this.prev ? this.prev.node.type !== 'Element' : !parentNode;
@ -107,7 +104,7 @@ export default class SlotWrapper extends Wrapper {
// if the slot is unmounted, move nodes back into the document fragment,
// so that it can be reinserted later
// TODO so that this can work with public API, component._slotted should
// TODO so that this can work with public API, component.$$.slotted should
// be all fragments, derived from options.slots. Not === options.slots
const unmountLeadin = block.builders.destroy.toString() !== destroyBefore
? `else`

@ -62,6 +62,6 @@ export default class TextWrapper extends Wrapper {
}
remount(name: string) {
return `@append(${name}._slotted.default, ${this.var});`;
return `@append(${name}.$$.slotted.default, ${this.var});`;
}
}

@ -4,6 +4,7 @@ import Block from '../Block';
import Title from '../../nodes/Title';
import FragmentWrapper from './Fragment';
import { stringify } from '../../../utils/stringify';
import addToSet from '../../../utils/addToSet';
export default class TitleWrapper extends Wrapper {
node: Title;
@ -32,12 +33,8 @@ export default class TitleWrapper extends Wrapper {
if (this.node.children.length === 1) {
// single {tag} — may be a non-string
const { expression } = this.node.children[0];
const { dependencies, snippet } = this.node.children[0].expression;
value = snippet;
dependencies.forEach(d => {
allDependencies.add(d);
});
value = expression.render();
addToSet(allDependencies, expression.dynamic_dependencies);
} else {
// '{foo} {bar}' — treat as string concatenation
value =
@ -47,9 +44,9 @@ export default class TitleWrapper extends Wrapper {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { dependencies, snippet } = chunk.expression;
const snippet = chunk.expression.render();
dependencies.forEach(d => {
chunk.expression.dynamic_dependencies.forEach(d => {
allDependencies.add(d);
});

@ -3,7 +3,9 @@ import Block from '../Block';
import Node from '../../nodes/shared/Node';
import Wrapper from './shared/Wrapper';
import deindent from '../../../utils/deindent';
import stringifyProps from '../../../utils/stringifyProps';
import addEventHandlers from './shared/addEventHandlers';
import Window from '../../nodes/Window';
import addActions from './shared/addActions';
const associatedEvents = {
innerWidth: 'resize',
@ -29,6 +31,8 @@ const readonly = new Set([
]);
export default class WindowWrapper extends Wrapper {
node: Window;
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: Node) {
super(renderer, block, parent, node);
}
@ -40,56 +44,16 @@ export default class WindowWrapper extends Wrapper {
const events = {};
const bindings: Record<string, string> = {};
this.node.handlers.forEach(handler => {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
component.addSourcemapLocations(handler.expression);
const isCustomEvent = component.events.has(handler.name);
let usesState = handler.dependencies.size > 0;
handler.render(component, block, 'window', false); // TODO hoist?
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);
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.destroy.addBlock(deindent`
window.removeEventListener("${handler.name}", ${handlerName});
`);
}
});
addActions(component, block, 'window', this.node.actions);
addEventHandlers(block, 'window', this.node.handlers);
this.node.bindings.forEach(binding => {
// in dev mode, throw if read-only values are written to
if (readonly.has(binding.name)) {
renderer.readonly.add(binding.value.node.name);
renderer.readonly.add(binding.expression.node.name);
}
bindings[binding.name] = binding.value.node.name;
bindings[binding.name] = binding.expression.node.name;
// bind:online is a special case, we need to listen for two separate events
if (binding.name === 'online') return;
@ -99,7 +63,7 @@ export default class WindowWrapper extends Wrapper {
if (!events[associatedEvent]) events[associatedEvent] = [];
events[associatedEvent].push({
name: binding.value.node.name,
name: binding.expression.node.name,
value: property
});
});
@ -109,7 +73,7 @@ export default class WindowWrapper extends Wrapper {
const timeout = block.getUniqueName(`window_updating_timeout`);
Object.keys(events).forEach(event => {
const handlerName = block.getUniqueName(`onwindow${event}`);
const handler_name = block.getUniqueName(`onwindow${event}`);
const props = events[event];
if (event === 'scroll') {
@ -141,35 +105,35 @@ export default class WindowWrapper extends Wrapper {
});
}
const handlerBody = deindent`
${event === 'scroll' && deindent`
if (${lock}) return;
${lock} = true;
`}
${component.options.dev && `component._updatingReadonlyProperty = true;`}
#component.set(${stringifyProps(props.map(prop => `${prop.name}: this.${prop.value}`))});
${component.options.dev && `component._updatingReadonlyProperty = false;`}
${event === 'scroll' && `${lock} = false;`}
`;
component.declarations.push(handler_name);
component.template_references.add(handler_name);
component.partly_hoisted.push(deindent`
function ${handler_name}() {
${event === 'scroll' && deindent`
if (${lock}) return;
${lock} = true;
`}
${props.map(prop => `${prop.name} = window.${prop.value}; $$make_dirty('${prop.name}');`)}
${event === 'scroll' && `${lock} = false;`}
}
`);
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
${handlerBody}
}
window.addEventListener("${event}", ${handlerName});
window.addEventListener("${event}", ctx.${handler_name});
@add_render_callback(ctx.${handler_name});
`);
block.builders.destroy.addBlock(deindent`
window.removeEventListener("${event}", ${handlerName});
window.removeEventListener("${event}", ctx.${handler_name});
`);
component.has_reactive_assignments = true;
});
// special case... might need to abstract this out if we add more special cases
if (bindings.scrollX || bindings.scrollY) {
block.builders.init.addBlock(deindent`
#component.on("state", ({ changed, current }) => {
#component.$on("state", ({ changed, current }) => {
if (${
[bindings.scrollX, bindings.scrollY].map(
binding => binding && `changed["${binding}"]`
@ -190,15 +154,15 @@ export default class WindowWrapper extends Wrapper {
// another special case. (I'm starting to think these are all special cases.)
if (bindings.online) {
const handlerName = block.getUniqueName(`onlinestatuschanged`);
const handler_name = block.getUniqueName(`onlinestatuschanged`);
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
function ${handler_name}(event) {
${component.options.dev && `component._updatingReadonlyProperty = true;`}
#component.set({ ${bindings.online}: navigator.onLine });
${component.options.dev && `component._updatingReadonlyProperty = false;`}
}
window.addEventListener("online", ${handlerName});
window.addEventListener("offline", ${handlerName});
window.addEventListener("online", ${handler_name});
window.addEventListener("offline", ${handler_name});
`);
// add initial value
@ -207,8 +171,8 @@ export default class WindowWrapper extends Wrapper {
);
block.builders.destroy.addBlock(deindent`
window.removeEventListener("online", ${handlerName});
window.removeEventListener("offline", ${handlerName});
window.removeEventListener("online", ${handler_name});
window.removeEventListener("offline", ${handler_name});
`);
}
}

@ -1,7 +1,6 @@
import Wrapper from './Wrapper';
import Renderer from '../../Renderer';
import Block from '../../Block';
import Node from '../../../nodes/shared/Node';
import MustacheTag from '../../../nodes/MustacheTag';
import RawMustacheTag from '../../../nodes/RawMustacheTag';
@ -12,28 +11,15 @@ export default class Tag extends Wrapper {
super(renderer, block, parent, node);
this.cannotUseInnerHTML();
block.addDependencies(node.expression.dependencies);
}
render(block: Block, parentNode: string, parentNodes: string) {
const { init } = this.renameThisMethod(
block,
value => `@setData(${this.var}, ${value});`
);
block.addElement(
this.var,
`@createText(${init})`,
parentNodes && `@claimText(${parentNodes}, ${init})`,
parentNode
);
block.addDependencies(node.expression.dynamic_dependencies);
}
renameThisMethod(
block: Block,
update: ((value: string) => string)
) {
const { snippet, dependencies } = this.node.expression;
const dependencies = this.node.expression.dynamic_dependencies;
const snippet = this.node.expression.render();
const value = this.node.shouldCache && block.getUniqueName(`${this.var}_value`);
const content = this.node.shouldCache ? value : snippet;
@ -48,9 +34,11 @@ export default class Tag extends Wrapper {
const updateCachedValue = `${value} !== (${value} = ${snippet})`;
const condition = this.node.shouldCache ?
(dependencies.size ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
changedCheck;
const condition =this.node.shouldCache
? dependencies.size > 0
? `(${changedCheck}) && ${updateCachedValue}`
: updateCachedValue
: changedCheck;
block.builders.update.addConditional(
condition,
@ -62,6 +50,6 @@ export default class Tag extends Wrapper {
}
remount(name: string) {
return `@append(${name}._slotted.default, ${this.var});`;
return `@append(${name}.$$.slotted.default, ${this.var});`;
}
}

@ -87,6 +87,6 @@ export default class Wrapper {
}
remount(name: string) {
return `${this.var}.m(${name}._slotted.default, null);`;
return `${this.var}.m(${name}.$$.slotted.default, null);`;
}
}

@ -0,0 +1,54 @@
import Renderer from '../../Renderer';
import Block from '../../Block';
import Action from '../../../nodes/Action';
import Component from '../../../Component';
export default function addActions(
component: Component,
block: Block,
target: string,
actions: Action[]
) {
actions.forEach(action => {
const { expression } = action;
let snippet, dependencies;
if (expression) {
snippet = expression.render();
dependencies = expression.dynamic_dependencies;
expression.declarations.forEach(declaration => {
block.builders.init.addBlock(declaration);
});
}
const name = block.getUniqueName(
`${action.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action`
);
block.addVariable(name);
const fn = component.imported_declarations.has(action.name) || component.hoistable_names.has(action.name)
? action.name
: `ctx.${action.name}`;
component.template_references.add(action.name);
block.builders.mount.addLine(
`${name} = ${fn}.call(null, ${target}${snippet ? `, ${snippet}` : ''}) || {};`
);
if (dependencies && dependencies.size > 0) {
let conditional = `typeof ${name}.update === 'function' && `;
const deps = [...dependencies].map(dependency => `changed.${dependency}`).join(' || ');
conditional += dependencies.size > 1 ? `(${deps})` : deps;
block.builders.update.addConditional(
conditional,
`${name}.update.call(null, ${snippet});`
);
}
block.builders.destroy.addLine(
`if (${name} && typeof ${name}.destroy === 'function') ${name}.destroy();`
);
});
}

@ -0,0 +1,36 @@
import Block from '../../Block';
import EventHandler from '../../../nodes/EventHandler';
export default function addEventHandlers(
block: Block,
target: string,
handlers: EventHandler[]
) {
handlers.forEach(handler => {
let snippet = handler.render();
if (handler.modifiers.has('preventDefault')) snippet = `@preventDefault(${snippet})`;
if (handler.modifiers.has('stopPropagation')) snippet = `@stopPropagation(${snippet})`;
const opts = ['passive', 'once', 'capture'].filter(mod => handler.modifiers.has(mod));
if (opts.length) {
const optString = (opts.length === 1 && opts[0] === 'capture')
? 'true'
: `{ ${opts.map(opt => `${opt}: true`).join(', ')} }`;
block.event_listeners.push(
`@addListener(${target}, "${handler.name}", ${snippet}, ${optString})`
);
} else {
block.event_listeners.push(
`@addListener(${target}, "${handler.name}", ${snippet})`
);
}
if (handler.expression) {
handler.expression.declarations.forEach(declaration => {
block.builders.init.addBlock(declaration);
});
}
});
}

@ -27,6 +27,7 @@ const handlers: Record<string, Handler> = {
Head,
IfBlock,
InlineComponent,
Meta: noop,
MustacheTag: Tag, // TODO MustacheTag is an anachronism
RawMustacheTag: HtmlTag,
Slot,
@ -38,15 +39,9 @@ const handlers: Record<string, Handler> = {
type AppendTarget = any; // TODO
export default class Renderer {
bindings: string[];
code: string;
targets: AppendTarget[];
constructor() {
this.bindings = [];
this.code = '';
this.targets = [];
}
has_bindings = false;
code = '';
targets: AppendTarget[] = [];
append(code: string) {
if (this.targets.length) {

@ -1,16 +1,16 @@
import Renderer from '../Renderer';
import { CompileOptions } from '../../../interfaces';
import { snip } from '../utils';
export default function(node, renderer: Renderer, options: CompileOptions) {
const { snippet } = node.expression;
renderer.append('${(function(__value) { if(@isPromise(__value)) return `');
renderer.render(node.pending.children, options);
renderer.append('`; return function(ctx) { return `');
renderer.append('`; return function(' + (node.value || '') + ') { return `');
renderer.render(node.then.children, options);
renderer.append(`\`;}(Object.assign({}, ctx, { ${node.value}: __value }));}(${snippet})) }`);
const snippet = snip(node.expression);
renderer.append(`\`;}(__value);}(${snippet})) }`);
}

@ -7,10 +7,9 @@ export default function(node, renderer, options) {
const { line, column } = options.locate(node.start + 1);
const obj = node.expressions.length === 0
? `ctx`
? `{}`
: `{ ${node.expressions
.map(e => e.node.name)
.map(name => `${name}: ctx.${name}`)
.join(', ')} }`;
const str = '${@debug(' + `${filename && stringify(filename)}, ${line}, ${column}, ${obj})}`;

@ -1,13 +1,15 @@
import { snip } from '../utils';
export default function(node, renderer, options) {
const { snippet } = node.expression;
const snippet = snip(node.expression);
const props = node.contexts.map(prop => `${prop.key.name}: item${prop.tail}`);
const { start, end } = node.context_node;
const getContext = node.index
? `(item, i) => Object.assign({}, ctx, { ${props.join(', ')}, ${node.index}: i })`
: `item => Object.assign({}, ctx, { ${props.join(', ')} })`;
const ctx = node.index
? `([✂${start}-${end}✂], ${node.index})`
: `([✂${start}-${end}✂])`
const open = `\${ ${node.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${getContext}, ctx => \``;
const open = `\${${node.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${ctx} => \``;
renderer.append(open);
renderer.render(node.children, options);

@ -3,6 +3,7 @@ import isVoidElementName from '../../../utils/isVoidElementName';
import Attribute from '../../nodes/Attribute';
import Node from '../../nodes/shared/Node';
import { escape, escapeTemplate } from '../../../utils/stringify';
import { snip } from '../utils';
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
const boolean_attributes = new Set([
@ -60,7 +61,7 @@ export default function(node, renderer, options) {
const classExpr = node.classes.map((classDir: Class) => {
const { expression, name } = classDir;
const snippet = expression ? expression.snippet : `ctx${quotePropIfNecessary(name)}`;
const snippet = expression ? snip(expression) : `ctx${quotePropIfNecessary(name)}`;
return `${snippet} ? "${name}" : ""`;
}).join(', ');
@ -71,7 +72,7 @@ export default function(node, renderer, options) {
const args = [];
node.attributes.forEach(attribute => {
if (attribute.isSpread) {
args.push(attribute.expression.snippet);
args.push(snip(attribute.expression));
} else {
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = stringifyAttribute(attribute);
@ -83,7 +84,7 @@ export default function(node, renderer, options) {
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: ${attribute.chunks[0].snippet} }`);
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: ${snip(attribute.chunks[0])} }`);
} else {
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: \`${stringifyAttribute(attribute)}\` }`);
}
@ -105,13 +106,13 @@ export default function(node, renderer, options) {
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
openingTag += '${' + attribute.chunks[0].snippet + ' ? " ' + attribute.name + '" : "" }';
openingTag += '${' + snip(attribute.chunks[0]) + ' ? " ' + attribute.name + '" : "" }';
} else if (attribute.name === 'class' && classExpr) {
addClassAttribute = false;
openingTag += ` class="\${[\`${stringifyAttribute(attribute)}\`, ${classExpr}].join(' ').trim() }"`;
} else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') {
const { name } = attribute;
const { snippet } = attribute.chunks[0];
const snippet = snip(attribute.chunks[0]);
openingTag += '${(v => v == null ? "" : ` ' + name + '="${@escape(' + snippet + ')}"`)(' + snippet + ')}';
} else {
@ -121,11 +122,12 @@ export default function(node, renderer, options) {
}
node.bindings.forEach(binding => {
const { name, value: { snippet } } = binding;
const { name, expression } = binding;
if (name === 'group') {
// TODO server-render group bindings
} else {
const snippet = snip(expression);
openingTag += ' ${(v => v ? ("' + name + '" + (v === true ? "" : "=" + JSON.stringify(v))) : "")(' + snippet + ')}';
}
});
@ -156,7 +158,7 @@ function stringifyAttribute(attribute: Attribute) {
return escapeTemplate(escape(chunk.data).replace(/"/g, '&quot;'));
}
return '${@escape(' + chunk.snippet + ')}';
return '${@escape(' + snip(chunk) + ')}';
})
.join('');
}

@ -1,5 +1,5 @@
export default function(node, renderer, options) {
renderer.append('${(__result.head += `');
renderer.append('${($$result.head += `');
renderer.render(node.children, options);

@ -1,3 +1,5 @@
import { snip } from '../utils';
export default function(node, renderer, options) {
renderer.append('${' + node.expression.snippet + '}');
renderer.append('${' + snip(node.expression) + '}');
}

@ -1,5 +1,7 @@
import { snip } from '../utils';
export default function(node, renderer, options) {
const { snippet } = node.expression;
const snippet = snip(node.expression);
renderer.append('${ ' + snippet + ' ? `');

@ -1,106 +1,94 @@
import { escape, escapeTemplate, stringify } from '../../../utils/stringify';
import getObject from '../../../utils/getObject';
import getTailSnippet from '../../../utils/getTailSnippet';
import { get_tail_snippet } from '../../../utils/get_tail_snippet';
import { quoteNameIfNecessary, quotePropIfNecessary } from '../../../utils/quoteIfNecessary';
import deindent from '../../../utils/deindent';
import { snip } from '../utils';
import Renderer from '../Renderer';
import stringifyProps from '../../../utils/stringifyProps';
type AppendTarget = any; // TODO
export default function(node, renderer, options) {
function stringifyAttribute(chunk: Node) {
function stringifyAttribute(chunk: Node) {
if (chunk.type === 'Text') {
return escapeTemplate(escape(chunk.data));
}
return '${@escape( ' + snip(chunk) + ')}';
}
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 escapeTemplate(escape(chunk.data));
return stringify(chunk.data);
}
return '${@escape( ' + chunk.snippet + ')}';
return snip(chunk);
}
const bindingProps = node.bindings.map(binding => {
const { name } = getObject(binding.value.node);
const tail = binding.value.node.type === 'MemberExpression'
? getTailSnippet(binding.value.node)
: '';
return '`' + attribute.chunks.map(stringifyAttribute).join('') + '`';
}
return `${quoteNameIfNecessary(binding.name)}: ctx${quotePropIfNecessary(name)}${tail}`;
});
function stringifyObject(props) {
return props.length > 0
? `{ ${props.join(', ')} }`
: `{};`
}
function getAttributeValue(attribute) {
if (attribute.isTrue) return `true`;
if (attribute.chunks.length === 0) return `''`;
export default function(node, renderer: Renderer, options) {
const binding_props = [];
const binding_fns = [];
if (attribute.chunks.length === 1) {
const chunk = attribute.chunks[0];
if (chunk.type === 'Text') {
return stringify(chunk.data);
}
node.bindings.forEach(binding => {
renderer.has_bindings = true;
return chunk.snippet;
}
// TODO this probably won't work for contextual bindings
const snippet = snip(binding.expression);
return '`' + attribute.chunks.map(stringifyAttribute).join('') + '`';
}
binding_props.push(`${binding.name}: ${snippet}`);
binding_fns.push(`${binding.name}: $$value => { ${snippet} = $$value; $$settled = false }`);
});
const usesSpread = node.attributes.find(attr => attr.isSpread);
const props = usesSpread
? `Object.assign(${
let props;
if (usesSpread) {
props = `Object.assign(${
node.attributes
.map(attribute => {
if (attribute.isSpread) {
return attribute.expression.snippet;
return snip(attribute.expression);
} else {
return `{ ${quoteNameIfNecessary(attribute.name)}: ${getAttributeValue(attribute)} }`;
return `{ ${attribute.name}: ${getAttributeValue(attribute)} }`;
}
})
.concat(bindingProps.map(p => `{ ${p} }`))
.concat(binding_props.map(p => `{ ${p} }`))
.join(', ')
})`
: `{ ${node.attributes
.map(attribute => `${quoteNameIfNecessary(attribute.name)}: ${getAttributeValue(attribute)}`)
.concat(bindingProps)
.join(', ')} }`;
})`;
} else {
props = stringifyProps(
node.attributes
.map(attribute => `${attribute.name}: ${getAttributeValue(attribute)}`)
.concat(binding_props)
);
}
const bindings = stringifyProps(binding_fns);
const expression = (
node.name === 'svelte:self'
? node.component.name
: node.name === 'svelte:component'
? `((${node.expression.snippet}) || @missingComponent)`
: `%components-${node.name}`
? `((${snip(node.expression)}) || @missingComponent)`
: node.name
);
node.bindings.forEach(binding => {
const conditions = [];
let parent = node;
while (parent = parent.parent) {
if (parent.type === 'IfBlock') {
// TODO handle contextual bindings...
conditions.push(`(${parent.expression.snippet})`);
}
}
conditions.push(
`!('${binding.name}' in ctx)`,
`${expression}.data`
);
const { name } = getObject(binding.value.node);
renderer.bindings.push(deindent`
if (${conditions.reverse().join('&&')}) {
tmp = ${expression}.data();
if ('${name}' in tmp) {
ctx${quotePropIfNecessary(binding.name)} = tmp.${name};
settled = false;
}
}
`);
});
let open = `\${@validateSsrComponent(${expression}, '${node.name}')._render(__result, ${props}`;
const component_options = [];
component_options.push(`store: options.store`);
const slot_fns = [];
if (node.children.length) {
const target: AppendTarget = {
@ -112,19 +100,16 @@ export default function(node, renderer, options) {
renderer.render(node.children, options);
const slotted = Object.keys(target.slots)
.map(name => `${quoteNameIfNecessary(name)}: () => \`${target.slots[name]}\``)
.join(', ');
component_options.push(`slotted: { ${slotted} }`);
Object.keys(target.slots).forEach(name => {
slot_fns.push(
`${quoteNameIfNecessary(name)}: () => \`${target.slots[name]}\``
);
});
renderer.targets.pop();
}
if (component_options.length) {
open += `, { ${component_options.join(', ')} }`;
}
const slots = stringifyProps(slot_fns);
renderer.append(open);
renderer.append(')}');
renderer.append(`\${@validate_component(${expression}, '${node.name}').$$render($$result, ${props}, ${bindings}, ${slots})}`);
}

@ -3,10 +3,10 @@ import { quotePropIfNecessary } from '../../../utils/quoteIfNecessary';
export default function(node, renderer, options) {
const name = node.attributes.find(attribute => attribute.name === 'name');
const slotName = name && name.chunks[0].data || 'default';
const prop = quotePropIfNecessary(slotName);
const slot_name = name && name.chunks[0].data || 'default';
const prop = quotePropIfNecessary(slot_name);
renderer.append(`\${options && options.slotted && options.slotted${prop} ? options.slotted${prop}() : \``);
renderer.append(`\${$$slots${prop} ? $$slots${prop}() : \``);
renderer.render(node.children, options);

@ -1,9 +1,13 @@
import { snip } from '../utils';
export default function(node, renderer, options) {
const snippet = snip(node.expression);
renderer.append(
node.parent &&
node.parent.type === 'Element' &&
node.parent.name === 'style'
? '${' + node.expression.snippet + '}'
: '${@escape(' + node.expression.snippet + ')}'
? '${' + snippet + '}'
: '${@escape(' + snippet + ')}'
);
}

@ -1,9 +1,7 @@
import deindent from '../../utils/deindent';
import Component from '../Component';
import globalWhitelist from '../../utils/globalWhitelist';
import { CompileOptions } from '../../interfaces';
import { stringify } from '../../utils/stringify';
import CodeBuilder from '../../utils/CodeBuilder';
import Renderer from './Renderer';
export default function ssr(
@ -12,147 +10,81 @@ export default function ssr(
) {
const renderer = new Renderer();
const format = options.format || 'cjs';
const { name } = component;
const { computations, name, templateProperties } = component;
// create main render() function
// create $$render function
renderer.render(trim(component.fragment.children), Object.assign({
locate: component.locate
}, options));
const css = component.customElement ?
// TODO concatenate CSS maps
const css = options.customElement ?
{ code: null, map: null } :
component.stylesheet.render(options.filename, true);
// generate initial state object
const expectedProperties = Array.from(component.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = expectedProperties.filter(prop => prop[0] === '$');
const initialState = [];
if (globals.length > 0) {
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
}
if (storeProps.length > 0) {
const initialize = `_init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`
initialState.push(`options.store.${initialize}`);
}
if (templateProperties.data) {
initialState.push(`%data()`);
} else if (globals.length === 0 && storeProps.length === 0) {
initialState.push('{}');
}
initialState.push('ctx');
let user_code;
let js = null;
if (component.javascript) {
const componentDefinition = new CodeBuilder();
// not all properties are relevant to SSR (e.g. lifecycle hooks)
const relevant = new Set([
'data',
'components',
'computed',
'helpers',
'preload',
'store'
]);
component.declarations.forEach(declaration => {
if (relevant.has(declaration.type)) {
componentDefinition.addBlock(declaration.block);
}
user_code = component.javascript;
} else if (component.ast.js.length === 0 && component.props.length > 0) {
const props = component.props.map(prop => {
return prop.as === prop.name
? prop.as
: `${prop.as}: ${prop.name}`
});
js = (
component.javascript[0] +
componentDefinition +
component.javascript[1]
);
user_code = `let { ${props.join(', ')} } = $$props;`
}
const debugName = `<${component.customElement ? component.tag : name}>`;
// TODO concatenate CSS maps
const result = (deindent`
${js}
var ${name} = {};
// TODO only do this for props with a default value
const parent_bindings = component.javascript
? component.props.map(prop => {
return `if ($$props.${prop.as} === void 0 && $$bindings.${prop.as} && ${prop.name} !== void 0) $$bindings.${prop.as}(${prop.name});`;
})
: [];
${options.filename && `${name}.filename = ${stringify(options.filename)}`};
const main = renderer.has_bindings
? deindent`
let $$settled;
let $$rendered;
${name}.data = function() {
return ${templateProperties.data ? `%data()` : `{}`};
};
do {
$$settled = true;
${name}.render = function(state, options = {}) {
var components = new Set();
${component.reactive_declarations.map(d => d.snippet)}
function addComponent(component) {
components.add(component);
}
$$rendered = \`${renderer.code}\`;
} while (!$$settled);
var result = { head: '', addComponent };
var html = ${name}._render(result, state, options);
return $$rendered;
`
: deindent`
${component.reactive_declarations.map(d => d.snippet)}
var cssCode = Array.from(components).map(c => c.css && c.css.code).filter(Boolean).join('\\n');
return \`${renderer.code}\`;`;
return {
html,
head: result.head,
css: { code: cssCode, map: null },
toString() {
return html;
}
};
}
const blocks = [
user_code,
parent_bindings.join('\n'),
css.code && `$$result.css.add(#css);`,
main
].filter(Boolean);
${name}._render = function(__result, ctx, options) {
${templateProperties.store && `options.store = %store();`}
__result.addComponent(${name});
${options.dev && storeProps.length > 0 && !templateProperties.store && deindent`
if (!options.store) {
throw new Error("${debugName} references store properties, but no store was provided");
}
`}
ctx = Object.assign(${initialState.join(', ')});
${computations.map(
({ key }) => `ctx.${key} = %computed-${key}(ctx);`
)}
${renderer.bindings.length &&
deindent`
var settled = false;
var tmp;
while (!settled) {
settled = true;
${renderer.bindings.join('\n\n')}
}
`}
return \`${renderer.code}\`;
};
${name}.css = {
return (deindent`
${css.code && deindent`
const #css = {
code: ${css.code ? stringify(css.code) : `''`},
map: ${css.map ? stringify(css.map.toString()) : 'null'}
};
};`}
var warned = false;
${component.module_javascript}
${templateProperties.preload && `${name}.preload = %preload;`}
`).trim();
${component.fully_hoisted.length > 0 && component.fully_hoisted.join('\n\n')}
return component.generate(result, options, { name, format });
const ${name} = @create_ssr_component(($$result, $$props, $$bindings, $$slots) => {
${blocks.join('\n\n')}
});
`).trim();
}
function trim(nodes) {

@ -0,0 +1,3 @@
export function snip(expression) {
return `[✂${expression.node.start}-${expression.node.end}✂]`;
}

@ -1,16 +0,0 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function actions(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-actions`,
message: `The 'actions' property must be an object literal`
});
}
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
}

@ -1,21 +0,0 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function transitions(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-transitions-property`,
message: `The 'transitions' property must be an object literal`
});
}
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach(() => {
// TODO probably some validation that can happen here...
// checking for use of `this` etc?
});
}

@ -1,36 +0,0 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import getName from '../../../../utils/getName';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function components(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-components-property`,
message: `The 'components' property must be an object literal`
});
}
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach((node: Node) => {
const name = getName(node.key);
if (name === 'state') {
// TODO is this still true?
component.error(node, {
code: `invalid-name`,
message: `Component constructors cannot be called 'state' due to technical limitations`
});
}
if (!/^[A-Z]/.test(name)) {
component.error(node, {
code: `component-lowercase`,
message: `Component names must be capitalised`
});
}
});
}

@ -1,84 +0,0 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import getName from '../../../../utils/getName';
import isValidIdentifier from '../../../../utils/isValidIdentifier';
import reservedNames from '../../../../utils/reservedNames';
import { Node } from '../../../../interfaces';
import walkThroughTopFunctionScope from '../../../../utils/walkThroughTopFunctionScope';
import isThisGetCallExpression from '../../../../utils/isThisGetCallExpression';
import Component from '../../../Component';
const isFunctionExpression = new Set([
'FunctionExpression',
'ArrowFunctionExpression',
]);
export default function computed(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-computed-property`,
message: `The 'computed' property must be an object literal`
});
}
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach((computation: Node) => {
const name = getName(computation.key);
if (!isValidIdentifier(name)) {
const suggestion = name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');
component.error(computation.key, {
code: `invalid-computed-name`,
message: `Computed property name '${name}' is invalid — must be a valid identifier such as ${suggestion}`
});
}
if (reservedNames.has(name)) {
component.error(computation.key, {
code: `invalid-computed-name`,
message: `Computed property name '${name}' is invalid — cannot be a JavaScript reserved word`
});
}
if (!isFunctionExpression.has(computation.value.type)) {
component.error(computation.value, {
code: `invalid-computed-value`,
message: `Computed properties can be function expressions or arrow function expressions`
});
}
const { body, params } = computation.value;
walkThroughTopFunctionScope(body, (node: Node) => {
if (isThisGetCallExpression(node) && !node.callee.property.computed) {
component.error(node, {
code: `impure-computed`,
message: `Cannot use this.get(...) — values must be passed into the function as arguments`
});
}
if (node.type === 'ThisExpression') {
component.error(node, {
code: `impure-computed`,
message: `Computed properties should be pure functions — they do not have access to the component instance and cannot use 'this'. Did you mean to put this in 'methods'?`
});
}
});
if (params.length === 0) {
component.error(computation.value, {
code: `impure-computed`,
message: `A computed value must depend on at least one property`
});
}
if (params.length > 1) {
component.error(computation.value, {
code: `invalid-computed-arguments`,
message: `Computed properties must take a single argument`
});
}
});
}

@ -1,15 +0,0 @@
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
const disallowed = new Set(['Literal', 'ObjectExpression', 'ArrayExpression']);
export default function data(component: Component, prop: Node) {
while (prop.type === 'ParenthesizedExpression') prop = prop.expression;
if (disallowed.has(prop.value.type)) {
component.error(prop.value, {
code: `invalid-data-property`,
message: `'data' must be a function`
});
}
}

@ -1,16 +0,0 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function events(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-events-property`,
message: `The 'events' property must be an object literal`
});
}
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
}

@ -1,49 +0,0 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Node } from '../../../../interfaces';
import walkThroughTopFunctionScope from '../../../../utils/walkThroughTopFunctionScope';
import isThisGetCallExpression from '../../../../utils/isThisGetCallExpression';
import Component from '../../../Component';
export default function helpers(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-helpers-property`,
message: `The 'helpers' property must be an object literal`
});
}
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach((prop: Node) => {
if (!/FunctionExpression/.test(prop.value.type)) return;
let usesArguments = false;
walkThroughTopFunctionScope(prop.value.body, (node: Node) => {
if (isThisGetCallExpression(node) && !node.callee.property.computed) {
component.error(node, {
code: `impure-helper`,
message: `Cannot use this.get(...) — values must be passed into the helper function as arguments`
});
}
if (node.type === 'ThisExpression') {
component.error(node, {
code: `impure-helper`,
message: `Helpers should be pure functions — they do not have access to the component instance and cannot use 'this'. Did you mean to put this in 'methods'?`
});
} else if (node.type === 'Identifier' && node.name === 'arguments') {
usesArguments = true;
}
});
if (prop.value.params.length === 0 && !usesArguments) {
component.warn(prop, {
code: `impure-helper`,
message: `Helpers should be pure functions, with at least one argument`
});
}
});
}

@ -1,11 +0,0 @@
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function immutable(component: Component, prop: Node) {
if (prop.value.type !== 'Literal' || typeof prop.value.value !== 'boolean') {
component.error(prop.value, {
code: `invalid-immutable-property`,
message: `'immutable' must be a boolean literal`
});
}
}

@ -1,47 +0,0 @@
import data from './data';
import actions from './actions';
import animations from './animations';
import computed from './computed';
import oncreate from './oncreate';
import ondestroy from './ondestroy';
import onstate from './onstate';
import onupdate from './onupdate';
import onrender from './onrender';
import onteardown from './onteardown';
import helpers from './helpers';
import methods from './methods';
import components from './components';
import events from './events';
import namespace from './namespace';
import preload from './preload';
import props from './props';
import tag from './tag';
import transitions from './transitions';
import setup from './setup';
import store from './store';
import immutable from './immutable';
export default {
data,
actions,
animations,
computed,
oncreate,
ondestroy,
onstate,
onupdate,
onrender,
onteardown,
helpers,
methods,
components,
events,
namespace,
preload,
props,
tag,
transitions,
setup,
store,
immutable,
};

@ -1,42 +0,0 @@
import checkForAccessors from '../utils/checkForAccessors';
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import usesThisOrArguments from '../utils/usesThisOrArguments';
import getName from '../../../../utils/getName';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
const builtin = new Set(['set', 'get', 'on', 'fire', 'destroy']);
export default function methods(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-methods-property`,
message: `The 'methods' property must be an object literal`
});
}
checkForAccessors(component, prop.value.properties, 'Methods');
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach((prop: Node) => {
const name = getName(prop.key);
if (builtin.has(name)) {
component.error(prop, {
code: `invalid-method-name`,
message: `Cannot overwrite built-in method '${name}'`
});
}
if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) {
component.error(prop, {
code: `invalid-method-value`,
message: `Method '${prop.key.name}' should be a function expression, not an arrow function expression`
});
}
}
});
}

@ -1,33 +0,0 @@
import * as namespaces from '../../../../utils/namespaces';
import nodeToString from '../../../../utils/nodeToString'
import fuzzymatch from '../../utils/fuzzymatch';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
const valid = new Set(namespaces.validNamespaces);
export default function namespace(component: Component, prop: Node) {
const ns = nodeToString(prop.value);
if (typeof ns !== 'string') {
component.error(prop, {
code: `invalid-namespace-property`,
message: `The 'namespace' property must be a string literal representing a valid namespace`
});
}
if (!valid.has(ns)) {
const match = fuzzymatch(ns, namespaces.validNamespaces);
if (match) {
component.error(prop, {
code: `invalid-namespace-property`,
message: `Invalid namespace '${ns}' (did you mean '${match}'?)`
});
} else {
component.error(prop, {
code: `invalid-namespace-property`,
message: `Invalid namespace '${ns}'`
});
}
}
}

@ -1,14 +0,0 @@
import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function oncreate(component: Component, prop: Node) {
if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) {
component.error(prop, {
code: `invalid-oncreate-property`,
message: `'oncreate' should be a function expression, not an arrow function expression`
});
}
}
}

@ -1,14 +0,0 @@
import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function ondestroy(component: Component, prop: Node) {
if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) {
component.error(prop, {
code: `invalid-ondestroy-property`,
message: `'ondestroy' should be a function expression, not an arrow function expression`
});
}
}
}

@ -1,12 +0,0 @@
import oncreate from './oncreate';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function onrender(component: Component, prop: Node) {
component.warn(prop, {
code: `deprecated-onrender`,
message: `'onrender' has been deprecated in favour of 'oncreate', and will cause an error in Svelte 2.x`
});
oncreate(component, prop);
}

@ -1,14 +0,0 @@
import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function onstate(component: Component, prop: Node) {
if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) {
component.error(prop, {
code: `invalid-onstate-property`,
message: `'onstate' should be a function expression, not an arrow function expression`
});
}
}
}

@ -1,12 +0,0 @@
import ondestroy from './ondestroy';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function onteardown(component: Component, prop: Node) {
component.warn(prop, {
code: `deprecated-onteardown`,
message: `'onteardown' has been deprecated in favour of 'ondestroy', and will cause an error in Svelte 2.x`
});
ondestroy(component, prop);
}

@ -1,14 +0,0 @@
import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function onupdate(component: Component, prop: Node) {
if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) {
component.error(prop, {
code: `invalid-onupdate-property`,
message: `'onupdate' should be a function expression, not an arrow function expression`
});
}
}
}

@ -1,6 +0,0 @@
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function preload(component: Component, prop: Node) {
// not sure there's anything we need to check here...
}

@ -1,21 +0,0 @@
import { Node } from '../../../../interfaces';
import nodeToString from '../../../../utils/nodeToString';
import Component from '../../../Component';
export default function props(component: Component, prop: Node) {
if (prop.value.type !== 'ArrayExpression') {
component.error(prop.value, {
code: `invalid-props-property`,
message: `'props' must be an array expression, if specified`
});
}
prop.value.elements.forEach((element: Node) => {
if (typeof nodeToString(element) !== 'string') {
component.error(element, {
code: `invalid-props-property`,
message: `'props' must be an array of string literals`
});
}
});
}

@ -1,15 +0,0 @@
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
const disallowed = new Set(['Literal', 'ObjectExpression', 'ArrayExpression']);
export default function setup(component: Component, prop: Node) {
while (prop.type === 'ParenthesizedExpression') prop = prop.expression;
if (disallowed.has(prop.value.type)) {
component.error(prop.value, {
code: `invalid-setup-property`,
message: `'setup' must be a function`
});
}
}

@ -1,6 +0,0 @@
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function store(component: Component, prop: Node) {
// not sure there's anything we need to check here...
}

@ -1,20 +0,0 @@
import { Node } from '../../../../interfaces';
import nodeToString from '../../../../utils/nodeToString';
import Component from '../../../Component';
export default function tag(component: Component, prop: Node) {
const tag = nodeToString(prop.value);
if (typeof tag !== 'string') {
component.error(prop.value, {
code: `invalid-tag-property`,
message: `'tag' must be a string literal`
});
}
if (!/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
component.error(prop.value, {
code: `invalid-tag-property`,
message: `tag name must be two or more words joined by the '-' character`
});
}
}

@ -1,21 +0,0 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function transitions(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-transitions-property`,
message: `The 'transitions' property must be an object literal`
});
}
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach(() => {
// TODO probably some validation that can happen here...
// checking for use of `this` etc?
});
}

@ -1,17 +0,0 @@
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function checkForAccessors(
component: Component,
properties: Node[],
label: string
) {
properties.forEach(prop => {
if (prop.kind !== 'init') {
component.error(prop, {
code: `illegal-accessor`,
message: `${label} cannot use getters and setters`
});
}
});
}

@ -1,16 +0,0 @@
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function checkForComputedKeys(
component: Component,
properties: Node[]
) {
properties.forEach(prop => {
if (prop.key.computed) {
component.error(prop, {
code: `computed-key`,
message: `Cannot use computed keys`
});
}
});
}

@ -1,23 +0,0 @@
import { Node } from '../../../../interfaces';
import getName from '../../../../utils/getName';
import Component from '../../../Component';
export default function checkForDupes(
component: Component,
properties: Node[]
) {
const seen = new Set();
properties.forEach(prop => {
const name = getName(prop.key);
if (seen.has(name)) {
component.error(prop, {
code: `duplicate-property`,
message: `Duplicate property '${name}'`
});
}
seen.add(name);
});
}

@ -1,33 +0,0 @@
import { walk } from 'estree-walker';
import isReference from 'is-reference';
import { Node } from '../../../../interfaces';
export default function usesThisOrArguments(node: Node) {
let result = false;
walk(node, {
enter(node: Node, parent: Node) {
if (
result ||
node.type === 'FunctionExpression' ||
node.type === 'FunctionDeclaration'
) {
return this.skip();
}
if (node.type === 'ThisExpression') {
result = true;
}
if (
node.type === 'Identifier' &&
isReference(node, parent) &&
node.name === 'arguments'
) {
result = true;
}
},
});
return result;
}

@ -1,8 +0,0 @@
import FuzzySet from './FuzzySet';
export default function fuzzymatch(name: string, names: string[]) {
const set = new FuzzySet(names);
const matches = set.get(name);
return matches && matches[0] && matches[0][0] > 0.7 ? matches[0][1] : null;
}

@ -1,6 +1,6 @@
import deindent from '../utils/deindent';
import list from '../utils/list';
import { CompileOptions, ModuleFormat, Node, ShorthandImport } from '../interfaces';
import { CompileOptions, ModuleFormat, Node } from '../interfaces';
interface Dependency {
name: string;
@ -8,7 +8,12 @@ interface Dependency {
source: string;
}
const wrappers = { es, amd, cjs, iife, umd, eval: expr };
const wrappers = { esm, cjs, eval: expr };
type Export = {
name: string;
as: string;
};
export default function wrapModule(
code: string,
@ -16,230 +21,167 @@ export default function wrapModule(
name: string,
options: CompileOptions,
banner: string,
sharedPath: string,
sveltePath = 'svelte',
helpers: { name: string, alias: string }[],
imports: Node[],
shorthandImports: ShorthandImport[],
module_exports: Export[],
source: string
): string {
if (format === 'es') return es(code, name, options, banner, sharedPath, helpers, imports, shorthandImports, source);
const dependencies = imports.map((declaration, i) => {
const defaultImport = declaration.specifiers.find(
(x: Node) =>
x.type === 'ImportDefaultSpecifier' ||
(x.type === 'ImportSpecifier' && x.imported.name === 'default')
);
const namespaceImport = declaration.specifiers.find(
(x: Node) => x.type === 'ImportNamespaceSpecifier'
);
const namedImports = declaration.specifiers.filter(
(x: Node) =>
x.type === 'ImportSpecifier' && x.imported.name !== 'default'
);
const name = defaultImport || namespaceImport
? (defaultImport || namespaceImport).local.name
: `__import${i}`;
const statements: string[] = [];
namedImports.forEach((specifier: Node) => {
statements.push(
`var ${specifier.local.name} = ${name}.${specifier.imported.name};`
);
});
if (defaultImport) {
statements.push(
`${name} = (${name} && ${name}.__esModule) ? ${name}["default"] : ${name};`
);
}
const internalPath = `${sveltePath}/internal.js`;
return { name, statements, source: declaration.source.value };
})
.concat(
shorthandImports.map(({ name, source }) => ({
name,
statements: [
`${name} = (${name} && ${name}.__esModule) ? ${name}["default"] : ${name};`,
],
source,
}))
);
if (format === 'esm') {
return esm(code, name, options, banner, sveltePath, internalPath, helpers, imports, module_exports, source);
}
if (format === 'amd') return amd(code, name, options, banner, dependencies);
if (format === 'cjs') return cjs(code, name, options, banner, sharedPath, helpers, dependencies);
if (format === 'iife') return iife(code, name, options, banner, dependencies);
if (format === 'umd') return umd(code, name, options, banner, dependencies);
if (format === 'eval') return expr(code, name, options, banner, dependencies);
if (format === 'cjs') return cjs(code, name, banner, sveltePath, internalPath, helpers, imports, module_exports);
if (format === 'eval') return expr(code, name, options, banner, imports);
throw new Error(`options.format is invalid (must be ${list(Object.keys(wrappers))})`);
}
function es(
function esm(
code: string,
name: string,
options: CompileOptions,
banner: string,
sharedPath: string,
sveltePath: string,
internalPath: string,
helpers: { name: string, alias: string }[],
imports: Node[],
shorthandImports: ShorthandImport[],
module_exports: Export[],
source: string
) {
const importHelpers = helpers.length > 0 && (
`import { ${helpers.map(h => h.name === h.alias ? h.name : `${h.name} as ${h.alias}`).join(', ')} } from ${JSON.stringify(sharedPath)};`
`import { ${helpers.map(h => h.name === h.alias ? h.name : `${h.name} as ${h.alias}`).join(', ')} } from ${JSON.stringify(internalPath)};`
);
const importBlock = imports.length > 0 && (
imports
.map((declaration: Node) => source.slice(declaration.start, declaration.end))
.join('\n')
);
.map((declaration: Node) => {
const import_source = declaration.source.value === 'svelte' ? sveltePath : declaration.source.value;
const shorthandImportBlock = shorthandImports.length > 0 && (
shorthandImports.map(({ name, source }) => `import ${name} from ${JSON.stringify(source)};`).join('\n')
return (
source.slice(declaration.start, declaration.source.start) +
JSON.stringify(import_source) +
source.slice(declaration.source.end, declaration.end)
);
})
.join('\n')
);
return deindent`
${banner}
${importHelpers}
${importBlock}
${shorthandImportBlock}
${code}
export default ${name};`;
}
function amd(
code: string,
name: string,
options: CompileOptions,
banner: string,
dependencies: Dependency[]
) {
const sourceString = dependencies.length
? `[${dependencies.map(d => `"${removeExtension(d.source)}"`).join(', ')}], `
: '';
const id = options.amd && options.amd.id;
return deindent`
define(${id ? `"${id}", ` : ''}${sourceString}function(${paramString(dependencies)}) { "use strict";
${getCompatibilityStatements(dependencies)}
${code}
return ${name};
});`;
export default ${name};
${module_exports.length > 0 && `export { ${module_exports.map(e => e.name === e.as ? e.name : `${e.name} as ${e.as}`).join(', ')} };`}`;
}
function cjs(
code: string,
name: string,
options: CompileOptions,
banner: string,
sharedPath: string,
sveltePath: string,
internalPath: string,
helpers: { name: string, alias: string }[],
dependencies: Dependency[]
imports: Node[],
module_exports: Export[]
) {
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 { ${helperDeclarations} } = require(${JSON.stringify(internalPath)});\n`
);
const requireBlock = dependencies.length > 0 && (
dependencies
.map(d => `var ${d.name} = require("${d.source}");`)
.join('\n\n')
);
const requires = imports.map(node => {
let lhs;
return deindent`
${banner}
"use strict";
if (node.specifiers[0].type === 'ImportNamespaceSpecifier') {
lhs = node.specifiers[0].local.name;
} else {
const properties = node.specifiers.map(s => {
if (s.type === 'ImportDefaultSpecifier') {
return `default: ${s.local.name}`;
}
${helperBlock}
${requireBlock}
${getCompatibilityStatements(dependencies)}
return s.local.name === s.imported.name
? s.local.name
: `${s.imported.name}: ${s.local.name}`;
});
${code}
lhs = `{ ${properties.join(', ')} }`;
}
module.exports = ${name};`
}
const source = node.source.value === 'svelte'
? sveltePath
: node.source.value;
function iife(
code: string,
name: string,
options: CompileOptions,
banner: string,
dependencies: Dependency[]
) {
if (!options.name) {
throw new Error(`Missing required 'name' option for IIFE export`);
}
return `const ${lhs} = require("${source}");`
});
const globals = getGlobals(dependencies, options);
const exports = [`exports.default = ${name};`].concat(
module_exports.map(x => `exports.${x.as} = ${x.name};`)
);
return deindent`
${banner}
var ${options.name} = (function(${paramString(dependencies)}) { "use strict";
${getCompatibilityStatements(dependencies)}
"use strict";
${code}
return ${name};
}(${globals.join(', ')}));`;
${helperBlock}
${requires}
${code}
${exports}`
}
function umd(
function expr(
code: string,
name: string,
options: CompileOptions,
banner: string,
dependencies: Dependency[]
imports: Node[]
) {
if (!options.name) {
throw new Error(`Missing required 'name' option for UMD export`);
}
const amdId = options.amd && options.amd.id ? `'${options.amd.id}', ` : '';
const amdDeps = dependencies.length
? `[${dependencies.map(d => `"${removeExtension(d.source)}"`).join(', ')}], `
: '';
const dependencies = imports.map((declaration, i) => {
const defaultImport = declaration.specifiers.find(
(x: Node) =>
x.type === 'ImportDefaultSpecifier' ||
(x.type === 'ImportSpecifier' && x.imported.name === 'default')
);
const cjsDeps = dependencies
.map(d => `require("${d.source}")`)
.join(', ');
const namespaceImport = declaration.specifiers.find(
(x: Node) => x.type === 'ImportNamespaceSpecifier'
);
const globals = getGlobals(dependencies, options);
const namedImports = declaration.specifiers.filter(
(x: Node) =>
x.type === 'ImportSpecifier' && x.imported.name !== 'default'
);
return deindent`
${banner}
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory(${cjsDeps}) :
typeof define === "function" && define.amd ? define(${amdId}${amdDeps}factory) :
(global.${options.name} = factory(${globals.join(', ')}));
}(this, (function (${paramString(dependencies)}) { "use strict";
const name = defaultImport || namespaceImport
? (defaultImport || namespaceImport).local.name
: `__import${i}`;
${getCompatibilityStatements(dependencies)}
const statements: string[] = [];
${code}
namedImports.forEach((specifier: Node) => {
statements.push(
`var ${specifier.local.name} = ${name}.${specifier.imported.name};`
);
});
return ${name};
if (defaultImport) {
statements.push(
`${name} = (${name} && ${name}.__esModule) ? ${name}["default"] : ${name};`
);
}
})));`;
}
return { name, statements, source: declaration.source.value };
});
function expr(
code: string,
name: string,
options: CompileOptions,
banner: string,
dependencies: Dependency[]
) {
const globals = getGlobals(dependencies, options);
return deindent`
@ -258,11 +200,6 @@ function paramString(dependencies: Dependency[]) {
return dependencies.map(dep => dep.name).join(', ');
}
function removeExtension(file: string) {
const index = file.lastIndexOf('.');
return ~index ? file.slice(0, index) : file;
}
function getCompatibilityStatements(dependencies: Dependency[]) {
if (!dependencies.length) return null;

@ -1,34 +1,4 @@
import compile from './compile/index';
import { CompileOptions } from './interfaces';
import deprecate from './utils/deprecate';
export function create(source: string, options: CompileOptions = {}) {
const onerror = options.onerror || (err => {
throw err;
});
if (options.onerror) {
// TODO remove in v3
deprecate(`Instead of using options.onerror, wrap svelte.create in a try-catch block`);
delete options.onerror;
}
options.format = 'eval';
try {
const compiled = compile(source, options);
if (!compiled || !compiled.js.code) {
return;
}
return (new Function(`return ${compiled.js.code}`))();
} catch (err) {
onerror(err);
}
}
export { default as compile } from './compile/index';
export { default as parse } from './parse/index';
export { default as preprocess } from './preprocess/index';
export const VERSION = '__VERSION__';

@ -35,7 +35,7 @@ export interface Warning {
toString: () => string;
}
export type ModuleFormat = 'es' | 'amd' | 'cjs' | 'iife' | 'umd' | 'eval';
export type ModuleFormat = 'esm' | 'cjs' | 'eval';
export interface CompileOptions {
format?: ModuleFormat;
@ -49,10 +49,10 @@ export interface CompileOptions {
outputFilename?: string;
cssOutputFilename?: string;
sveltePath?: string;
dev?: boolean;
immutable?: boolean;
shared?: boolean | string;
hydratable?: boolean;
legacy?: boolean;
customElement?: CustomElementOptions | true;
@ -61,18 +61,8 @@ export interface CompileOptions {
preserveComments?: boolean | false;
onwarn?: (warning: Warning) => void;
// to remove in v3
onerror?: (error: Error) => void;
skipIntroByDefault?: boolean;
nestedTransitions?: boolean;
}
export interface ShorthandImport {
name: string;
source: string;
};
export interface Visitor {
enter: (node: Node) => void;
leave?: (node: Node) => void;

@ -0,0 +1,207 @@
import { add_render_callback, flush, intro, schedule_update } from './scheduler.js';
import { current_component, set_current_component } from './lifecycle.js'
import { is_function, run, run_all, noop } from './utils.js';
import { blankObject } from './utils.js';
import { children } from './dom.js';
export function bind(component, name, callback) {
component.$$.bound[name] = callback;
callback(component.$$.get()[name]);
}
export function mount_component(component, target, anchor) {
const { fragment, refs, inject_refs, on_mount, on_destroy, after_render } = component.$$;
fragment[fragment.i ? 'i' : 'm'](target, anchor);
inject_refs(refs);
// onMount happens after the initial afterUpdate. Because
// afterUpdate callbacks happen in reverse order (inner first)
// we schedule onMount callbacks before afterUpdate callbacks
add_render_callback(() => {
const new_on_destroy = on_mount.map(run).filter(is_function);
if (on_destroy) {
on_destroy.push(...new_on_destroy);
} else {
// Edge case — component was destroyed immediately,
// most likely as a result of a binding initialising
run_all(new_on_destroy);
}
component.$$.on_mount = [];
});
after_render.forEach(add_render_callback);
}
function destroy(component, detach) {
if (component.$$) {
run_all(component.$$.on_destroy);
component.$$.fragment.d(detach);
// TODO null out other refs, including component.$$ (but need to
// preserve final state?)
component.$$.on_destroy = component.$$.fragment = null;
component.$$.get = () => ({});
}
}
function make_dirty(component, key) {
if (!component.$$.dirty) {
schedule_update(component);
component.$$.dirty = {};
}
component.$$.dirty[key] = true;
}
function empty() {
return {};
}
export function init(component, options, define, create_fragment, not_equal) {
const previous_component = current_component;
set_current_component(component);
component.$$ = {
fragment: null,
// state
get: empty,
set: noop,
update: noop,
inject_refs: noop,
not_equal,
bound: blankObject(),
// lifecycle
on_mount: [],
on_destroy: [],
before_render: [],
after_render: [],
// everything else
callbacks: blankObject(),
slotted: options.slots || {},
refs: {},
dirty: null,
binding_groups: []
};
let ready = false;
define(component, options.props || {}, key => {
if (ready) make_dirty(component, key);
if (component.$$.bound[key]) component.$$.bound[key](component.$$.get()[key]);
});
component.$$.update();
ready = true;
run_all(component.$$.before_render);
component.$$.fragment = create_fragment(component, component.$$.get());
if (options.target) {
intro.enabled = !!options.intro;
if (options.hydrate) {
component.$$.fragment.l(children(options.target));
} else {
component.$$.fragment.c();
}
mount_component(component, options.target, options.anchor);
flush();
intro.enabled = true;
}
set_current_component(previous_component);
}
export let SvelteElement;
if (typeof HTMLElement !== 'undefined') {
SvelteElement = class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
for (let key in this.$$.slotted) {
this.appendChild(this.$$.slotted[key]);
}
}
attributeChangedCallback(attr, oldValue, newValue) {
this[attr] = newValue;
}
$destroy() {
destroy(this, true);
this.$destroy = noop;
}
$on(type, callback) {
// TODO should this delegate to addEventListener?
const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
callbacks.push(callback);
return () => {
const index = callbacks.indexOf(callback);
if (index !== -1) callbacks.splice(index, 1);
};
}
$set(values) {
if (this.$$) {
const state = this.$$.get();
this.$$.set(values);
for (const key in values) {
if (this.$$.not_equal(state[key], values[key])) make_dirty(this, key);
}
}
}
}
}
export class SvelteComponent {
$destroy() {
destroy(this, true);
this.$destroy = noop;
}
$on(type, callback) {
const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
callbacks.push(callback);
return () => {
const index = callbacks.indexOf(callback);
if (index !== -1) callbacks.splice(index, 1);
};
}
$set(values) {
if (this.$$) {
const state = this.$$.get();
this.$$.set(values);
for (const key in values) {
if (this.$$.not_equal(state[key], values[key])) make_dirty(this, key);
}
}
}
}
export class SvelteComponentDev extends SvelteComponent {
constructor(options) {
if (!options || (!options.target && !options.$$inline)) {
throw new Error(`'target' is a required option`);
}
super();
}
$destroy() {
super.$destroy();
this.$destroy = () => {
console.warn(`Component was already destroyed`);
};
}
}

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

Loading…
Cancel
Save