Merge branch 'master' into gh-927

pull/932/head
Rich Harris 8 years ago
commit 5f84375d42

@ -1,5 +1,12 @@
# Svelte changelog
## 1.42.0
* Implement `indeterminate` binding for checkbox inputs ([#910](https://github.com/sveltejs/svelte/issues/910))
* Use `<option>` children as `value` attribute if none exists ([#928](https://github.com/sveltejs/svelte/issues/928))
* Allow quoted property names in default export and sub-properties ([#914](https://github.com/sveltejs/svelte/issues/914))
* Various improvements to generated code for bindings
## 1.41.4
* Handle self-destructive bindings ([#917](https://github.com/sveltejs/svelte/issues/917))

@ -1,4 +1,5 @@
--require babel-register
--compilers ts-node/register
--require source-map-support/register
--full-trace
--recursive
./**/__test__.js
test/*/index.js
test/test.js

@ -1,6 +1,6 @@
{
"name": "svelte",
"version": "1.41.4",
"version": "1.42.0",
"description": "The magical disappearing UI framework",
"main": "compiler/svelte.js",
"files": [
@ -51,7 +51,7 @@
"eslint": "^4.3.0",
"eslint-plugin-html": "^3.0.0",
"eslint-plugin-import": "^2.2.0",
"estree-walker": "^0.5.0",
"estree-walker": "^0.5.1",
"glob": "^7.1.1",
"jsdom": "^11.1.0",
"locate-character": "^2.0.0",
@ -61,27 +61,28 @@
"node-resolve": "^1.3.3",
"nyc": "^11.1.0",
"prettier": "^1.7.0",
"reify": "^0.12.3",
"rollup": "^0.48.2",
"rollup-plugin-buble": "^0.15.0",
"rollup-plugin-commonjs": "^8.0.2",
"rollup-plugin-json": "^2.1.0",
"rollup-plugin-node-resolve": "^3.0.0",
"rollup-plugin-replace": "^2.0.0",
"rollup-plugin-typescript": "^0.8.1",
"rollup-plugin-virtual": "^1.0.1",
"rollup-watch": "^4.3.1",
"source-map": "^0.5.6",
"source-map-support": "^0.4.8",
"ts-node": "^3.3.0",
"tslib": "^1.8.0",
"typescript": "^2.3.2"
"typescript": "^2.6.1"
},
"nyc": {
"include": [
"src/**/*.js",
"compiler/svelte.js",
"shared.js"
],
"exclude": [
"src/**/__test__.js",
"src/shared/**"
]
"sourceMap": true,
"instrument": true
}
}

@ -1,9 +1,11 @@
import path from 'path';
import replace from 'rollup-plugin-replace';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import json from 'rollup-plugin-json';
import typescript from 'rollup-plugin-typescript';
import buble from 'rollup-plugin-buble';
import pkg from './package.json';
const src = path.resolve('src');
@ -27,6 +29,9 @@ export default [
}
}
},
replace({
__VERSION__: pkg.version
}),
resolve(),
commonjs(),
json(),

@ -0,0 +1 @@
export const test = typeof process !== 'undefined' && process.env.TEST;

@ -322,7 +322,7 @@ export default class Stylesheet {
leave: (node: Node) => {
if (node.type === 'Rule' || node.type === 'Atrule') stack.pop();
if (node.type === 'Atrule') currentAtrule = stack[stack.length - 1];
if (node.type === 'Atrule') currentAtrule = <Atrule>stack[stack.length - 1];
}
});
} else {

@ -1,25 +1,24 @@
import MagicString, { Bundle } from 'magic-string';
import { walk } from 'estree-walker';
import { walk, childKeys } from 'estree-walker';
import { getLocator } from 'locate-character';
import deindent from '../utils/deindent';
import CodeBuilder from '../utils/CodeBuilder';
import getCodeFrame from '../utils/getCodeFrame';
import isReference from '../utils/isReference';
import flattenReference from '../utils/flattenReference';
import globalWhitelist from '../utils/globalWhitelist';
import reservedNames from '../utils/reservedNames';
import namespaces from '../utils/namespaces';
import { removeNode, removeObjectKey } from '../utils/removeNode';
import wrapModule from './shared/utils/wrapModule';
import annotateWithScopes from '../utils/annotateWithScopes';
import annotateWithScopes, { Scope } from '../utils/annotateWithScopes';
import getName from '../utils/getName';
import clone from '../utils/clone';
import DomBlock from './dom/Block';
import SsrBlock from './server-side-rendering/Block';
import Stylesheet from '../css/Stylesheet';
import { test } from '../config';
import { Node, GenerateOptions, Parsed, CompileOptions, CustomElementOptions } from '../interfaces';
const test = typeof global !== 'undefined' && global.__svelte_test;
interface Computation {
key: string;
deps: string[]
@ -70,6 +69,19 @@ function removeIndentation(
}
}
// We need to tell estree-walker that it should always
// look for an `else` block, otherwise it might get
// the wrong idea about the shape of each/if blocks
childKeys.EachBlock = [
'children',
'else'
];
childKeys.IfBlock = [
'children',
'else'
];
export default class Generator {
ast: Parsed;
parsed: Parsed;
@ -155,7 +167,12 @@ export default class Generator {
this.aliases = new Map();
this.usedNames = new Set();
this.parseJs(dom);
this.computations = [];
this.templateProperties = {};
this.walkJs(dom);
this.walkTemplate();
this.name = this.alias(name);
if (options.customElement === true) {
@ -195,12 +212,10 @@ export default class Generator {
context: string,
isEventHandler: boolean
): {
dependencies: string[],
contexts: Set<string>,
indexes: Set<string>,
snippet: string
indexes: Set<string>
} {
this.addSourcemapLocations(expression);
// this.addSourcemapLocations(expression);
const usedContexts: Set<string> = new Set();
const usedIndexes: Set<string> = new Set();
@ -208,7 +223,7 @@ export default class Generator {
const { code, helpers } = this;
const { contexts, indexes } = block;
let scope = annotateWithScopes(expression); // TODO this already happens in findDependencies
let scope: Scope;
let lexicalDepth = 0;
const self = this;
@ -230,7 +245,7 @@ export default class Generator {
});
} else if (isReference(node, parent)) {
const { name } = flattenReference(node);
if (scope.has(name)) return;
if (scope && scope.has(name)) return;
if (name === 'event' && isEventHandler) {
// noop
@ -266,16 +281,7 @@ export default class Generator {
}
}
if (globalWhitelist.has(name)) {
code.prependRight(node.start, `('${name}' in state ? state.`);
code.appendLeft(
node.object ? node.object.end : node.end,
` : ${name})`
);
} else {
code.prependRight(node.start, `state.`);
}
code.prependRight(node.start, `state.`);
usedContexts.add('state');
}
@ -289,73 +295,12 @@ export default class Generator {
},
});
const dependencies: Set<string> = new Set(expression._dependencies || []);
if (expression._dependencies) {
expression._dependencies.forEach((prop: string) => {
if (this.indirectDependencies.has(prop)) {
this.indirectDependencies.get(prop).forEach(dependency => {
dependencies.add(dependency);
});
}
});
}
return {
dependencies: Array.from(dependencies),
contexts: usedContexts,
indexes: usedIndexes,
snippet: `[✂${expression.start}-${expression.end}✂]`,
indexes: usedIndexes
};
}
findDependencies(
contextDependencies: Map<string, string[]>,
indexes: Map<string, string>,
expression: Node
) {
if (expression._dependencies) return expression._dependencies;
let scope = annotateWithScopes(expression);
const dependencies: string[] = [];
const generator = this; // can't use arrow functions, because of this.skip()
walk(expression, {
enter(node: Node, parent: Node) {
if (node._scope) {
scope = node._scope;
return;
}
if (isReference(node, parent)) {
const { name } = flattenReference(node);
if (scope.has(name) || generator.helpers.has(name)) return;
if (contextDependencies.has(name)) {
dependencies.push(...contextDependencies.get(name));
} else if (!indexes.has(name)) {
dependencies.push(name);
}
this.skip();
}
},
leave(node: Node) {
if (node._scope) scope = scope.parent;
},
});
dependencies.forEach(name => {
if (!globalWhitelist.has(name)) {
this.expectedProperties.add(name);
}
});
return (expression._dependencies = dependencies);
}
generate(result: string, options: CompileOptions, { banner = '', sharedPath, helpers, name, format }: GenerateOptions ) {
const pattern = /\[✂(\d+)-(\d+)$/;
@ -453,17 +398,19 @@ export default class Generator {
};
}
parseJs(dom: boolean) {
const { code, source } = this;
walkJs(dom: boolean) {
const {
code,
source,
computations,
templateProperties,
imports
} = this;
const { js } = this.parsed;
const imports = this.imports;
const computations: Computation[] = [];
const templateProperties: Record<string, Node> = {};
const componentDefinition = new CodeBuilder();
let namespace = null;
if (js) {
this.addSourcemapLocations(js.content);
@ -497,13 +444,13 @@ export default class Generator {
if (defaultExport) {
defaultExport.declaration.properties.forEach((prop: Node) => {
templateProperties[prop.key.name] = prop;
templateProperties[getName(prop.key)] = prop;
});
['helpers', 'events', 'components', 'transitions'].forEach(key => {
if (templateProperties[key]) {
templateProperties[key].value.properties.forEach((prop: Node) => {
this[key].add(prop.key.name);
this[key].add(getName(prop.key));
});
}
});
@ -574,7 +521,7 @@ export default class Generator {
if (templateProperties.components) {
templateProperties.components.value.properties.forEach((property: Node) => {
addDeclaration(property.key.name, property.value, 'components');
addDeclaration(getName(property.key), property.value, 'components');
});
}
@ -582,7 +529,7 @@ export default class Generator {
const dependencies = new Map();
templateProperties.computed.value.properties.forEach((prop: Node) => {
const key = prop.key.name;
const key = getName(prop.key);
const value = prop.value;
const deps = value.params.map(
@ -605,12 +552,12 @@ export default class Generator {
computations.push({ key, deps });
const prop = templateProperties.computed.value.properties.find((prop: Node) => prop.key.name === key);
const prop = templateProperties.computed.value.properties.find((prop: Node) => getName(prop.key) === key);
addDeclaration(key, prop.value, 'computed');
};
templateProperties.computed.value.properties.forEach((prop: Node) =>
visit(prop.key.name)
visit(getName(prop.key))
);
}
@ -620,13 +567,13 @@ export default class Generator {
if (templateProperties.events && dom) {
templateProperties.events.value.properties.forEach((property: Node) => {
addDeclaration(property.key.name, property.value, 'events');
addDeclaration(getName(property.key), property.value, 'events');
});
}
if (templateProperties.helpers) {
templateProperties.helpers.value.properties.forEach((property: Node) => {
addDeclaration(property.key.name, property.value, 'helpers');
addDeclaration(getName(property.key), property.value, 'helpers');
});
}
@ -636,7 +583,7 @@ export default class Generator {
if (templateProperties.namespace) {
const ns = templateProperties.namespace.value.value;
namespace = namespaces[ns] || ns;
this.namespace = namespaces[ns] || ns;
}
if (templateProperties.onrender) templateProperties.oncreate = templateProperties.onrender; // remove after v2
@ -663,7 +610,7 @@ export default class Generator {
if (templateProperties.transitions) {
templateProperties.transitions.value.properties.forEach((property: Node) => {
addDeclaration(property.key.name, property.value, 'transitions');
addDeclaration(getName(property.key), property.value, 'transitions');
});
}
}
@ -692,9 +639,132 @@ export default class Generator {
this.javascript = a === b ? null : `[✂${a}-${b}✂]`;
}
}
}
walkTemplate() {
const {
code,
expectedProperties,
helpers
} = this;
const { html } = this.parsed;
const contextualise = (node: Node, contextDependencies: Map<string, string[]>, indexes: Set<string>) => {
this.addSourcemapLocations(node); // TODO this involves an additional walk — can we roll it in somewhere else?
let scope = annotateWithScopes(node);
const dependencies: Set<string> = new Set();
this.computations = computations;
this.namespace = namespace;
this.templateProperties = templateProperties;
walk(node, {
enter(node: Node, parent: Node) {
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
if (node._scope) {
scope = node._scope;
return;
}
if (isReference(node, parent)) {
const { name } = flattenReference(node);
if (scope && scope.has(name) || helpers.has(name)) return;
if (contextDependencies.has(name)) {
contextDependencies.get(name).forEach(dependency => {
dependencies.add(dependency);
});
} else if (!indexes.has(name)) {
dependencies.add(name);
}
this.skip();
}
},
leave(node: Node, parent: Node) {
if (node._scope) scope = scope.parent;
}
});
dependencies.forEach(dependency => {
expectedProperties.add(dependency);
});
return {
snippet: `[✂${node.start}-${node.end}✂]`,
dependencies: Array.from(dependencies)
};
}
const contextStack = [];
const indexStack = [];
const dependenciesStack = [];
let contextDependencies = new Map();
const contextDependenciesStack: Map<string, string[]>[] = [contextDependencies];
let indexes = new Set();
const indexesStack: Set<string>[] = [indexes];
walk(html, {
enter(node: Node, parent: Node) {
if (node.type === 'EachBlock') {
node.metadata = contextualise(node.expression, contextDependencies, indexes);
contextDependencies = new Map(contextDependencies);
contextDependencies.set(node.context, node.metadata.dependencies);
if (node.destructuredContexts) {
for (let i = 0; i < node.destructuredContexts.length; i += 1) {
const name = node.destructuredContexts[i];
const value = `${node.context}[${i}]`;
contextDependencies.set(name, node.metadata.dependencies);
}
}
contextDependenciesStack.push(contextDependencies);
if (node.index) {
indexes = new Set(indexes);
indexes.add(node.index);
indexesStack.push(indexes);
}
}
if (node.type === 'IfBlock') {
node.metadata = contextualise(node.expression, contextDependencies, indexes);
}
if (node.type === 'MustacheTag' || node.type === 'RawMustacheTag' || node.type === 'AttributeShorthand') {
node.metadata = contextualise(node.expression, contextDependencies, indexes);
this.skip();
}
if (node.type === 'Binding') {
node.metadata = contextualise(node.value, contextDependencies, indexes);
this.skip();
}
if (node.type === 'EventHandler' && node.expression) {
node.expression.arguments.forEach((arg: Node) => {
arg.metadata = contextualise(arg, contextDependencies, indexes);
});
this.skip();
}
},
leave(node: Node, parent: Node) {
if (node.type === 'EachBlock') {
contextDependenciesStack.pop();
contextDependencies = contextDependenciesStack[contextDependenciesStack.length - 1];
if (node.index) {
indexesStack.pop();
indexes = indexesStack[indexesStack.length - 1];
}
}
}
});
}
}

@ -10,12 +10,12 @@ export interface BlockOptions {
generator?: DomGenerator;
expression?: Node;
context?: string;
destructuredContexts?: string[];
comment?: string;
key?: string;
contexts?: Map<string, string>;
indexes?: Map<string, string>;
changeableIndexes?: Map<string, boolean>;
contextDependencies?: Map<string, string[]>;
params?: string[];
indexNames?: Map<string, string>;
listNames?: Map<string, string>;
@ -38,7 +38,6 @@ export default class Block {
contexts: Map<string, string>;
indexes: Map<string, string>;
changeableIndexes: Map<string, boolean>;
contextDependencies: Map<string, string[]>;
dependencies: Set<string>;
params: string[];
indexNames: Map<string, string>;
@ -86,7 +85,6 @@ export default class Block {
this.contexts = options.contexts;
this.indexes = options.indexes;
this.changeableIndexes = options.changeableIndexes;
this.contextDependencies = options.contextDependencies;
this.dependencies = new Set();
this.params = options.params;
@ -176,14 +174,6 @@ export default class Block {
);
}
findDependencies(expression: Node) {
return this.generator.findDependencies(
this.contextDependencies,
this.indexes,
expression
);
}
mount(name: string, parentNode: string) {
if (parentNode) {
this.builders.mount.addLine(`@appendNode(${name}, ${parentNode});`);
@ -212,7 +202,7 @@ export default class Block {
}
// minor hack we need to ensure that any {{{triples}}} are detached first
this.builders.unmount.addBlockAtStart(this.builders.detachRaw);
this.builders.unmount.addBlockAtStart(this.builders.detachRaw.toString());
const properties = new CodeBuilder();

@ -6,6 +6,7 @@ import { walk } from 'estree-walker';
import deindent from '../../utils/deindent';
import { stringify, escape } from '../../utils/stringify';
import CodeBuilder from '../../utils/CodeBuilder';
import globalWhitelist from '../../utils/globalWhitelist';
import reservedNames from '../../utils/reservedNames';
import visit from './visit';
import shared from './shared';
@ -13,11 +14,9 @@ import Generator from '../Generator';
import Stylesheet from '../../css/Stylesheet';
import preprocess from './preprocess';
import Block from './Block';
import { version } from '../../../package.json';
import { test } from '../../config';
import { Parsed, CompileOptions, Node } from '../../interfaces';
const test = typeof global !== 'undefined' && global.__svelte_test;
export class DomGenerator extends Generator {
blocks: (Block|string)[];
readonly: Set<string>;
@ -166,9 +165,9 @@ export default function dom(
builder.addBlock(block.toString());
});
const sharedPath = options.shared === true
const sharedPath: string = options.shared === true
? 'svelte/shared.js'
: options.shared;
: options.shared || '';
const prototypeBase =
`${name}.prototype` +
@ -184,13 +183,28 @@ export default function dom(
const debugName = `<${generator.customElement ? generator.tag : name}>`;
// generate initial state object
const globals = Array.from(generator.expectedProperties).filter(prop => globalWhitelist.has(prop));
const initialState = [];
if (globals.length > 0) {
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
}
if (templateProperties.data) {
initialState.push(`%data()`);
} else if (globals.length === 0) {
initialState.push('{}');
}
initialState.push(`options.data`);
const constructorBody = deindent`
${options.dev && `this._debugName = '${debugName}';`}
${options.dev && !generator.customElement &&
`if (!options || (!options.target && !options._root)) throw new Error("'target' is a required option");`}
@init(this, options);
${generator.usesRefs && `this.refs = {};`}
this._state = @assign(${templateProperties.data ? '%data()' : '{}'}, options.data);
this._state = @assign(${initialState.join(', ')});
${generator.metaBindings}
${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`}
${options.dev &&
@ -428,7 +442,7 @@ export default function dom(
);
return generator.generate(result, options, {
banner: `/* ${filename ? `${filename} ` : ``}generated by Svelte v${version} */`,
banner: `/* ${filename ? `${filename} ` : ``}generated by Svelte v${"__VERSION__"} */`,
sharedPath,
helpers,
name,

@ -74,9 +74,7 @@ const preprocessors = {
) => {
cannotUseInnerHTML(node);
node.var = block.getUniqueName('text');
const dependencies = block.findDependencies(node.expression);
block.addDependencies(dependencies);
block.addDependencies(node.metadata.dependencies);
},
RawMustacheTag: (
@ -90,9 +88,7 @@ const preprocessors = {
) => {
cannotUseInnerHTML(node);
node.var = block.getUniqueName('raw');
const dependencies = block.findDependencies(node.expression);
block.addDependencies(dependencies);
block.addDependencies(node.metadata.dependencies);
},
Text: (
@ -133,8 +129,7 @@ const preprocessors = {
function attachBlocks(node: Node) {
node.var = block.getUniqueName(`if_block`);
const dependencies = block.findDependencies(node.expression);
block.addDependencies(dependencies);
block.addDependencies(node.metadata.dependencies);
node._block = block.child({
comment: createDebuggingComment(node, generator),
@ -209,7 +204,7 @@ const preprocessors = {
cannotUseInnerHTML(node);
node.var = block.getUniqueName(`each`);
const dependencies = block.findDependencies(node.expression);
const { dependencies } = node.metadata;
block.addDependencies(dependencies);
const indexNames = new Map(block.indexNames);
@ -235,24 +230,18 @@ const preprocessors = {
const changeableIndexes = new Map(block.changeableIndexes);
if (node.index) changeableIndexes.set(node.index, node.key);
const contextDependencies = new Map(block.contextDependencies);
contextDependencies.set(node.context, dependencies);
if (node.destructuredContexts) {
for (const i = 0; i < node.destructuredContexts.length; i++) {
for (let i = 0; i < node.destructuredContexts.length; i += 1) {
contexts.set(node.destructuredContexts[i], `${context}[${i}]`);
contextDependencies.set(node.destructuredContexts[i], dependencies);
}
}
node._block = block.child({
comment: createDebuggingComment(node, generator),
name: generator.getUniqueName('create_each_block'),
expression: node.expression,
context: node.context,
key: node.key,
contextDependencies,
contexts,
indexes,
changeableIndexes,
@ -319,7 +308,7 @@ const preprocessors = {
if (chunk.type !== 'Text') {
if (node.parent) cannotUseInnerHTML(node.parent);
const dependencies = block.findDependencies(chunk.expression);
const dependencies = chunk.metadata.dependencies;
block.addDependencies(dependencies);
// special case — <option value='{{foo}}'> — see below
@ -341,12 +330,10 @@ const preprocessors = {
if (attribute.type === 'EventHandler' && attribute.expression) {
attribute.expression.arguments.forEach((arg: Node) => {
const dependencies = block.findDependencies(arg);
block.addDependencies(dependencies);
block.addDependencies(arg.metadata.dependencies);
});
} else if (attribute.type === 'Binding') {
const dependencies = block.findDependencies(attribute.value);
block.addDependencies(dependencies);
block.addDependencies(attribute.metadata.dependencies);
} else if (attribute.type === 'Transition') {
if (attribute.intro)
generator.hasIntroTransitions = block.hasIntroMethod = true;
@ -358,6 +345,19 @@ const preprocessors = {
}
});
const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value');
// Treat these the same way:
// <option>{{foo}}</option>
// <option value='{{foo}}'>{{foo}}</option>
if (node.name === 'option' && !valueAttribute) {
node.attributes.push({
type: 'Attribute',
name: 'value',
value: node.children
});
}
// special case — in a case like this...
//
// <select bind:value='foo'>
@ -369,12 +369,10 @@ const preprocessors = {
// so that if `foo.qux` changes, we know that we need to
// mark `bar` and `baz` as dirty too
if (node.name === 'select') {
const value = node.attributes.find(
(attribute: Node) => attribute.name === 'value'
);
if (value) {
const binding = node.attributes.find((node: Node) => node.type === 'Binding' && node.name === 'value');
if (binding) {
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
const dependencies = block.findDependencies(value.value);
const dependencies = binding.metadata.dependencies;
state.selectBindingDependencies = dependencies;
dependencies.forEach((prop: string) => {
generator.indirectDependencies.set(prop, new Set());
@ -412,7 +410,6 @@ const preprocessors = {
);
node._state = getChildState(state, {
isTopLevel: false,
parentNode: node.var,
parentNodes: block.getUniqueName(`${node.var}_nodes`),
parentNodeName: node.name,
@ -520,7 +517,6 @@ export default function preprocess(
contexts: new Map(),
indexes: new Map(),
changeableIndexes: new Map(),
contextDependencies: new Map(),
params: ['state'],
indexNames: new Map(),

@ -7,21 +7,10 @@ import getTailSnippet from '../../../utils/getTailSnippet';
import getObject from '../../../utils/getObject';
import getExpressionPrecedence from '../../../utils/getExpressionPrecedence';
import { stringify } from '../../../utils/stringify';
import stringifyProps from '../../../utils/stringifyProps';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
function stringifyProps(props: string[]) {
if (!props.length) return '{}';
const joined = props.join(', ');
if (joined.length > 40) {
// make larger data objects readable
return `{\n\t${props.join(',\n\t')}\n}`;
}
return `{ ${joined} }`;
}
interface Attribute {
name: string;
value: any;
@ -193,7 +182,7 @@ export default function visitComponent(
`);
beforecreate = deindent`
#component._root._beforecreate.push(function () {
#component._root._beforecreate.push(function() {
var state = #component.get(), childState = ${name}.get(), newState = {};
if (!childState) return;
${setParentFromChildOnInit}
@ -210,7 +199,7 @@ export default function visitComponent(
block.builders.update.addBlock(deindent`
var ${name}_changes = {};
${updates.join('\n')}
${name}._set( ${name}_changes );
${name}._set(${name}_changes);
${bindings.length && `${name_updating} = {};`}
`);
}
@ -230,7 +219,7 @@ export default function visitComponent(
block.builders.create.addLine(`${name}._fragment.c();`);
block.builders.claim.addLine(
`${name}._fragment.l( ${state.parentNodes} );`
`${name}._fragment.l(${state.parentNodes});`
);
block.builders.mount.addLine(
@ -363,7 +352,8 @@ function mungeAttribute(attribute: Node, block: Block): Attribute {
}
// simple dynamic attributes
const { dependencies, snippet } = block.contextualise(value.expression);
block.contextualise(value.expression); // TODO remove
const { dependencies, snippet } = value.metadata;
// TODO only update attributes that have changed
return {
@ -384,15 +374,14 @@ function mungeAttribute(attribute: Node, block: Block): Attribute {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { dependencies, snippet } = block.contextualise(
chunk.expression
);
block.contextualise(chunk.expression); // TODO remove
const { dependencies, snippet } = chunk.metadata;
dependencies.forEach(dependency => {
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
return getExpressionPrecedence(chunk.expression) <= 13 ? `( ${snippet} )` : snippet;
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
@ -407,9 +396,8 @@ function mungeAttribute(attribute: Node, block: Block): Attribute {
function mungeBinding(binding: Node, block: Block): Binding {
const { name } = getObject(binding.value);
const { snippet, contexts, dependencies } = block.contextualise(
binding.value
);
const { contexts } = block.contextualise(binding.value);
const { dependencies, snippet } = binding.metadata;
const contextual = block.contexts.has(name);

@ -45,7 +45,8 @@ export default function visitEachBlock(
mountOrIntro,
};
const { snippet } = block.contextualise(node.expression);
block.contextualise(node.expression);
const { snippet } = node.metadata;
block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
@ -362,7 +363,7 @@ function unkeyed(
block: Block,
state: State,
node: Node,
snippet,
snippet: string,
{
create_each_block,
each_block_value,
@ -402,8 +403,8 @@ function unkeyed(
}
`);
const dependencies = block.findDependencies(node.expression);
const allDependencies = new Set(node._block.dependencies);
const { dependencies } = node.metadata;
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});

@ -72,7 +72,9 @@ export default function visitAttribute(
if (attribute.value.length === 1) {
// single {{tag}} — may be a non-string
const { expression } = attribute.value[0];
const { snippet, dependencies, indexes } = block.contextualise(expression);
const { indexes } = block.contextualise(expression);
const { dependencies, snippet } = attribute.value[0].metadata;
value = snippet;
dependencies.forEach(d => {
allDependencies.add(d);
@ -94,7 +96,8 @@ export default function visitAttribute(
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { snippet, dependencies, indexes } = block.contextualise(chunk.expression);
const { indexes } = block.contextualise(chunk.expression);
const { dependencies, snippet } = chunk.metadata;
if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
hasChangeableIndex = true;

@ -1,349 +0,0 @@
import deindent from '../../../../utils/deindent';
import flattenReference from '../../../../utils/flattenReference';
import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
import getObject from '../../../../utils/getObject';
import getTailSnippet from '../../../../utils/getTailSnippet';
const readOnlyMediaAttributes = new Set([
'duration',
'buffered',
'seekable',
'played'
]);
export default function visitBinding(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node
) {
const { name } = getObject(attribute.value);
const { snippet, contexts, dependencies } = block.contextualise(
attribute.value
);
contexts.forEach(context => {
if (!~state.allUsedContexts.indexOf(context))
state.allUsedContexts.push(context);
});
const eventNames = getBindingEventName(node, attribute);
const handler = block.getUniqueName(
`${state.parentNode}_${eventNames.join('_')}_handler`
);
const isMultipleSelect =
node.name === 'select' &&
node.attributes.find(
(attr: Node) => attr.name.toLowerCase() === 'multiple'
); // TODO use getStaticAttributeValue
const type = getStaticAttributeValue(node, 'type');
const bindingGroup = attribute.name === 'group'
? getBindingGroup(generator, attribute.value)
: null;
const isMediaElement = node.name === 'audio' || node.name === 'video';
const isReadOnly = isMediaElement && readOnlyMediaAttributes.has(attribute.name)
const value = getBindingValue(
generator,
block,
state,
node,
attribute,
isMultipleSelect,
isMediaElement,
bindingGroup,
type
);
let setter = getSetter(generator, block, name, snippet, state.parentNode, attribute, dependencies, value);
let updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`;
const needsLock = !isReadOnly && node.name !== 'input' || !/radio|checkbox|range|color/.test(type); // TODO others?
const lock = `#${state.parentNode}_updating`;
let updateConditions = needsLock ? [`!${lock}`] : [];
if (needsLock) block.addVariable(lock, 'false');
// <select> special case
if (node.name === 'select') {
if (!isMultipleSelect) {
setter = `var selectedOption = ${state.parentNode}.querySelector(':checked') || ${state.parentNode}.options[0];\n${setter}`;
}
const value = block.getUniqueName('value');
const option = block.getUniqueName('option');
const ifStatement = isMultipleSelect
? deindent`
${option}.selected = ~${value}.indexOf(${option}.__value);`
: deindent`
if (${option}.__value === ${value}) {
${option}.selected = true;
break;
}`;
const { name } = getObject(attribute.value);
const tailSnippet = getTailSnippet(attribute.value);
updateElement = deindent`
var ${value} = ${snippet};
for (var #i = 0; #i < ${state.parentNode}.options.length; #i += 1) {
var ${option} = ${state.parentNode}.options[#i];
${ifStatement}
}
`;
generator.hasComplexBindings = true;
block.builders.hydrate.addBlock(
`if (!('${name}' in state)) #component._root._beforecreate.push(${handler});`
);
} else if (attribute.name === 'group') {
// <input type='checkbox|radio' bind:group='selected'> special case
if (type === 'radio') {
setter = deindent`
if (!${state.parentNode}.checked) return;
${setter}
`;
}
const condition = type === 'checkbox'
? `~${snippet}.indexOf(${state.parentNode}.__value)`
: `${state.parentNode}.__value === ${snippet}`;
block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${state.parentNode});`
);
block.builders.destroy.addBlock(
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${state.parentNode}), 1);`
);
updateElement = `${state.parentNode}.checked = ${condition};`;
} else if (isMediaElement) {
generator.hasComplexBindings = true;
block.builders.hydrate.addBlock(`#component._root._beforecreate.push(${handler});`);
if (attribute.name === 'currentTime') {
const frame = block.getUniqueName(`${state.parentNode}_animationframe`);
block.addVariable(frame);
setter = deindent`
cancelAnimationFrame(${frame});
if (!${state.parentNode}.paused) ${frame} = requestAnimationFrame(${handler});
${setter}
`;
updateConditions.push(`!isNaN(${snippet})`);
} else if (attribute.name === 'paused') {
// this is necessary to prevent the audio restarting by itself
const last = block.getUniqueName(`${state.parentNode}_paused_value`);
block.addVariable(last, 'true');
updateConditions = [`${last} !== (${last} = ${snippet})`];
updateElement = `${state.parentNode}[${last} ? "pause" : "play"]();`;
}
}
block.builders.init.addBlock(deindent`
function ${handler}() {
${needsLock && `${lock} = true;`}
${setter}
${needsLock && `${lock} = false;`}
}
`);
if (node.name === 'input' && type === 'range') {
// need to bind to `input` and `change`, for the benefit of IE
block.builders.hydrate.addBlock(deindent`
@addListener(${state.parentNode}, "input", ${handler});
@addListener(${state.parentNode}, "change", ${handler});
`);
block.builders.destroy.addBlock(deindent`
@removeListener(${state.parentNode}, "input", ${handler});
@removeListener(${state.parentNode}, "change", ${handler});
`);
} else {
eventNames.forEach(eventName => {
block.builders.hydrate.addLine(
`@addListener(${state.parentNode}, "${eventName}", ${handler});`
);
block.builders.destroy.addLine(
`@removeListener(${state.parentNode}, "${eventName}", ${handler});`
);
});
}
if (!isMediaElement) {
node.initialUpdate = updateElement;
node.initialUpdateNeedsStateObject = !block.contexts.has(name);
}
if (!isReadOnly) { // audio/video duration is read-only, it never updates
if (updateConditions.length) {
block.builders.update.addBlock(deindent`
if (${updateConditions.join(' && ')}) {
${updateElement}
}
`);
} else {
block.builders.update.addBlock(deindent`
${updateElement}
`);
}
}
if (attribute.name === 'paused') {
block.builders.create.addLine(
`@addListener(${state.parentNode}, "play", ${handler});`
);
block.builders.destroy.addLine(
`@removeListener(${state.parentNode}, "play", ${handler});`
);
}
}
function getBindingEventName(node: Node, attribute: Node) {
if (node.name === 'input') {
const typeAttribute = node.attributes.find(
(attr: Node) => attr.type === 'Attribute' && attr.name === 'type'
);
const type = typeAttribute ? typeAttribute.value[0].data : 'text'; // TODO in validation, should throw if type attribute is not static
return [type === 'checkbox' || type === 'radio' ? 'change' : 'input'];
}
if (node.name === 'textarea') return ['input'];
if (attribute.name === 'currentTime') return ['timeupdate'];
if (attribute.name === 'duration') return ['durationchange'];
if (attribute.name === 'paused') return ['pause'];
if (attribute.name === 'buffered') return ['progress', 'loadedmetadata'];
if (attribute.name === 'seekable') return ['loadedmetadata'];
if (attribute.name === 'played') return ['timeupdate'];
return ['change'];
}
function getBindingValue(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node,
isMultipleSelect: boolean,
isMediaElement: boolean,
bindingGroup: number,
type: string
) {
// <select multiple bind:value='selected>
if (isMultipleSelect) {
return `[].map.call(${state.parentNode}.querySelectorAll(':checked'), function(option) { return option.__value; })`;
}
// <select bind:value='selected>
if (node.name === 'select') {
return 'selectedOption && selectedOption.__value';
}
// <input type='checkbox' bind:group='foo'>
if (attribute.name === 'group') {
if (type === 'checkbox') {
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
}
return `${state.parentNode}.__value`;
}
// <input type='range|number' bind:value>
if (type === 'range' || type === 'number') {
return `@toNumber(${state.parentNode}.${attribute.name})`;
}
if (isMediaElement && (attribute.name === 'buffered' || attribute.name === 'seekable' || attribute.name === 'played')) {
return `@timeRangesToArray(${state.parentNode}.${attribute.name})`
}
// everything else
return `${state.parentNode}.${attribute.name}`;
}
function getBindingGroup(generator: DomGenerator, value: Node) {
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
const keypath = parts.join('.');
// TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context
let index = generator.bindingGroups.indexOf(keypath);
if (index === -1) {
index = generator.bindingGroups.length;
generator.bindingGroups.push(keypath);
}
return index;
}
function getSetter(
generator: DomGenerator,
block: Block,
name: string,
snippet: string,
_this: string,
attribute: Node,
dependencies: string[],
value: string,
) {
const tail = attribute.value.type === 'MemberExpression'
? getTailSnippet(attribute.value)
: '';
if (block.contexts.has(name)) {
const prop = dependencies[0];
const computed = isComputed(attribute.value);
return deindent`
var list = ${_this}._svelte.${block.listNames.get(name)};
var index = ${_this}._svelte.${block.indexNames.get(name)};
${computed && `var state = #component.get();`}
list[index]${tail} = ${value};
${computed
? `#component.set({${dependencies.map((prop: string) => `${prop}: state.${prop}`).join(', ')} });`
: `#component.set({${dependencies.map((prop: string) => `${prop}: #component.get('${prop}')`).join(', ')} });`}
`;
}
if (attribute.value.type === 'MemberExpression') {
// This is a little confusing, and should probably be tidied up
// at some point. It addresses a tricky bug (#893), wherein
// Svelte tries to `set()` a computed property, which throws an
// error in dev mode. a) it's possible that we should be
// replacing computations with *their* dependencies, and b)
// we should probably populate `generator.readonly` sooner so
// that we don't have to do the `.some()` here
dependencies = dependencies.filter(prop => !generator.computations.some(computation => computation.key === prop));
return deindent`
var state = #component.get();
${snippet} = ${value};
#component.set({ ${dependencies.map((prop: string) => `${prop}: state.${prop}`).join(', ')} });
`;
}
return `#component.set({ ${name}: ${value} });`;
}
function isComputed(node: Node) {
while (node.type === 'MemberExpression') {
if (node.computed) return true;
node = node.object;
}
return false;
}

@ -4,9 +4,9 @@ import visitSlot from '../Slot';
import visitComponent from '../Component';
import visitWindow from './meta/Window';
import visitAttribute from './Attribute';
import visitEventHandler from './EventHandler';
import visitBinding from './Binding';
import visitRef from './Ref';
import addBindings from './addBindings';
import flattenReference from '../../../../utils/flattenReference';
import validCalleeObjects from '../../../../utils/validCalleeObjects';
import * as namespaces from '../../../../utils/namespaces';
import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue';
import isVoidElementName from '../../../../utils/isVoidElementName';
@ -18,24 +18,10 @@ import { State } from '../../interfaces';
import reservedNames from '../../../../utils/reservedNames';
import { stringify } from '../../../../utils/stringify';
const meta = {
const meta: Record<string, any> = {
':Window': visitWindow,
};
const order = {
Attribute: 1,
Binding: 2,
EventHandler: 3,
Ref: 4,
};
const visitors = {
Attribute: visitAttribute,
EventHandler: visitEventHandler,
Binding: visitBinding,
Ref: visitRef,
};
export default function visitElement(
generator: DomGenerator,
block: Block,
@ -69,8 +55,6 @@ export default function visitElement(
`${componentStack[componentStack.length - 1].var}._slotted.${slot.value[0].data}` : // TODO this looks bonkers
state.parentNode;
const isToplevel = !parentNode;
block.addVariable(name);
block.builders.create.addLine(
`${name} = ${getRenderStatement(
@ -93,6 +77,10 @@ export default function visitElement(
);
} else {
block.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`);
// TODO we eventually need to consider what happens to elements
// that belong to the same outgroup as an outroing element...
block.builders.unmount.addLine(`@detachNode(${name});`);
}
// add CSS encapsulation attribute
@ -109,122 +97,191 @@ export default function visitElement(
}
}
function visitAttributesAndAddProps() {
let intro;
let outro;
node.attributes
.sort((a: Node, b: Node) => order[a.type] - order[b.type])
.forEach((attribute: Node) => {
if (attribute.type === 'Transition') {
if (attribute.intro) intro = attribute;
if (attribute.outro) outro = attribute;
return;
}
visitors[attribute.type](generator, block, childState, node, attribute);
if (node.name === 'textarea') {
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
if (node.children.length > 0) {
node.attributes.push({
type: 'Attribute',
name: 'value',
value: node.children,
});
if (intro || outro)
addTransitions(generator, block, childState, node, intro, outro);
node.children = [];
}
}
// insert static children with textContent or innerHTML
if (!childState.namespace && node.canUseInnerHTML && node.children.length > 0) {
if (node.children.length === 1 && node.children[0].type === 'Text') {
block.builders.create.addLine(
`${name}.textContent = ${stringify(node.children[0].data)};`
);
} else {
block.builders.create.addLine(
`${name}.innerHTML = ${stringify(node.children.map(toHTML).join(''))};`
);
}
} else {
node.children.forEach((child: Node) => {
visit(generator, block, childState, child, elementStack.concat(node), componentStack);
});
}
addBindings(generator, block, childState, node);
if (childState.allUsedContexts.length || childState.usesComponent) {
const initialProps: string[] = [];
const updates: string[] = [];
node.attributes.filter((a: Node) => a.type === 'Attribute').forEach((attribute: Node) => {
visitAttribute(generator, block, childState, node, attribute);
});
if (childState.usesComponent) {
initialProps.push(`component: #component`);
}
// event handlers
node.attributes.filter((a: Node) => a.type === 'EventHandler').forEach((attribute: Node) => {
const isCustomEvent = generator.events.has(attribute.name);
const shouldHoist = !isCustomEvent && state.inEachBlock;
childState.allUsedContexts.forEach((contextName: string) => {
if (contextName === 'state') return;
const context = shouldHoist ? null : name;
const usedContexts: string[] = [];
const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get(contextName);
if (attribute.expression) {
generator.addSourcemapLocations(attribute.expression);
initialProps.push(
`${listName}: ${listName},\n${indexName}: ${indexName}`
);
updates.push(
`${name}._svelte.${listName} = ${listName};\n${name}._svelte.${indexName} = ${indexName};`
const flattened = flattenReference(attribute.expression.callee);
if (!validCalleeObjects.has(flattened.name)) {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
if (shouldHoist) childState.usesComponent = true; // this feels a bit hacky but it works!
}
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, context, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
if (!~childState.allUsedContexts.indexOf(context))
childState.allUsedContexts.push(context);
});
});
}
if (initialProps.length) {
block.builders.hydrate.addBlock(deindent`
${name}._svelte = {
${initialProps.join(',\n')}
};
`);
const _this = context || 'this';
const declarations = usedContexts.map(name => {
if (name === 'state') {
if (shouldHoist) childState.usesComponent = true;
return `var state = ${block.alias('component')}.get();`;
}
if (updates.length) {
block.builders.update.addBlock(updates.join('\n'));
}
}
}
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
const contextName = block.contexts.get(name);
if (isToplevel) {
// TODO we eventually need to consider what happens to elements
// that belong to the same outgroup as an outroing element...
block.builders.unmount.addLine(`@detachNode(${name});`);
}
return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
});
if (node.name !== 'select') {
if (node.name === 'textarea') {
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
if (node.children.length > 0) {
node.attributes.push({
type: 'Attribute',
name: 'value',
value: node.children,
// get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise
const handlerName = (shouldHoist ? generator : block).getUniqueName(
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
);
// create the handler body
const handlerBody = deindent`
${childState.usesComponent &&
`var ${block.alias('component')} = ${_this}._svelte.component;`}
${declarations}
${attribute.expression ?
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
`${block.alias('component')}.fire("${attribute.name}", event);`}
`;
if (isCustomEvent) {
block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${attribute.name}.call(#component, ${name}, function(event) {
${handlerBody}
});
`);
node.children = [];
block.builders.destroy.addLine(deindent`
${handlerName}.teardown();
`);
} else {
const handler = deindent`
function ${handlerName}(event) {
${handlerBody}
}
`;
if (shouldHoist) {
generator.blocks.push(handler);
} else {
block.builders.init.addBlock(handler);
}
block.builders.hydrate.addLine(
`@addListener(${name}, "${attribute.name}", ${handlerName});`
);
block.builders.destroy.addLine(
`@removeListener(${name}, "${attribute.name}", ${handlerName});`
);
}
});
// <select> value attributes are an annoying special case — it must be handled
// *after* its children have been updated
visitAttributesAndAddProps();
}
// refs
node.attributes.filter((a: Node) => a.type === 'Ref').forEach((attribute: Node) => {
const ref = `#component.refs.${attribute.name}`;
// special case bound <option> without a value attribute
if (
node.name === 'option' &&
!node.attributes.find(
(attribute: Node) =>
attribute.type === 'Attribute' && attribute.name === 'value'
)
) {
// TODO check it's bound
const statement = `${name}.__value = ${name}.textContent;`;
node.initialUpdate = node.lateUpdate = statement;
}
block.builders.mount.addLine(
`${ref} = ${name};`
);
if (!childState.namespace && node.canUseInnerHTML && node.children.length > 0) {
if (node.children.length === 1 && node.children[0].type === 'Text') {
block.builders.create.addLine(
`${name}.textContent = ${stringify(node.children[0].data)};`
block.builders.destroy.addLine(
`if (${ref} === ${name}) ${ref} = null;`
);
generator.usesRefs = true; // so component.refs object is created
});
addTransitions(generator, block, childState, node);
if (childState.allUsedContexts.length || childState.usesComponent) {
const initialProps: string[] = [];
const updates: string[] = [];
if (childState.usesComponent) {
initialProps.push(`component: #component`);
}
childState.allUsedContexts.forEach((contextName: string) => {
if (contextName === 'state') return;
const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get(contextName);
initialProps.push(
`${listName}: ${listName},\n${indexName}: ${indexName}`
);
} else {
block.builders.create.addLine(
`${name}.innerHTML = ${stringify(node.children.map(toHTML).join(''))};`
updates.push(
`${name}._svelte.${listName} = ${listName};\n${name}._svelte.${indexName} = ${indexName};`
);
}
} else {
node.children.forEach((child: Node) => {
visit(generator, block, childState, child, elementStack.concat(node), componentStack);
});
}
if (node.lateUpdate) {
block.builders.update.addLine(node.lateUpdate);
}
if (initialProps.length) {
block.builders.hydrate.addBlock(deindent`
${name}._svelte = {
${initialProps.join(',\n')}
};
`);
}
if (node.name === 'select') {
visitAttributesAndAddProps();
if (updates.length) {
block.builders.update.addBlock(updates.join('\n'));
}
}
if (node.initialUpdate) {

@ -1,111 +0,0 @@
import deindent from '../../../../utils/deindent';
import flattenReference from '../../../../utils/flattenReference';
import validCalleeObjects from '../../../../utils/validCalleeObjects';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function visitEventHandler(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node
) {
const name = attribute.name;
const isCustomEvent = generator.events.has(name);
const shouldHoist = !isCustomEvent && state.inEachBlock;
const context = shouldHoist ? null : state.parentNode;
const usedContexts: string[] = [];
if (attribute.expression) {
generator.addSourcemapLocations(attribute.expression);
const flattened = flattenReference(attribute.expression.callee);
if (!validCalleeObjects.has(flattened.name)) {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
if (shouldHoist) state.usesComponent = true; // this feels a bit hacky but it works!
}
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, context, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
if (!~state.allUsedContexts.indexOf(context))
state.allUsedContexts.push(context);
});
});
}
const _this = context || 'this';
const declarations = usedContexts.map(name => {
if (name === 'state') {
if (shouldHoist) state.usesComponent = true;
return `var state = ${block.alias('component')}.get();`;
}
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
const contextName = block.contexts.get(name);
return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
});
// get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise
const handlerName = (shouldHoist ? generator : block).getUniqueName(
`${name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
);
// create the handler body
const handlerBody = deindent`
${state.usesComponent &&
`var ${block.alias('component')} = ${_this}._svelte.component;`}
${declarations}
${attribute.expression ?
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
`${block.alias('component')}.fire("${attribute.name}", event);`}
`;
if (isCustomEvent) {
block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${name}.call(#component, ${state.parentNode}, function(event) {
${handlerBody}
});
`);
block.builders.destroy.addLine(deindent`
${handlerName}.teardown();
`);
} else {
const handler = deindent`
function ${handlerName}(event) {
${handlerBody}
}
`;
if (shouldHoist) {
generator.blocks.push(handler);
} else {
block.builders.init.addBlock(handler);
}
block.builders.hydrate.addLine(
`@addListener(${state.parentNode}, "${name}", ${handlerName});`
);
block.builders.destroy.addLine(
`@removeListener(${state.parentNode}, "${name}", ${handlerName});`
);
}
}

@ -1,25 +0,0 @@
import deindent from '../../../../utils/deindent';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function visitRef(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node
) {
const name = attribute.name;
block.builders.mount.addLine(
`#component.refs.${name} = ${state.parentNode};`
);
block.builders.destroy.addLine(deindent`
if (#component.refs.${name} === ${state.parentNode}) #component.refs.${name} = null;
`);
generator.usesRefs = true; // so this component.refs object is created
}

@ -36,7 +36,8 @@ export default function visitStyleAttribute(
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { snippet, dependencies, indexes } = block.contextualise(chunk.expression);
const { indexes } = block.contextualise(chunk.expression);
const { dependencies, snippet } = chunk.metadata;
if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
hasChangeableIndex = true;

@ -0,0 +1,396 @@
import deindent from '../../../../utils/deindent';
import flattenReference from '../../../../utils/flattenReference';
import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
import getObject from '../../../../utils/getObject';
import getTailSnippet from '../../../../utils/getTailSnippet';
import stringifyProps from '../../../../utils/stringifyProps';
import { generateRule } from '../../../../shared/index';
import flatten from '../../../../utils/flattenReference';
interface Binding {
name: string;
}
const readOnlyMediaAttributes = new Set([
'duration',
'buffered',
'seekable',
'played'
]);
function isMediaNode(name: string) {
return name === 'audio' || name === 'video';
}
const events = [
{
eventNames: ['input'],
filter: (node: Node, binding: Binding) =>
node.name === 'textarea' ||
node.name === 'input' && !/radio|checkbox/.test(getStaticAttributeValue(node, 'type'))
},
{
eventNames: ['change'],
filter: (node: Node, binding: Binding) =>
node.name === 'select' ||
node.name === 'input' && /radio|checkbox|range/.test(getStaticAttributeValue(node, 'type'))
},
// media events
{
eventNames: ['timeupdate'],
filter: (node: Node, binding: Binding) =>
isMediaNode(node.name) &&
(binding.name === 'currentTime' || binding.name === 'played')
},
{
eventNames: ['durationchange'],
filter: (node: Node, binding: Binding) =>
isMediaNode(node.name) &&
binding.name === 'duration'
},
{
eventNames: ['play', 'pause'],
filter: (node: Node, binding: Binding) =>
isMediaNode(node.name) &&
binding.name === 'paused'
},
{
eventNames: ['progress'],
filter: (node: Node, binding: Binding) =>
isMediaNode(node.name) &&
binding.name === 'buffered'
},
{
eventNames: ['loadedmetadata'],
filter: (node: Node, binding: Binding) =>
isMediaNode(node.name) &&
(binding.name === 'buffered' || binding.name === 'seekable')
}
];
export default function addBindings(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
const bindings: Node[] = node.attributes.filter((a: Node) => a.type === 'Binding');
if (bindings.length === 0) return;
if (node.name === 'select' || isMediaNode(node.name)) generator.hasComplexBindings = true;
const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(getStaticAttributeValue(node, 'type'));
const mungedBindings = bindings.map(binding => {
const isReadOnly = isMediaNode(node.name) && readOnlyMediaAttributes.has(binding.name);
let updateCondition: string;
const { name } = getObject(binding.value);
const { contexts } = block.contextualise(binding.value);
const { snippet } = binding.metadata;
// 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 = binding.metadata.dependencies.slice();
binding.metadata.dependencies.forEach((prop: string) => {
const indirectDependencies = generator.indirectDependencies.get(prop);
if (indirectDependencies) {
indirectDependencies.forEach(indirectDependency => {
if (!~dependencies.indexOf(indirectDependency)) dependencies.push(indirectDependency);
});
}
});
contexts.forEach(context => {
if (!~state.allUsedContexts.indexOf(context))
state.allUsedContexts.push(context);
});
// view to model
const valueFromDom = getValueFromDom(generator, node, binding);
const handler = getEventHandler(generator, block, name, snippet, binding, dependencies, valueFromDom);
// model to view
let updateDom = getDomUpdater(node, binding, snippet);
let initialUpdate = updateDom;
// special cases
if (binding.name === 'group') {
const bindingGroup = getBindingGroup(generator, binding.value);
block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
);
block.builders.destroy.addLine(
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${node.var}), 1);`
);
}
if (binding.name === 'currentTime') {
updateCondition = `!isNaN(${snippet})`;
initialUpdate = null;
}
if (binding.name === 'paused') {
// this is necessary to prevent audio restarting by itself
const last = block.getUniqueName(`${node.var}_is_paused`);
block.addVariable(last, 'true');
updateCondition = `${last} !== (${last} = ${snippet})`;
updateDom = `${node.var}[${last} ? "pause" : "play"]();`;
initialUpdate = null;
}
return {
name: binding.name,
object: name,
handler,
updateDom,
initialUpdate,
needsLock: !isReadOnly && needsLock,
updateCondition
};
});
const lock = mungedBindings.some(binding => binding.needsLock) ?
block.getUniqueName(`${node.var}_updating`) :
null;
if (lock) block.addVariable(lock, 'false');
const groups = events
.map(event => {
return {
events: event.eventNames,
bindings: mungedBindings.filter(binding => event.filter(node, binding))
};
})
.filter(group => group.bindings.length);
groups.forEach(group => {
const handler = block.getUniqueName(`${node.var}_${group.events.join('_')}_handler`);
const needsLock = group.bindings.some(binding => binding.needsLock);
group.bindings.forEach(binding => {
if (!binding.updateDom) return;
const updateConditions = needsLock ? [`!${lock}`] : [];
if (binding.updateCondition) updateConditions.push(binding.updateCondition);
block.builders.update.addLine(
updateConditions.length ? `if (${updateConditions.join(' && ')}) ${binding.updateDom}` : binding.updateDom
);
});
const usesContext = group.bindings.some(binding => binding.handler.usesContext);
const usesState = group.bindings.some(binding => binding.handler.usesState);
const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');
const props = new Set();
group.bindings.forEach(binding => {
binding.handler.props.forEach(prop => {
props.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
let animation_frame;
if (group.events[0] === 'timeupdate') {
animation_frame = block.getUniqueName(`${node.var}_animationframe`);
block.addVariable(animation_frame);
}
block.builders.init.addBlock(deindent`
function ${handler}() {
${
animation_frame && deindent`
cancelAnimationFrame(${animation_frame});
if (!${node.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
}
${usesContext && `var context = ${node.var}._svelte;`}
${usesState && `var state = #component.get();`}
${needsLock && `${lock} = true;`}
${mutations.length > 0 && mutations}
#component.set({ ${Array.from(props).join(', ')} });
${needsLock && `${lock} = false;`}
}
`);
group.events.forEach(name => {
block.builders.hydrate.addLine(
`@addListener(${node.var}, "${name}", ${handler});`
);
block.builders.destroy.addLine(
`@removeListener(${node.var}, "${name}", ${handler});`
);
});
const allInitialStateIsDefined = group.bindings
.map(binding => `'${binding.object}' in state`)
.join(' && ');
if (node.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || readOnlyMediaAttributes.has(binding.name))) {
generator.hasComplexBindings = true;
block.builders.hydrate.addLine(
`if (!(${allInitialStateIsDefined})) #component._root._beforecreate.push(${handler});`
);
}
});
node.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
}
function getDomUpdater(
node: Node,
binding: Node,
snippet: string
) {
if (readOnlyMediaAttributes.has(binding.name)) {
return null;
}
if (node.name === 'select') {
return getStaticAttributeValue(node, 'multiple') === true ?
`@selectOptions(${node.var}, ${snippet})` :
`@selectOption(${node.var}, ${snippet})`;
}
if (binding.name === 'group') {
const type = getStaticAttributeValue(node, 'type');
const condition = type === 'checkbox'
? `~${snippet}.indexOf(${node.var}.__value)`
: `${node.var}.__value === ${snippet}`;
return `${node.var}.checked = ${condition};`
}
return `${node.var}.${binding.name} = ${snippet};`;
}
function getBindingGroup(generator: DomGenerator, value: Node) {
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
const keypath = parts.join('.');
// TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context
let index = generator.bindingGroups.indexOf(keypath);
if (index === -1) {
index = generator.bindingGroups.length;
generator.bindingGroups.push(keypath);
}
return index;
}
function getEventHandler(
generator: DomGenerator,
block: Block,
name: string,
snippet: string,
attribute: Node,
dependencies: string[],
value: string,
) {
if (block.contexts.has(name)) {
const tail = attribute.value.type === 'MemberExpression'
? getTailSnippet(attribute.value)
: '';
const list = `context.${block.listNames.get(name)}`;
const index = `context.${block.indexNames.get(name)}`;
return {
usesContext: true,
usesState: true,
mutation: `${list}[${index}]${tail} = ${value};`,
props: dependencies.map(prop => `${prop}: state.${prop}`)
};
}
if (attribute.value.type === 'MemberExpression') {
// This is a little confusing, and should probably be tidied up
// at some point. It addresses a tricky bug (#893), wherein
// Svelte tries to `set()` a computed property, which throws an
// error in dev mode. a) it's possible that we should be
// replacing computations with *their* dependencies, and b)
// we should probably populate `generator.readonly` sooner so
// that we don't have to do the `.some()` here
dependencies = dependencies.filter(prop => !generator.computations.some(computation => computation.key === prop));
return {
usesContext: false,
usesState: true,
mutation: `${snippet} = ${value}`,
props: dependencies.map((prop: string) => `${prop}: state.${prop}`)
};
}
return {
usesContext: false,
usesState: false,
mutation: null,
props: [`${name}: ${value}`]
};
}
function getValueFromDom(
generator: DomGenerator,
node: Node,
binding: Node
) {
// <select bind:value='selected>
if (node.name === 'select') {
return getStaticAttributeValue(node, 'multiple') === true ?
`@selectMultipleValue(${node.var})` :
`@selectValue(${node.var})`;
}
const type = getStaticAttributeValue(node, 'type');
// <input type='checkbox' bind:group='foo'>
if (binding.name === 'group') {
const bindingGroup = getBindingGroup(generator, binding.value);
if (type === 'checkbox') {
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
}
return `${node.var}.__value`;
}
// <input type='range|number' bind:value>
if (type === 'range' || type === 'number') {
return `@toNumber(${node.var}.${binding.name})`;
}
if ((binding.name === 'buffered' || binding.name === 'seekable' || binding.name === 'played')) {
return `@timeRangesToArray(${node.var}.${binding.name})`
}
// everything else
return `${node.var}.${binding.name}`;
}
function isComputed(node: Node) {
while (node.type === 'MemberExpression') {
if (node.computed) return true;
node = node.object;
}
return false;
}

@ -8,14 +8,20 @@ export default function addTransitions(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
intro,
outro
node: Node
) {
const intro = node.attributes.find((a: Node) => a.type === 'Transition' && a.intro);
const outro = node.attributes.find((a: Node) => a.type === 'Transition' && a.outro);
if (!intro && !outro) return;
if (intro) block.contextualise(intro.expression); // TODO remove all these
if (outro) block.contextualise(outro.expression);
if (intro === outro) {
const name = block.getUniqueName(`${node.var}_transition`);
const snippet = intro.expression
? block.contextualise(intro.expression).snippet
? intro.expression.metadata.snippet
: '{}';
block.addVariable(name);
@ -45,7 +51,7 @@ export default function addTransitions(
if (intro) {
block.addVariable(introName);
const snippet = intro.expression
? block.contextualise(intro.expression).snippet
? intro.expression.metadata.snippet
: '{}';
const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?
@ -70,7 +76,7 @@ export default function addTransitions(
if (outro) {
block.addVariable(outroName);
const snippet = outro.expression
? block.contextualise(outro.expression).snippet
? intro.expression.metadata.snippet
: '{}';
const fn = `%transitions-${outro.name}`;

@ -112,6 +112,7 @@ const lookup = {
'http-equiv': { propertyName: 'httpEquiv', appliesTo: ['meta'] },
icon: { appliesTo: ['command'] },
id: {},
indeterminate: { appliesTo: ['input'] },
ismap: { propertyName: 'isMap', appliesTo: ['img'] },
itemprop: {},
keytype: { appliesTo: ['keygen'] },

@ -28,7 +28,7 @@ export default function visitWindow(
node: Node
) {
const events = {};
const bindings = {};
const bindings: Record<string, string> = {};
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'EventHandler') {
@ -38,7 +38,8 @@ export default function visitWindow(
let usesState = false;
attribute.expression.arguments.forEach((arg: Node) => {
const { dependencies } = block.contextualise(arg, null, true);
block.contextualise(arg, null, true);
const { dependencies } = arg.metadata;
if (dependencies.length) usesState = true;
});
@ -82,10 +83,6 @@ export default function visitWindow(
const associatedEvent = associatedEvents[attribute.name];
if (!associatedEvent) {
throw new Error(`Cannot bind to ${attribute.name} on <:Window>`);
}
if (!events[associatedEvent]) events[associatedEvent] = [];
events[associatedEvent].push(
`${attribute.value.name}: this.${attribute.name}`

@ -24,9 +24,11 @@ function getBranches(
elementStack: Node[],
componentStack: Node[]
) {
block.contextualise(node.expression); // TODO remove
const branches = [
{
condition: block.contextualise(node.expression).snippet,
condition: node.metadata.snippet,
block: node._block.name,
hasUpdateMethod: node._block.hasUpdateMethod,
hasIntroMethod: node._block.hasIntroMethod,
@ -143,7 +145,6 @@ function simple(
var ${name} = (${branch.condition}) && ${branch.block}(${params}, #component);
`);
const isTopLevel = !state.parentNode;
const mountOrIntro = branch.hasIntroMethod ? 'i' : 'm';
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
@ -246,7 +247,6 @@ function compound(
var ${name} = ${current_block_type_and}${current_block_type}(${params}, #component);
`);
const isTopLevel = !state.parentNode;
const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm';
const targetNode = state.parentNode || '#target';
@ -345,7 +345,6 @@ function compoundWithOutros(
`);
}
const isTopLevel = !state.parentNode;
const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm';
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';

@ -12,7 +12,8 @@ export default function visitTag(
name: string,
update: (value: string) => string
) {
const { dependencies, indexes, snippet } = block.contextualise(node.expression);
const { indexes } = block.contextualise(node.expression);
const { dependencies, snippet } = node.metadata;
const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
@ -30,7 +31,7 @@ export default function visitTag(
if (dependencies.length || hasChangeableIndex) {
const changedCheck = (
(block.hasOutroMethod ? `#outroing || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')
);
const updateCachedValue = `${value} !== (${value} = ${snippet})`;

@ -5,6 +5,8 @@ import Block from './Block';
import preprocess from './preprocess';
import visit from './visit';
import { removeNode, removeObjectKey } from '../../utils/removeNode';
import getName from '../../utils/getName';
import globalWhitelist from '../../utils/globalWhitelist';
import { Parsed, Node, CompileOptions } from '../../interfaces';
import { AppendTarget } from './interfaces';
import { stringify } from '../../utils/stringify';
@ -70,6 +72,22 @@ export default function ssr(
{ css: null, cssMap: null } :
generator.stylesheet.render(options.filename, true);
// generate initial state object
// TODO this doesn't work, because expectedProperties isn't populated
const globals = Array.from(generator.expectedProperties).filter(prop => globalWhitelist.has(prop));
const initialState = [];
if (globals.length > 0) {
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
}
if (templateProperties.data) {
initialState.push(`%data()`);
} else if (globals.length === 0) {
initialState.push('{}');
}
initialState.push('state');
const result = deindent`
${generator.javascript}
@ -82,9 +100,7 @@ export default function ssr(
};
${name}.render = function(state, options) {
${templateProperties.data
? `state = Object.assign(%data(), state || {});`
: `state = state || {};`}
state = Object.assign(${initialState.join(', ')});
${computations.map(
({ key, deps }) =>
@ -132,7 +148,7 @@ export default function ssr(
}
${templateProperties.components.value.properties.map((prop: Node) => {
return `addComponent(%components-${prop.key.name});`;
return `addComponent(%components-${getName(prop.key)});`;
})}
`}

@ -5,12 +5,6 @@ import { Node } from '../../interfaces';
function noop () {}
function isElseIf(node: Node) {
return (
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
);
}
const preprocessors = {
MustacheTag: noop,
RawMustacheTag: noop,
@ -21,21 +15,15 @@ const preprocessors = {
node: Node,
elementStack: Node[]
) => {
function attachBlocks(node: Node) {
preprocessChildren(generator, node, elementStack);
preprocessChildren(generator, node, elementStack);
if (isElseIf(node.else)) {
attachBlocks(node.else.children[0]);
} else if (node.else) {
preprocessChildren(
generator,
node.else,
elementStack
);
}
if (node.else) {
preprocessChildren(
generator,
node.else,
elementStack
);
}
attachBlocks(node);
},
EachBlock: (
@ -69,6 +57,19 @@ const preprocessors = {
if (slot && isChildOfComponent(node, generator)) {
node.slotted = true;
}
// Treat these the same way:
// <option>{{foo}}</option>
// <option value='{{foo}}'>{{foo}}</option>
const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value');
if (node.name === 'option' && !valueAttribute) {
node.attributes.push({
type: 'Attribute',
name: 'value',
value: node.children
});
}
}
if (node.children.length) {

@ -16,7 +16,8 @@ export default function visitComponent(
function stringifyAttribute(chunk: Node) {
if (chunk.type === 'Text') return chunk.data;
if (chunk.type === 'MustacheTag') {
const { snippet } = block.contextualise(chunk.expression);
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
return '${__escape( ' + snippet + ')}';
}
}
@ -45,7 +46,8 @@ export default function visitComponent(
if (chunk.type === 'Text') {
value = isNaN(chunk.data) ? stringify(chunk.data) : chunk.data;
} else {
const { snippet } = block.contextualise(chunk.expression);
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
value = snippet;
}
} else {

@ -8,7 +8,8 @@ export default function visitEachBlock(
block: Block,
node: Node
) {
const { dependencies, snippet } = block.contextualise(node.expression);
block.contextualise(node.expression);
const { dependencies, snippet } = node.metadata;
const open = `\${ ${node.else ? `${snippet}.length ? ` : ''}${snippet}.map(${node.index ? `(${node.context}, ${node.index})` : node.context} => \``;
generator.append(open);
@ -25,7 +26,7 @@ export default function visitEachBlock(
contextDependencies.set(node.context, dependencies);
if (node.destructuredContexts) {
for (const i = 0; i < node.destructuredContexts.length; i++) {
for (let i = 0; i < node.destructuredContexts.length; i += 1) {
contexts.set(node.destructuredContexts[i], `${node.context}[${i}]`);
contextDependencies.set(node.destructuredContexts[i], dependencies);
}

@ -19,7 +19,8 @@ function stringifyAttributeValue(block: Block, chunks: Node[]) {
return escape(chunk.data).replace(/"/g, '&quot;');
}
const { snippet } = block.contextualise(chunk.expression);
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
return '${' + snippet + '}';
})
.join('');

@ -8,7 +8,8 @@ export default function visitIfBlock(
block: Block,
node: Node
) {
const { snippet } = block.contextualise(node.expression);
block.contextualise(node.expression);
const { snippet } = node.metadata;
generator.append('${ ' + snippet + ' ? `');

@ -7,6 +7,8 @@ export default function visitMustacheTag(
block: Block,
node: Node
) {
const { snippet } = block.contextualise(node.expression);
block.contextualise(node.expression);
const { snippet } = node.metadata;
generator.append('${__escape(' + snippet + ')}');
}

@ -7,6 +7,8 @@ export default function visitRawMustacheTag(
block: Block,
node: Node
) {
const { snippet } = block.contextualise(node.expression);
block.contextualise(node.expression);
const { snippet } = node.metadata;
generator.append('${' + snippet + '}');
}

@ -7,7 +7,4 @@ export default function isChildOfComponent(node: Node, generator: Generator) {
if (generator.components.has(node.name)) return true;
if (/-/.test(node.name)) return false;
}
// TODO do this in validation
throw new Error(`Element with a slot='...' attribute must be a descendant of a component or custom element`);
}

@ -1,4 +1,5 @@
import deindent from '../../../utils/deindent';
import list from '../../../utils/list';
import { CompileOptions, ModuleFormat, Node } from '../../../interfaces';
interface Dependency {
@ -65,7 +66,7 @@ export default function wrapModule(
if (format === 'umd') return umd(code, name, options, banner, dependencies);
if (format === 'eval') return expr(code, name, options, banner, dependencies);
throw new Error(`Not implemented: ${format}`);
throw new Error(`options.format is invalid (must be ${list(Object.keys(wrappers))})`);
}
function es(

@ -3,10 +3,11 @@ import validate from './validate/index';
import generate from './generators/dom/index';
import generateSSR from './generators/server-side-rendering/index';
import { assign } from './shared/index.js';
import { version } from '../package.json';
import Stylesheet from './css/Stylesheet';
import { Parsed, CompileOptions, Warning } from './interfaces';
const version = '__VERSION__';
function normalizeOptions(options: CompileOptions): CompileOptions {
let normalizedOptions = assign({ generate: 'dom' }, options);
const { onwarn, onerror } = normalizedOptions;

@ -65,7 +65,7 @@ export interface GenerateOptions {
name: string;
format: ModuleFormat;
banner?: string;
sharedPath?: string | boolean;
sharedPath?: string;
helpers?: { name: string, alias: string }[];
}

@ -23,6 +23,8 @@ interface ParserOptions {
filename?: string;
}
type ParserState = (parser: Parser) => (ParserState | void);
export class Parser {
readonly template: string;
readonly filename?: string;
@ -59,7 +61,7 @@ export class Parser {
this.stack.push(this.html);
let state = fragment;
let state: ParserState = fragment;
while (this.index < this.template.length) {
state = state(this) || fragment;
@ -94,7 +96,7 @@ export class Parser {
return this.stack[this.stack.length - 1];
}
acornError(err: Error) {
acornError(err: any) {
this.error(err.message.replace(/ \(\d+:\d+\)$/, ''), err.pos);
}

@ -149,3 +149,32 @@ export function setInputType(input, type) {
export function setStyle(node, key, value) {
node.style.setProperty(key, value);
}
export function selectOption(select, value) {
for (var i = 0; i < select.options.length; i += 1) {
var option = select.options[i];
if (option.__value === value) {
option.selected = true;
return;
}
}
}
export function selectOptions(select, value) {
for (var i = 0; i < select.options.length; i += 1) {
var option = select.options[i];
option.selected = ~value.indexOf(option.__value);
}
}
export function selectValue(select) {
var selectedOption = select.querySelector(':checked') || select.options[0];
return selectedOption && selectedOption.__value;
}
export function selectMultipleValue(select) {
return [].map.call(select.querySelectorAll(':checked'), function(option) {
return option.__value;
});
}

@ -38,7 +38,7 @@ export default function annotateWithScopes(expression: Node) {
return scope;
}
class Scope {
export class Scope {
parent: Scope;
block: boolean;
declarations: Set<string>;

@ -0,0 +1,6 @@
import { Node } from '../interfaces';
export default function getMethodName(node: Node) {
if (node.type === 'Identifier') return node.name;
if (node.type === 'Literal') return String(node.value);
}

@ -7,6 +7,7 @@ export default function getStaticAttributeValue(node: Node, name: string) {
if (!attribute) return null;
if (attribute.value === true) return true;
if (attribute.value.length === 0) return '';
if (attribute.value.length === 1 && attribute.value[0].type === 'Text') {

@ -1,4 +1,5 @@
import MagicString from 'magic-string';
import getName from '../utils/getName';
import { Node } from '../interfaces';
const keys = {
@ -51,7 +52,7 @@ export function removeObjectKey(code: MagicString, node: Node, key: string) {
let i = node.properties.length;
while (i--) {
const property = node.properties[i];
if (property.key.type === 'Identifier' && property.key.name === key) {
if (property.key.type === 'Identifier' && getName(property.key) === key) {
removeNode(code, node, property);
}
}

@ -0,0 +1,11 @@
export default function stringifyProps(props: string[]) {
if (!props.length) return '{}';
const joined = props.join(', ');
if (joined.length > 40) {
// make larger data objects readable
return `{\n\t${props.join(',\n\t')}\n}`;
}
return `{ ${joined} }`;
}

@ -1,6 +1,6 @@
import * as namespaces from '../../utils/namespaces';
import validateEventHandler from './validateEventHandler';
import { Validator } from '../index';
import validate, { Validator } from '../index';
import { Node } from '../../interfaces';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|title|tref|tspan|unknown|use|view|vkern)$/;
@ -83,17 +83,17 @@ export default function validateElement(
}
checkTypeAttribute(validator, node);
} else if (name === 'checked') {
} else if (name === 'checked' || name === 'indeterminate') {
if (node.name !== 'input') {
validator.error(
`'checked' is not a valid binding on <${node.name}> elements`,
`'${name}' is not a valid binding on <${node.name}> elements`,
attribute.start
);
}
if (checkTypeAttribute(validator, node) !== 'checkbox') {
validator.error(
`'checked' binding can only be used with <input type="checkbox">`,
`'${name}' binding can only be used with <input type="checkbox">`,
attribute.start
);
}
@ -191,22 +191,7 @@ export default function validateElement(
}
if (attribute.name === 'slot' && !isComponent) {
let i = stack.length;
while (i--) {
const parent = stack[i];
if (parent.type === 'Element' && validator.components.has(parent.name)) break;
if (parent.type === 'IfBlock' || parent.type === 'EachBlock') {
const message = `Cannot place slotted elements inside an ${parent.type === 'IfBlock' ? 'if' : 'each'}-block`;
validator.error(message, attribute.start);
}
}
if (isDynamic(attribute)) {
validator.error(
`slot attribute cannot have a dynamic value`,
attribute.start
);
}
checkSlotAttribute(validator, node, attribute, stack);
}
}
});
@ -232,6 +217,32 @@ function checkTypeAttribute(validator: Validator, node: Node) {
return attribute.value[0].data;
}
function checkSlotAttribute(validator: Validator, node: Node, attribute: Node, stack: Node[]) {
if (isDynamic(attribute)) {
validator.error(
`slot attribute cannot have a dynamic value`,
attribute.start
);
}
let i = stack.length;
while (i--) {
const parent = stack[i];
if (parent.type === 'Element') {
// if we're inside a component or a custom element, gravy
if (validator.components.has(parent.name)) return;
if (/-/.test(parent.name)) return;
}
if (parent.type === 'IfBlock' || parent.type === 'EachBlock') {
const message = `Cannot place slotted elements inside an ${parent.type === 'IfBlock' ? 'if' : 'each'}-block`;
validator.error(message, attribute.start);
}
}
validator.error(`Element with a slot='...' attribute must be a descendant of a component or custom element`, attribute.start);
}
function isDynamic(attribute: Node) {
return attribute.value.length > 1 || attribute.value[0].type !== 'Text';
}

@ -1,5 +1,5 @@
import flattenReference from '../../utils/flattenReference';
import list from '../utils/list';
import list from '../../utils/list';
import { Validator } from '../index';
import validCalleeObjects from '../../utils/validCalleeObjects';
import { Node } from '../../interfaces';

@ -1,6 +1,6 @@
import flattenReference from '../../utils/flattenReference';
import fuzzymatch from '../utils/fuzzymatch';
import list from '../utils/list';
import list from '../../utils/list';
import validateEventHandler from './validateEventHandler';
import { Validator } from '../index';
import { Node } from '../../interfaces';
@ -12,6 +12,7 @@ const validBindings = [
'outerHeight',
'scrollX',
'scrollY',
'online'
];
export default function validateWindow(validator: Validator, node: Node, refs: Map<string, Node[]>, refCallees: Node[]) {

@ -3,6 +3,7 @@ import fuzzymatch from '../utils/fuzzymatch';
import checkForDupes from './utils/checkForDupes';
import checkForComputedKeys from './utils/checkForComputedKeys';
import namespaces from '../../utils/namespaces';
import getName from '../../utils/getName';
import { Validator } from '../';
import { Node } from '../../interfaces';
@ -29,7 +30,7 @@ export default function validateJs(validator: Validator, js: Node) {
const props = validator.properties;
node.declaration.properties.forEach((prop: Node) => {
props.set(prop.key.name, prop);
props.set(getName(prop.key), prop);
});
// Remove these checks in version 2
@ -49,25 +50,26 @@ export default function validateJs(validator: Validator, js: Node) {
// ensure all exported props are valid
node.declaration.properties.forEach((prop: Node) => {
const propValidator = propValidators[prop.key.name];
const name = getName(prop.key);
const propValidator = propValidators[name];
if (propValidator) {
propValidator(validator, prop);
} else {
const match = fuzzymatch(prop.key.name, validPropList);
const match = fuzzymatch(name, validPropList);
if (match) {
validator.error(
`Unexpected property '${prop.key.name}' (did you mean '${match}'?)`,
`Unexpected property '${name}' (did you mean '${match}'?)`,
prop.start
);
} else if (/FunctionExpression/.test(prop.value.type)) {
validator.error(
`Unexpected property '${prop.key.name}' (did you mean to include it in 'methods'?)`,
`Unexpected property '${name}' (did you mean to include it in 'methods'?)`,
prop.start
);
} else {
validator.error(
`Unexpected property '${prop.key.name}'`,
`Unexpected property '${name}'`,
prop.start
);
}
@ -86,7 +88,7 @@ export default function validateJs(validator: Validator, js: Node) {
['components', 'methods', 'helpers', 'transitions'].forEach(key => {
if (validator.properties.has(key)) {
validator.properties.get(key).value.properties.forEach((prop: Node) => {
validator[key].set(prop.key.name, prop.value);
validator[key].set(getName(prop.key), prop.value);
});
}
});

@ -1,5 +1,6 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import getName from '../../../utils/getName';
import { Validator } from '../../';
import { Node } from '../../../interfaces';
@ -9,21 +10,22 @@ export default function components(validator: Validator, prop: Node) {
`The 'components' property must be an object literal`,
prop.start
);
return;
}
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
prop.value.properties.forEach((component: Node) => {
if (component.key.name === 'state') {
const name = getName(component.key);
if (name === 'state') {
validator.error(
`Component constructors cannot be called 'state' due to technical limitations`,
component.start
);
}
if (!/^[A-Z]/.test(component.key.name)) {
if (!/^[A-Z]/.test(name)) {
validator.warn(`Component names should be capitalised`, component.start);
}
});

@ -14,7 +14,6 @@ export default function computed(validator: Validator, prop: Node) {
`The 'computed' property must be an object literal`,
prop.start
);
return;
}
checkForDupes(validator, prop.value.properties);
@ -26,7 +25,6 @@ export default function computed(validator: Validator, prop: Node) {
`Computed properties can be function expressions or arrow function expressions`,
computation.value.start
);
return;
}
const params = computation.value.params;
@ -36,7 +34,6 @@ export default function computed(validator: Validator, prop: Node) {
`A computed value must depend on at least one property`,
computation.value.start
);
return;
}
params.forEach((param: Node) => {

@ -9,7 +9,6 @@ export default function events(validator: Validator, prop: Node) {
`The 'events' property must be an object literal`,
prop.start
);
return;
}
checkForDupes(validator, prop.value.properties);

@ -10,7 +10,6 @@ export default function helpers(validator: Validator, prop: Node) {
`The 'helpers' property must be an object literal`,
prop.start
);
return;
}
checkForDupes(validator, prop.value.properties);

@ -2,6 +2,7 @@ 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 { Validator } from '../../';
import { Node } from '../../../interfaces';
@ -13,7 +14,6 @@ export default function methods(validator: Validator, prop: Node) {
`The 'methods' property must be an object literal`,
prop.start
);
return;
}
checkForAccessors(validator, prop.value.properties, 'Methods');
@ -21,9 +21,11 @@ export default function methods(validator: Validator, prop: Node) {
checkForComputedKeys(validator, prop.value.properties);
prop.value.properties.forEach((prop: Node) => {
if (builtin.has(prop.key.name)) {
const name = getName(prop.key);
if (builtin.has(name)) {
validator.error(
`Cannot overwrite built-in method '${prop.key.name}'`,
`Cannot overwrite built-in method '${name}'`,
prop.start
);
}

@ -9,7 +9,6 @@ export default function transitions(validator: Validator, prop: Node) {
`The 'transitions' property must be an object literal`,
prop.start
);
return;
}
checkForDupes(validator, prop.value.properties);

@ -1,5 +1,6 @@
import { Validator } from '../../';
import { Node } from '../../../interfaces';
import getName from '../../../utils/getName';
export default function checkForDupes(
validator: Validator,
@ -8,10 +9,12 @@ export default function checkForDupes(
const seen = new Set();
properties.forEach(prop => {
if (seen.has(prop.key.name)) {
validator.error(`Duplicate property '${prop.key.name}'`, prop.start);
const name = getName(prop.key);
if (seen.has(name)) {
validator.error(`Duplicate property '${name}'`, prop.start);
}
seen.add(prop.key.name);
seen.add(name);
});
}

@ -157,6 +157,51 @@ describe("formats", () => {
return testIife(code, "Foo", { answer: 42 }, `<div>42</div>`);
});
it('requires options.name', () => {
assert.throws(() => {
svelte.compile('', {
format: 'iife'
});
}, /Missing required 'name' option for IIFE export/);
});
it('suggests using options.globals for default imports', () => {
const warnings = [];
svelte.compile(`
<script>
import _ from 'lodash';
</script>
`,
{
format: 'iife',
name: 'App',
onwarn: warning => {
warnings.push(warning);
}
}
);
assert.deepEqual(warnings, [{
message: `No name was supplied for imported module 'lodash'. Guessing '_', but you should use options.globals`
}]);
});
it('insists on options.globals for named imports', () => {
assert.throws(() => {
svelte.compile(`
<script>
import { fade } from 'svelte-transitions';
</script>
`,
{
format: 'iife',
name: 'App'
}
);
}, /Could not determine name for imported module 'svelte-transitions' use options.globals/);
});
});
describe("umd", () => {
@ -190,6 +235,14 @@ describe("formats", () => {
testCjs(code, { answer: 42 }, `<div>42</div>`);
testIife(code, "Foo", { answer: 42 }, `<div>42</div>`);
});
it('requires options.name', () => {
assert.throws(() => {
svelte.compile('', {
format: 'umd'
});
}, /Missing required 'name' option for UMD export/);
});
});
describe("eval", () => {
@ -218,4 +271,14 @@ describe("formats", () => {
return testEval(code, "Foo", { answer: 42 }, `<div>42</div>`);
});
});
describe('unknown format', () => {
it('throws an error', () => {
assert.throws(() => {
svelte.compile('', {
format: 'nope'
});
}, /options.format is invalid \(must be es, amd, cjs, iife, umd or eval\)/);
});
});
});

@ -8,11 +8,9 @@ import chalk from 'chalk';
// for coverage purposes, we need to test source files,
// but for sanity purposes, we need to test dist files
export function loadSvelte(test) {
if (test) global.__svelte_test = true;
process.env.TEST = test ? 'true' : '';
const resolved = process.env.COVERAGE
? require.resolve('../src/index.js')
: require.resolve('../compiler/svelte.js');
const resolved = require.resolve('../compiler/svelte.js');
delete require.cache[resolved];
return require(resolved);

@ -294,8 +294,8 @@ function create_each_block(state, comments, comment, i, component) {
},
h: function hydrate() {
div.className = "comment";
span.className = "meta";
div.className = "comment";
},
m: function mount(target, anchor) {

@ -95,8 +95,8 @@ function create_each_block(state, comments, comment, i, component) {
},
h: function hydrate() {
div.className = "comment";
span.className = "meta";
div.className = "comment";
},
m: function mount(target, anchor) {

@ -202,8 +202,8 @@ function create_main_fragment(state, component) {
},
h: function hydrate() {
input.type = "checkbox";
addListener(input, "change", input_change_handler);
input.type = "checkbox";
},
m: function mount(target, anchor) {

@ -15,8 +15,8 @@ function create_main_fragment(state, component) {
},
h: function hydrate() {
input.type = "checkbox";
addListener(input, "change", input_change_handler);
input.type = "checkbox";
},
m: function mount(target, anchor) {

@ -197,78 +197,51 @@ var proto = {
/* generated by Svelte vX.Y.Z */
function create_main_fragment(state, component) {
var audio, audio_updating = false, audio_animationframe, audio_paused_value = true;
function audio_progress_loadedmetadata_handler() {
audio_updating = true;
component.set({ buffered: timeRangesToArray(audio.buffered) });
audio_updating = false;
}
function audio_loadedmetadata_handler() {
audio_updating = true;
component.set({ seekable: timeRangesToArray(audio.seekable) });
audio_updating = false;
}
var audio, audio_is_paused = true, audio_updating = false, audio_animationframe;
function audio_timeupdate_handler() {
audio_updating = true;
component.set({ played: timeRangesToArray(audio.played) });
audio_updating = false;
}
function audio_timeupdate_handler_1() {
audio_updating = true;
cancelAnimationFrame(audio_animationframe);
if (!audio.paused) audio_animationframe = requestAnimationFrame(audio_timeupdate_handler_1);
component.set({ currentTime: audio.currentTime });
if (!audio.paused) audio_animationframe = requestAnimationFrame(audio_timeupdate_handler);
audio_updating = true;
component.set({ played: timeRangesToArray(audio.played), currentTime: audio.currentTime });
audio_updating = false;
}
function audio_durationchange_handler() {
audio_updating = true;
component.set({ duration: audio.duration });
audio_updating = false;
}
function audio_pause_handler() {
function audio_play_pause_handler() {
audio_updating = true;
component.set({ paused: audio.paused });
audio_updating = false;
}
function audio_progress_handler() {
component.set({ buffered: timeRangesToArray(audio.buffered) });
}
function audio_loadedmetadata_handler() {
component.set({ buffered: timeRangesToArray(audio.buffered), seekable: timeRangesToArray(audio.seekable) });
}
return {
c: function create() {
audio = createElement("audio");
addListener(audio, "play", audio_pause_handler);
this.h();
},
h: function hydrate() {
component._root._beforecreate.push(audio_progress_loadedmetadata_handler);
addListener(audio, "progress", audio_progress_loadedmetadata_handler);
addListener(audio, "loadedmetadata", audio_progress_loadedmetadata_handler);
component._root._beforecreate.push(audio_loadedmetadata_handler);
addListener(audio, "loadedmetadata", audio_loadedmetadata_handler);
component._root._beforecreate.push(audio_timeupdate_handler);
addListener(audio, "timeupdate", audio_timeupdate_handler);
component._root._beforecreate.push(audio_timeupdate_handler_1);
addListener(audio, "timeupdate", audio_timeupdate_handler_1);
component._root._beforecreate.push(audio_durationchange_handler);
if (!('played' in state && 'currentTime' in state)) component._root._beforecreate.push(audio_timeupdate_handler);
addListener(audio, "durationchange", audio_durationchange_handler);
component._root._beforecreate.push(audio_pause_handler);
addListener(audio, "pause", audio_pause_handler);
if (!('duration' in state)) component._root._beforecreate.push(audio_durationchange_handler);
addListener(audio, "play", audio_play_pause_handler);
addListener(audio, "pause", audio_play_pause_handler);
addListener(audio, "progress", audio_progress_handler);
if (!('buffered' in state)) component._root._beforecreate.push(audio_progress_handler);
addListener(audio, "loadedmetadata", audio_loadedmetadata_handler);
if (!('buffered' in state && 'seekable' in state)) component._root._beforecreate.push(audio_loadedmetadata_handler);
},
m: function mount(target, anchor) {
@ -276,13 +249,8 @@ function create_main_fragment(state, component) {
},
p: function update(changed, state) {
if (!audio_updating && !isNaN(state.currentTime )) {
audio.currentTime = state.currentTime ;
}
if (audio_paused_value !== (audio_paused_value = state.paused)) {
audio[audio_paused_value ? "pause" : "play"]();
}
if (!audio_updating && !isNaN(state.currentTime )) audio.currentTime = state.currentTime ;
if (!audio_updating && audio_is_paused !== (audio_is_paused = state.paused)) audio[audio_is_paused ? "pause" : "play"]();
},
u: function unmount() {
@ -290,14 +258,12 @@ function create_main_fragment(state, component) {
},
d: function destroy$$1() {
removeListener(audio, "progress", audio_progress_loadedmetadata_handler);
removeListener(audio, "loadedmetadata", audio_progress_loadedmetadata_handler);
removeListener(audio, "loadedmetadata", audio_loadedmetadata_handler);
removeListener(audio, "timeupdate", audio_timeupdate_handler);
removeListener(audio, "timeupdate", audio_timeupdate_handler_1);
removeListener(audio, "durationchange", audio_durationchange_handler);
removeListener(audio, "pause", audio_pause_handler);
removeListener(audio, "play", audio_pause_handler);
removeListener(audio, "play", audio_play_pause_handler);
removeListener(audio, "pause", audio_play_pause_handler);
removeListener(audio, "progress", audio_progress_handler);
removeListener(audio, "loadedmetadata", audio_loadedmetadata_handler);
}
};
}

@ -2,78 +2,51 @@
import { addListener, assign, callAll, createElement, detachNode, init, insertNode, proto, removeListener, timeRangesToArray } from "svelte/shared.js";
function create_main_fragment(state, component) {
var audio, audio_updating = false, audio_animationframe, audio_paused_value = true;
function audio_progress_loadedmetadata_handler() {
audio_updating = true;
component.set({ buffered: timeRangesToArray(audio.buffered) });
audio_updating = false;
}
function audio_loadedmetadata_handler() {
audio_updating = true;
component.set({ seekable: timeRangesToArray(audio.seekable) });
audio_updating = false;
}
var audio, audio_is_paused = true, audio_updating = false, audio_animationframe;
function audio_timeupdate_handler() {
audio_updating = true;
component.set({ played: timeRangesToArray(audio.played) });
audio_updating = false;
}
function audio_timeupdate_handler_1() {
audio_updating = true;
cancelAnimationFrame(audio_animationframe);
if (!audio.paused) audio_animationframe = requestAnimationFrame(audio_timeupdate_handler_1);
component.set({ currentTime: audio.currentTime });
if (!audio.paused) audio_animationframe = requestAnimationFrame(audio_timeupdate_handler);
audio_updating = true;
component.set({ played: timeRangesToArray(audio.played), currentTime: audio.currentTime });
audio_updating = false;
}
function audio_durationchange_handler() {
audio_updating = true;
component.set({ duration: audio.duration });
audio_updating = false;
}
function audio_pause_handler() {
function audio_play_pause_handler() {
audio_updating = true;
component.set({ paused: audio.paused });
audio_updating = false;
}
function audio_progress_handler() {
component.set({ buffered: timeRangesToArray(audio.buffered) });
}
function audio_loadedmetadata_handler() {
component.set({ buffered: timeRangesToArray(audio.buffered), seekable: timeRangesToArray(audio.seekable) });
}
return {
c: function create() {
audio = createElement("audio");
addListener(audio, "play", audio_pause_handler);
this.h();
},
h: function hydrate() {
component._root._beforecreate.push(audio_progress_loadedmetadata_handler);
addListener(audio, "progress", audio_progress_loadedmetadata_handler);
addListener(audio, "loadedmetadata", audio_progress_loadedmetadata_handler);
component._root._beforecreate.push(audio_loadedmetadata_handler);
addListener(audio, "loadedmetadata", audio_loadedmetadata_handler);
component._root._beforecreate.push(audio_timeupdate_handler);
addListener(audio, "timeupdate", audio_timeupdate_handler);
component._root._beforecreate.push(audio_timeupdate_handler_1);
addListener(audio, "timeupdate", audio_timeupdate_handler_1);
component._root._beforecreate.push(audio_durationchange_handler);
if (!('played' in state && 'currentTime' in state)) component._root._beforecreate.push(audio_timeupdate_handler);
addListener(audio, "durationchange", audio_durationchange_handler);
component._root._beforecreate.push(audio_pause_handler);
addListener(audio, "pause", audio_pause_handler);
if (!('duration' in state)) component._root._beforecreate.push(audio_durationchange_handler);
addListener(audio, "play", audio_play_pause_handler);
addListener(audio, "pause", audio_play_pause_handler);
addListener(audio, "progress", audio_progress_handler);
if (!('buffered' in state)) component._root._beforecreate.push(audio_progress_handler);
addListener(audio, "loadedmetadata", audio_loadedmetadata_handler);
if (!('buffered' in state && 'seekable' in state)) component._root._beforecreate.push(audio_loadedmetadata_handler);
},
m: function mount(target, anchor) {
@ -81,13 +54,8 @@ function create_main_fragment(state, component) {
},
p: function update(changed, state) {
if (!audio_updating && !isNaN(state.currentTime )) {
audio.currentTime = state.currentTime ;
}
if (audio_paused_value !== (audio_paused_value = state.paused)) {
audio[audio_paused_value ? "pause" : "play"]();
}
if (!audio_updating && !isNaN(state.currentTime )) audio.currentTime = state.currentTime ;
if (!audio_updating && audio_is_paused !== (audio_is_paused = state.paused)) audio[audio_is_paused ? "pause" : "play"]();
},
u: function unmount() {
@ -95,14 +63,12 @@ function create_main_fragment(state, component) {
},
d: function destroy() {
removeListener(audio, "progress", audio_progress_loadedmetadata_handler);
removeListener(audio, "loadedmetadata", audio_progress_loadedmetadata_handler);
removeListener(audio, "loadedmetadata", audio_loadedmetadata_handler);
removeListener(audio, "timeupdate", audio_timeupdate_handler);
removeListener(audio, "timeupdate", audio_timeupdate_handler_1);
removeListener(audio, "durationchange", audio_durationchange_handler);
removeListener(audio, "pause", audio_pause_handler);
removeListener(audio, "play", audio_pause_handler);
removeListener(audio, "play", audio_play_pause_handler);
removeListener(audio, "pause", audio_play_pause_handler);
removeListener(audio, "progress", audio_progress_handler);
removeListener(audio, "loadedmetadata", audio_loadedmetadata_handler);
}
};
}

@ -5,7 +5,7 @@ SvelteComponent.data = function() {
};
SvelteComponent.render = function(state, options) {
state = state || {};
state = Object.assign({}, state);
return ``.trim();
};

@ -7,7 +7,7 @@ SvelteComponent.data = function() {
};
SvelteComponent.render = function(state, options) {
state = state || {};
state = Object.assign({}, state);
return ``.trim();
};

@ -0,0 +1,256 @@
function noop() {}
function assign(target) {
var k,
source,
i = 1,
len = arguments.length;
for (; i < len; i++) {
source = arguments[i];
for (k in source) target[k] = source[k];
}
return target;
}
function appendNode(node, target) {
target.appendChild(node);
}
function insertNode(node, target, anchor) {
target.insertBefore(node, anchor);
}
function detachNode(node) {
node.parentNode.removeChild(node);
}
function createElement(name) {
return document.createElement(name);
}
function createText(data) {
return document.createTextNode(data);
}
function blankObject() {
return Object.create(null);
}
function destroy(detach) {
this.destroy = noop;
this.fire('destroy');
this.set = this.get = noop;
if (detach !== false) this._fragment.u();
this._fragment.d();
this._fragment = this._state = null;
}
function differs(a, b) {
return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
function dispatchObservers(component, group, changed, newState, oldState) {
for (var key in group) {
if (!changed[key]) continue;
var newValue = newState[key];
var oldValue = oldState[key];
var callbacks = group[key];
if (!callbacks) continue;
for (var i = 0; i < callbacks.length; i += 1) {
var callback = callbacks[i];
if (callback.__calling) continue;
callback.__calling = true;
callback.call(component, newValue, oldValue);
callback.__calling = false;
}
}
}
function fire(eventName, data) {
var handlers =
eventName in this._handlers && this._handlers[eventName].slice();
if (!handlers) return;
for (var i = 0; i < handlers.length; i += 1) {
handlers[i].call(this, data);
}
}
function get(key) {
return key ? this._state[key] : this._state;
}
function init(component, options) {
component.options = options;
component._observers = { pre: blankObject(), post: blankObject() };
component._handlers = blankObject();
component._root = options._root || component;
component._bind = options._bind;
}
function observe(key, callback, options) {
var group = options && options.defer
? this._observers.post
: this._observers.pre;
(group[key] || (group[key] = [])).push(callback);
if (!options || options.init !== false) {
callback.__calling = true;
callback.call(this, this._state[key]);
callback.__calling = false;
}
return {
cancel: function() {
var index = group[key].indexOf(callback);
if (~index) group[key].splice(index, 1);
}
};
}
function on(eventName, handler) {
if (eventName === 'teardown') return this.on('destroy', handler);
var handlers = this._handlers[eventName] || (this._handlers[eventName] = []);
handlers.push(handler);
return {
cancel: function() {
var index = handlers.indexOf(handler);
if (~index) handlers.splice(index, 1);
}
};
}
function set(newState) {
this._set(assign({}, newState));
if (this._root._lock) return;
this._root._lock = true;
callAll(this._root._beforecreate);
callAll(this._root._oncreate);
callAll(this._root._aftercreate);
this._root._lock = false;
}
function _set(newState) {
var oldState = this._state,
changed = {},
dirty = false;
for (var key in newState) {
if (differs(newState[key], oldState[key])) changed[key] = dirty = true;
}
if (!dirty) return;
this._state = assign({}, oldState, newState);
this._recompute(changed, this._state);
if (this._bind) this._bind(changed, this._state);
if (this._fragment) {
dispatchObservers(this, this._observers.pre, changed, this._state, oldState);
this._fragment.p(changed, this._state);
dispatchObservers(this, this._observers.post, changed, this._state, oldState);
}
}
function callAll(fns) {
while (fns && fns.length) fns.pop()();
}
function _mount(target, anchor) {
this._fragment.m(target, anchor);
}
function _unmount() {
this._fragment.u();
}
var proto = {
destroy: destroy,
get: get,
fire: fire,
observe: observe,
on: on,
set: set,
teardown: destroy,
_recompute: noop,
_set: _set,
_mount: _mount,
_unmount: _unmount
};
/* generated by Svelte vX.Y.Z */
function create_main_fragment(state, component) {
var window_updating = false, text, p, text_1, text_2;
function onwindowscroll(event) {
window_updating = true;
component.set({
y: this.scrollY
});
window_updating = false;
}
window.addEventListener("scroll", onwindowscroll);
component.observe("y", function(y) {
if (window_updating) return;
window.scrollTo(window.scrollX, y);
});
return {
c: function create() {
text = createText("\n\n");
p = createElement("p");
text_1 = createText("scrolled to ");
text_2 = createText(state.y);
},
m: function mount(target, anchor) {
insertNode(text, target, anchor);
insertNode(p, target, anchor);
appendNode(text_1, p);
appendNode(text_2, p);
},
p: function update(changed, state) {
if (changed.y) {
text_2.data = state.y;
}
},
u: function unmount() {
detachNode(text);
detachNode(p);
},
d: function destroy$$1() {
window.removeEventListener("scroll", onwindowscroll);
}
};
}
function SvelteComponent(options) {
init(this, options);
this._state = assign({}, options.data);
this._state.y = window.scrollY;
this._fragment = create_main_fragment(this._state, this);
if (options.target) {
this._fragment.c();
this._fragment.m(options.target, options.anchor || null);
}
}
assign(SvelteComponent.prototype, proto);
export default SvelteComponent;

@ -0,0 +1,68 @@
/* generated by Svelte vX.Y.Z */
import { appendNode, assign, createElement, createText, detachNode, init, insertNode, proto } from "svelte/shared.js";
function create_main_fragment(state, component) {
var window_updating = false, text, p, text_1, text_2;
function onwindowscroll(event) {
window_updating = true;
component.set({
y: this.scrollY
});
window_updating = false;
};
window.addEventListener("scroll", onwindowscroll);
component.observe("y", function(y) {
if (window_updating) return;
window.scrollTo(window.scrollX, y);
});
return {
c: function create() {
text = createText("\n\n");
p = createElement("p");
text_1 = createText("scrolled to ");
text_2 = createText(state.y);
},
m: function mount(target, anchor) {
insertNode(text, target, anchor);
insertNode(p, target, anchor);
appendNode(text_1, p);
appendNode(text_2, p);
},
p: function update(changed, state) {
if (changed.y) {
text_2.data = state.y;
}
},
u: function unmount() {
detachNode(text);
detachNode(p);
},
d: function destroy() {
window.removeEventListener("scroll", onwindowscroll);
}
};
}
function SvelteComponent(options) {
init(this, options);
this._state = assign({}, options.data);
this._state.y = window.scrollY;
this._fragment = create_main_fragment(this._state, this);
if (options.target) {
this._fragment.c();
this._fragment.m(options.target, options.anchor || null);
}
}
assign(SvelteComponent.prototype, proto);
export default SvelteComponent;

@ -0,0 +1,3 @@
<:Window bind:scrollY=y/>
<p>scrolled to {{y}}</p>

@ -0,0 +1,21 @@
export default {
// This is a bit of a funny one — there's no equivalent attribute,
// so it can't be server-rendered
'skip-ssr': true,
data: {
indeterminate: true
},
html: `
<input type='checkbox'>
`,
test(assert, component, target) {
const input = target.querySelector('input');
assert.ok(input.indeterminate);
component.set({ indeterminate: false });
assert.ok(!input.indeterminate);
}
};

@ -0,0 +1 @@
<input type='checkbox' indeterminate='{{indeterminate}}'>

@ -0,0 +1,42 @@
export default {
'skip-ssr': true,
data: {
indeterminate: true,
},
html: `
<input type="checkbox">
<p>checked? false</p>
<p>indeterminate? true</p>
`,
test(assert, component, target, window) {
const input = target.querySelector('input');
assert.equal(input.checked, false);
assert.equal(input.indeterminate, true);
const event = new window.Event('change');
input.checked = true;
input.indeterminate = false;
input.dispatchEvent(event);
assert.equal(component.get('indeterminate'), false);
assert.equal(component.get('checked'), true);
assert.htmlEqual(target.innerHTML, `
<input type="checkbox">
<p>checked? true</p>
<p>indeterminate? false</p>
`);
component.set({ indeterminate: true });
assert.equal(input.indeterminate, true);
assert.equal(input.checked, true);
assert.htmlEqual(target.innerHTML, `
<input type="checkbox">
<p>checked? true</p>
<p>indeterminate? true</p>
`);
},
};

@ -0,0 +1,3 @@
<input type='checkbox' bind:checked bind:indeterminate>
<p>checked? {{checked}}</p>
<p>indeterminate? {{indeterminate}}</p>

@ -0,0 +1,40 @@
export default {
data: {
values: [1, 2, 3],
foo: 2
},
html: `
<select>
<option value='1'>1</option>
<option value='2'>2</option>
<option value='3'>3</option>
</select>
<p>foo: 2</p>
`,
test(assert, component, target, window) {
const select = target.querySelector('select');
const options = [...target.querySelectorAll('option')];
assert.ok(options[1].selected);
assert.equal(component.get('foo'), 2);
const change = new window.Event('change');
options[2].selected = true;
select.dispatchEvent(change);
assert.equal(component.get('foo'), 3);
assert.htmlEqual( target.innerHTML, `
<select>
<option value='1'>1</option>
<option value='2'>2</option>
<option value='3'>3</option>
</select>
<p>foo: 3</p>
` );
}
};

@ -0,0 +1,7 @@
<select bind:value='foo'>
{{#each values as v}}
<option>{{v}}</option>
{{/each}}
</select>
<p>foo: {{foo}}</p>

@ -5,9 +5,9 @@ export default {
<p>selected: a</p>
<select>
<option>a</option>
<option>b</option>
<option>c</option>
<option value='a'>a</option>
<option value='b'>b</option>
<option value='c'>c</option>
</select>
<p>selected: a</p>

@ -3,9 +3,9 @@ export default {
<p>selected: b</p>
<select>
<option>a</option>
<option>b</option>
<option>c</option>
<option value='a'>a</option>
<option value='b'>b</option>
<option value='c'>c</option>
</select>
<p>selected: b</p>

@ -22,9 +22,9 @@ export default {
assert.htmlEqual( target.innerHTML, `
<select>
<option>one</option>
<option>two</option>
<option>three</option>
<option value='one'>one</option>
<option value='two'>two</option>
<option value='three'>three</option>
</select>
<p>selected: two</p>
` );

@ -3,9 +3,9 @@ export default {
<p>selected: one</p>
<select>
<option>one</option>
<option>two</option>
<option>three</option>
<option value='one'>one</option>
<option value='two'>two</option>
<option value='three'>three</option>
</select>
<p>selected: one</p>
@ -32,9 +32,9 @@ export default {
<p>selected: two</p>
<select>
<option>one</option>
<option>two</option>
<option>three</option>
<option value='one'>one</option>
<option value='two'>two</option>
<option value='three'>three</option>
</select>
<p>selected: two</p>

@ -0,0 +1,61 @@
export default {
html: `
<div class="todo done">
<input type="checkbox">
<input type="text">
</div>
<div class="todo done">
<input type="checkbox">
<input type="text">
</div>
<div class="todo ">
<input type="checkbox">
<input type="text">
</div>
`,
data: {
todos: {
first: {
description: 'Buy some milk',
done: true,
},
second: {
description: 'Do the laundry',
done: true,
},
third: {
description: "Find life's true purpose",
done: false,
},
},
},
test(assert, component, target, window) {
const input = document.querySelectorAll('input[type="checkbox"]')[2];
const change = new window.Event('change');
input.checked = true;
input.dispatchEvent(change);
assert.ok(component.get('todos').third.done);
assert.htmlEqual(target.innerHTML, `
<div class="todo done">
<input type="checkbox">
<input type="text">
</div>
<div class="todo done">
<input type="checkbox">
<input type="text">
</div>
<div class="todo done">
<input type="checkbox">
<input type="text">
</div>
`);
},
};

@ -0,0 +1,6 @@
{{#each Object.keys(todos) as key}}
<div class='todo {{todos[key].done ? "done": ""}}'>
<input type='checkbox' bind:checked='todos[key].done'>
<input type='text' bind:value='todos[key].description'>
</div>
{{/each}}

@ -21,9 +21,7 @@ function tryToReadFile(file) {
describe("ssr", () => {
before(() => {
require(process.env.COVERAGE
? "../../src/server-side-rendering/register.js"
: "../../ssr/register");
require("../../ssr/register");
return setupHtmlEqual();
});

@ -4,6 +4,8 @@ const path = require('path');
require('console-group').install();
require('source-map-support').install();
process.env.TEST = true;
require.extensions['.js'] = function(module, filename) {
const exports = [];

@ -34,9 +34,11 @@ describe("sourcemaps", () => {
cascade: config.cascade
});
const _code = code.replace(/Svelte v\d+\.\d+\.\d+/, match => match.replace(/\d/g, 'x'));
fs.writeFileSync(
`${outputFilename}.js`,
`${code}\n//# sourceMappingURL=output.js.map`
`${_code}\n//# sourceMappingURL=output.js.map`
);
fs.writeFileSync(
`${outputFilename}.js.map`,
@ -62,12 +64,12 @@ describe("sourcemaps", () => {
const locateInSource = getLocator(input);
const smc = new SourceMapConsumer(map);
const locateInGenerated = getLocator(code);
const locateInGenerated = getLocator(_code);
const smcCss = cssMap && new SourceMapConsumer(cssMap);
const locateInGeneratedCss = getLocator(css || '');
test({ assert, code, map, smc, smcCss, locateInSource, locateInGenerated, locateInGeneratedCss });
test({ assert, code: _code, map, smc, smcCss, locateInSource, locateInGenerated, locateInGeneratedCss });
});
});
});

@ -0,0 +1,8 @@
[{
"message": "'type' attribute must be specified",
"loc": {
"line": 1,
"column": 24
},
"pos": 24
}]

@ -0,0 +1,10 @@
<button on:click='foo()'></button>
<script>
export default {
methods: {
'foo': () => {},
'bar': () => {}
}
};
</script>

@ -0,0 +1,8 @@
[{
"message": "Invalid namespace 'lol'",
"pos": 29,
"loc": {
"line": 3,
"column": 2
}
}]

@ -0,0 +1,5 @@
<script>
export default {
namespace: 'lol'
};
</script>

@ -0,0 +1,8 @@
[{
"message": "The 'namespace' property must be a string literal representing a valid namespace",
"pos": 79,
"loc": {
"line": 5,
"column": 2
}
}]

@ -0,0 +1,7 @@
<script>
const namespace = 'http://www.w3.org/1999/svg';
export default {
namespace
};
</script>

@ -0,0 +1,8 @@
[{
"message": "The 'components' property must be an object literal",
"loc": {
"line": 3,
"column": 2
},
"pos": 29
}]

@ -0,0 +1,5 @@
<script>
export default {
components: 'not an object literal'
};
</script>

@ -0,0 +1,8 @@
[{
"message": "The 'events' property must be an object literal",
"loc": {
"line": 3,
"column": 2
},
"pos": 29
}]

@ -0,0 +1,5 @@
<script>
export default {
events: 'not an object literal'
};
</script>

@ -0,0 +1,8 @@
[{
"message": "The 'helpers' property must be an object literal",
"loc": {
"line": 3,
"column": 2
},
"pos": 29
}]

@ -0,0 +1,5 @@
<script>
export default {
helpers: 'not an object literal'
};
</script>

@ -0,0 +1,8 @@
[{
"message": "The 'methods' property must be an object literal",
"loc": {
"line": 3,
"column": 2
},
"pos": 29
}]

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

Loading…
Cancel
Save