Merge branch 'master' into hydration

pull/7738/head
Rich Harris 8 years ago
commit 451ea0068c

@ -33,6 +33,9 @@
"plugin:import/errors",
"plugin:import/warnings"
],
"plugins": [
"html"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"

@ -1,5 +1,15 @@
# Svelte changelog
## 1.22.5
* Fix nested component unmounting bug ([#643](https://github.com/sveltejs/svelte/issues/643))
## 1.22.4
* Include `ast` in `svelte.compile` return value ([#632](https://github.com/sveltejs/svelte/issues/632))
* Set initial value of `<select>` binding, if unspecified ([#639](https://github.com/sveltejs/svelte/issues/639))
* Mark indirect dependencies of `<select>` bindings (i.e. the dependencies of their `<option>` values) ([#639](https://github.com/sveltejs/svelte/issues/639))
## 1.22.3
* Fix nested component unmounting bug ([#625](https://github.com/sveltejs/svelte/issues/625))

@ -62,7 +62,7 @@ const { code, map } = svelte.compile( source, {
The Svelte compiler exposes the following API:
* `compile( source [, options ] ) => { code, map }` - Compile the component with the given options (see below). Returns an object containing the compiled JavaScript and a sourcemap.
* `compile( source [, options ] ) => { code, map, ast, css }` - Compile the component with the given options (see below). Returns an object containing the compiled JavaScript, a sourcemap, an AST and transformed CSS.
* `create( source [, options ] ) => function` - Compile the component and return the component itself.
* `VERSION` - The version of this copy of the Svelte compiler as a string, `'x.x.x'`.

@ -1,6 +1,6 @@
{
"name": "svelte",
"version": "1.22.3",
"version": "1.22.5",
"description": "The magical disappearing UI framework",
"main": "compiler/svelte.js",
"files": [
@ -62,6 +62,7 @@
"console-group": "^0.3.2",
"css-tree": "1.0.0-alpha16",
"eslint": "^3.12.2",
"eslint-plugin-html": "^3.0.0",
"eslint-plugin-import": "^2.2.0",
"estree-walker": "^0.3.0",
"fuzzyset.js": "0.0.1",

@ -10,6 +10,7 @@ import getIntro from './shared/utils/getIntro';
import getOutro from './shared/utils/getOutro';
import processCss from './shared/processCss';
import annotateWithScopes from '../utils/annotateWithScopes';
import clone from '../utils/clone';
import DomBlock from './dom/Block';
import SsrBlock from './server-side-rendering/Block';
import { Node, Parsed, CompileOptions } from '../interfaces';
@ -32,6 +33,7 @@ export default class Generator {
code: MagicString;
bindingGroups: string[];
indirectDependencies: Map<string, Set<string>>;
expectedProperties: Set<string>;
cascade: boolean;
css: string;
@ -48,6 +50,8 @@ export default class Generator {
name: string,
options: CompileOptions
) {
this.ast = clone(parsed);
this.parsed = parsed;
this.source = source;
this.name = name;
@ -61,6 +65,7 @@ export default class Generator {
this.importedComponents = new Map();
this.bindingGroups = [];
this.indirectDependencies = new Map();
// track which properties are needed, so we can provide useful info
// in dev mode
@ -185,8 +190,20 @@ export default class Generator {
},
});
const dependencies = 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: expression._dependencies, // TODO probably a better way to do this
dependencies: Array.from(dependencies),
contexts: usedContexts,
snippet: `[✂${expression.start}-${expression.end}✂]`,
};
@ -330,6 +347,7 @@ export default class Generator {
addString('\n\n' + getOutro(format, name, options, this.imports));
return {
ast: this.ast,
code: compiled.toString(),
map: compiled.generateMap({
includeContent: true,

@ -174,7 +174,7 @@ export default class Block {
);
}
findDependencies(expression) {
findDependencies(expression: Node) {
return this.generator.findDependencies(
this.contextDependencies,
this.indexes,

@ -8,4 +8,5 @@ export interface State {
inEachBlock?: boolean;
allUsedContexts?: string[];
usesComponent?: boolean;
selectBindingDependencies?: string[];
}

@ -217,6 +217,60 @@ const preprocessors = {
state: State,
node: Node
) => {
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Attribute' && attribute.value !== true) {
attribute.value.forEach((chunk: Node) => {
if (chunk.type !== 'Text') {
const dependencies = block.findDependencies(chunk.expression);
block.addDependencies(dependencies);
// special case — <option value='{{foo}}'> — see below
if (node.name === 'option' && attribute.name === 'value' && state.selectBindingDependencies) {
state.selectBindingDependencies.forEach(prop => {
dependencies.forEach((dependency: string) => {
generator.indirectDependencies.get(prop).add(dependency);
});
});
}
}
});
} else if (attribute.type === 'Binding') {
const dependencies = block.findDependencies(attribute.value);
block.addDependencies(dependencies);
} else if (attribute.type === 'Transition') {
if (attribute.intro)
generator.hasIntroTransitions = block.hasIntroMethod = true;
if (attribute.outro) {
generator.hasOutroTransitions = block.hasOutroMethod = true;
block.outros += 1;
}
}
});
// special case — in a case like this...
//
// <select bind:value='foo'>
// <option value='{{bar}}'>bar</option>
// <option value='{{baz}}'>baz</option>
// </option>
//
// ...we need to know that `foo` depends on `bar` and `baz`,
// so that if `foo.qux` changes, we know that we need to
// mark `bar` and `baz` as dirty too
if (node.name === 'select') {
const value = node.attributes.find((attribute: Node) => attribute.name === 'value');
if (value) {
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
const dependencies = block.findDependencies(value.value);
state.selectBindingDependencies = dependencies;
dependencies.forEach((prop: string) => {
generator.indirectDependencies.set(prop, new Set());
});
} else {
state.selectBindingDependencies = null;
}
}
const isComponent =
generator.components.has(node.name) || node.name === ':Self';
@ -239,27 +293,6 @@ const preprocessors = {
});
}
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Attribute' && attribute.value !== true) {
attribute.value.forEach((chunk: Node) => {
if (chunk.type !== 'Text') {
const dependencies = block.findDependencies(chunk.expression);
block.addDependencies(dependencies);
}
});
} else if (attribute.type === 'Binding') {
const dependencies = block.findDependencies(attribute.value);
block.addDependencies(dependencies);
} else if (attribute.type === 'Transition') {
if (attribute.intro)
generator.hasIntroTransitions = block.hasIntroMethod = true;
if (attribute.outro) {
generator.hasOutroTransitions = block.hasOutroMethod = true;
block.outros += 1;
}
}
});
if (node.children.length) {
if (isComponent) {
const name = block.getUniqueName(

@ -89,6 +89,11 @@ export default function visitBinding(
${ifStatement}
}
`;
generator.hasComplexBindings = true;
block.builders.create.addBlock(
`if ( !('${name}' in state) ) ${block.component}._bindings.push( ${handler} );`
);
} else if (attribute.name === 'group') {
// <input type='checkbox|radio' bind:group='selected'> special case
if (type === 'radio') {

@ -271,11 +271,12 @@ function compound(
`);
}
block.builders.unmount.addLine(
`${if_name}${name}.unmount();`
);
block.builders.destroy.addLine(
`${if_name}{
${name}.unmount();
${name}.destroy();
}`
`${if_name}${name}.destroy();`
);
}

@ -25,7 +25,10 @@ export default function getSetter({
${computed && `var state = ${block.component}.get();`}
list[index]${tail} = ${value};
${block.component}._set({ ${prop}: ${block.component}.get( '${prop}' ) });
${computed ?
`${block.component}._set({ ${dependencies.map((prop: string) => `${prop}: state.${prop}`).join(', ')} });` :
`${block.component}._set({ ${dependencies.map((prop: string) => `${prop}: ${block.component}.get( '${prop}' )`).join(', ')} });`
}
`;
}
@ -35,7 +38,7 @@ export default function getSetter({
return deindent`
var state = ${block.component}.get();
${snippet} = ${value};
${block.component}._set({ ${name}: state.${name} });
${block.component}._set({ ${dependencies.map((prop: string) => `${prop}: state.${prop}`).join(', ')} });
`;
}

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

@ -164,14 +164,12 @@ function create_main_fragment ( state, component ) {
},
unmount: function () {
if_block.unmount();
detachNode( if_block_anchor );
},
destroy: function () {
{
if_block.unmount();
if_block.destroy();
}
if_block.destroy();
}
};
}

@ -29,14 +29,12 @@ function create_main_fragment ( state, component ) {
},
unmount: function () {
if_block.unmount();
detachNode( if_block_anchor );
},
destroy: function () {
{
if_block.unmount();
if_block.destroy();
}
if_block.destroy();
}
};
}
@ -116,4 +114,4 @@ SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = functio
this._torndown = true;
};
export default SvelteComponent;
export default SvelteComponent;

@ -1,10 +1,10 @@
import assert from "assert";
import * as fs from "fs";
import { svelte } from "../helpers.js";
import assert from 'assert';
import fs from 'fs';
import { svelte } from '../helpers.js';
describe("parse", () => {
fs.readdirSync("test/parser/samples").forEach(dir => {
if (dir[0] === ".") return;
describe('parse', () => {
fs.readdirSync('test/parser/samples').forEach(dir => {
if (dir[0] === '.') return;
// add .solo to a sample directory name to only run that test
const solo = /\.solo$/.test(dir);
@ -17,14 +17,14 @@ describe("parse", () => {
(solo ? it.only : it)(dir, () => {
const input = fs
.readFileSync(`test/parser/samples/${dir}/input.html`, "utf-8")
.replace(/\s+$/, "");
.readFileSync(`test/parser/samples/${dir}/input.html`, 'utf-8')
.replace(/\s+$/, '');
try {
const actual = svelte.parse(input);
fs.writeFileSync(
`test/parser/samples/${dir}/_actual.json`,
JSON.stringify(actual, null, "\t")
JSON.stringify(actual, null, '\t')
);
const expected = require(`./samples/${dir}/output.json`);
@ -32,7 +32,7 @@ describe("parse", () => {
assert.deepEqual(actual.css, expected.css);
assert.deepEqual(actual.js, expected.js);
} catch (err) {
if (err.name !== "ParseError") throw err;
if (err.name !== 'ParseError') throw err;
try {
const expected = require(`./samples/${dir}/error.json`);
@ -41,13 +41,13 @@ describe("parse", () => {
assert.deepEqual(err.loc, expected.loc);
assert.equal(err.pos, expected.pos);
} catch (err2) {
throw err2.code === "MODULE_NOT_FOUND" ? err : err2;
throw err2.code === 'MODULE_NOT_FOUND' ? err : err2;
}
}
});
});
it("handles errors with options.onerror", () => {
it('handles errors with options.onerror', () => {
let errored = false;
svelte.compile(`<h1>unclosed`, {
@ -60,9 +60,18 @@ describe("parse", () => {
assert.ok(errored);
});
it("throws without options.onerror", () => {
it('throws without options.onerror', () => {
assert.throws(() => {
svelte.compile(`<h1>unclosed`);
}, /<h1> was left open/);
});
it('includes AST in svelte.compile output', () => {
const dir = fs.readdirSync('test/parser/samples')[0];
const source = fs.readFileSync(`test/parser/samples/${dir}/input.html`, 'utf-8');
const { ast } = svelte.compile(source);
const parsed = svelte.parse(source);
assert.deepEqual(ast, parsed);
});
});

@ -0,0 +1,91 @@
const tasks = [
{ description: 'put your left leg in', done: false },
{ description: 'your left leg out', done: false },
{ description: 'in, out, in, out', done: false },
{ description: 'shake it all about', done: false }
];
export default {
'skip-ssr': true,
allowES2015: true,
data: {
tasks,
selected: tasks[0]
},
html: `
<select>
<option value='[object Object]'>put your left leg in</option>
<option value='[object Object]'>your left leg out</option>
<option value='[object Object]'>in, out, in, out</option>
<option value='[object Object]'>shake it all about</option>
</select>
<label>
<input type='checkbox'> put your left leg in
</label>
<h2>Pending tasks</h2>
<p>put your left leg in</p>
<p>your left leg out</p>
<p>in, out, in, out</p>
<p>shake it all about</p>
`,
test(assert, component, target, window) {
const input = target.querySelector('input');
const select = target.querySelector('select');
const options = target.querySelectorAll('option');
const change = new window.Event('change');
input.checked = true;
input.dispatchEvent(change);
assert.ok(component.get('tasks')[0].done);
assert.htmlEqual(target.innerHTML, `
<select>
<option value='[object Object]'>put your left leg in</option>
<option value='[object Object]'>your left leg out</option>
<option value='[object Object]'>in, out, in, out</option>
<option value='[object Object]'>shake it all about</option>
</select>
<label>
<input type='checkbox'> put your left leg in
</label>
<h2>Pending tasks</h2>
<p>your left leg out</p>
<p>in, out, in, out</p>
<p>shake it all about</p>
`);
options[1].selected = true;
select.dispatchEvent(change);
assert.equal(component.get('selected'), tasks[1]);
assert.ok(!input.checked);
input.checked = true;
input.dispatchEvent(change);
assert.ok(component.get('tasks')[1].done);
assert.htmlEqual(target.innerHTML, `
<select>
<option value='[object Object]'>put your left leg in</option>
<option value='[object Object]'>your left leg out</option>
<option value='[object Object]'>in, out, in, out</option>
<option value='[object Object]'>shake it all about</option>
</select>
<label>
<input type='checkbox'> your left leg out
</label>
<h2>Pending tasks</h2>
<p>in, out, in, out</p>
<p>shake it all about</p>
`);
}
};

@ -0,0 +1,14 @@
<select bind:value='selected'>
{{#each tasks as task}}
<option value='{{task}}'>{{task.description}}</option>
{{/each}}
</select>
<label>
<input type='checkbox' bind:checked='selected.done'> {{selected.description}}
</label>
<h2>Pending tasks</h2>
{{#each tasks.filter(t => !t.done) as task}}
<p>{{task.description}}</p>
{{/each}}

@ -0,0 +1,25 @@
export default {
'skip-ssr': true, // TODO would be nice to fix this in SSR as well
html: `
<p>selected: a</p>
<select>
<option>a</option>
<option>b</option>
<option>c</option>
</select>
<p>selected: a</p>
`,
test ( assert, component, target ) {
const select = target.querySelector( 'select' );
const options = [ ...target.querySelectorAll( 'option' ) ];
assert.equal( select.value, 'a' );
assert.ok( options[0].selected );
component.destroy();
}
};

@ -0,0 +1,9 @@
<p>selected: {{selected}}</p>
<select bind:value='selected'>
<option>a</option>
<option>b</option>
<option>c</option>
</select>
<p>selected: {{selected}}</p>

@ -0,0 +1,22 @@
<div class="level1">
{{#each values as value}}
<h4>level 1 #{{value}}</h4>
<Level2 condition="{{value % 2}}">
<Level3>
<span>And more stuff goes in here</span>
</Level3>
</Level2>
{{/each}}
</div>
<script>
import Level2 from './Level2.html';
import Level3 from './Level3.html';
export default {
components: {
Level2,
Level3
}
};
</script>

@ -0,0 +1,8 @@
<div class="level2" ref:wat>
<h4>level 2</h4>
{{#if condition}}
<span>TRUE! {{yield}}</span>
{{else}}
<span>FALSE! {{yield}}</span>
{{/if}}
</div>

@ -0,0 +1,4 @@
<div class="level3" ref:thingy>
<h4>level 3</h4>
{{yield}}
</div>

@ -0,0 +1,9 @@
export default {
data: {
values: [1, 2, 3, 4]
},
test(assert, component) {
component.set({ values: [2, 3] });
}
};

@ -0,0 +1,11 @@
<Level1 :values/>
<script>
import Level1 from './Level1.html';
export default {
components: {
Level1
}
}
</script>

@ -976,6 +976,34 @@ doctrine@^2.0.0:
esutils "^2.0.2"
isarray "^1.0.0"
dom-serializer@0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
dependencies:
domelementtype "~1.1.1"
entities "~1.1.1"
domelementtype@1, domelementtype@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
domelementtype@~1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
domhandler@^2.3.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259"
dependencies:
domelementtype "1"
domutils@^1.5.1:
version "1.6.2"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff"
dependencies:
dom-serializer "0"
domelementtype "1"
ecc-jsbn@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
@ -986,6 +1014,10 @@ electron-to-chromium@^1.2.7:
version "1.3.8"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.8.tgz#b2c8a2c79bb89fbbfd3724d9555e15095b5f5fb6"
entities@^1.1.1, entities@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
error-ex@^1.2.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
@ -1083,6 +1115,12 @@ eslint-module-utils@^2.0.0:
debug "2.2.0"
pkg-dir "^1.0.0"
eslint-plugin-html@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-3.0.0.tgz#2c982a71b9d965654f0f3a52358735821a1a4239"
dependencies:
htmlparser2 "^3.8.2"
eslint-plugin-import@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.2.0.tgz#72ba306fad305d67c4816348a4699a4229ac8b4e"
@ -1472,6 +1510,17 @@ html-encoding-sniffer@^1.0.1:
dependencies:
whatwg-encoding "^1.0.1"
htmlparser2@^3.8.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
dependencies:
domelementtype "^1.3.0"
domhandler "^2.3.0"
domutils "^1.5.1"
entities "^1.1.1"
inherits "^2.0.1"
readable-stream "^2.0.2"
http-signature@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
@ -1499,7 +1548,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@^2.0.3, inherits@~2.0.1:
inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
@ -2285,7 +2334,7 @@ read-pkg@^1.0.0:
normalize-package-data "^2.3.2"
path-type "^1.0.0"
readable-stream@^2.2.2:
readable-stream@^2.0.2, readable-stream@^2.2.2:
version "2.2.9"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.9.tgz#cf78ec6f4a6d1eb43d26488cac97f042e74b7fc8"
dependencies:

Loading…
Cancel
Save