From a69442734d934cd6fe343d4dd4cb54cd0885eaab Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 3 Feb 2018 18:00:08 -0500 Subject: [PATCH 01/17] remove empty style blocks in prod mode - fixes #1138 --- src/css/Stylesheet.ts | 21 +++++++++++-------- src/index.ts | 2 +- .../cascade-false-empty-rule-dev/_config.js | 4 ++++ .../cascade-false-empty-rule-dev/expected.css | 1 + .../cascade-false-empty-rule-dev/input.html | 7 +++++++ .../cascade-false-empty-rule/_config.js | 3 +++ .../cascade-false-empty-rule/expected.css | 0 .../cascade-false-empty-rule/input.html | 7 +++++++ 8 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 test/css/samples/cascade-false-empty-rule-dev/_config.js create mode 100644 test/css/samples/cascade-false-empty-rule-dev/expected.css create mode 100644 test/css/samples/cascade-false-empty-rule-dev/input.html create mode 100644 test/css/samples/cascade-false-empty-rule/_config.js create mode 100644 test/css/samples/cascade-false-empty-rule/expected.css create mode 100644 test/css/samples/cascade-false-empty-rule/input.html diff --git a/src/css/Stylesheet.ts b/src/css/Stylesheet.ts index 6254795697..200e73f582 100644 --- a/src/css/Stylesheet.ts +++ b/src/css/Stylesheet.ts @@ -25,12 +25,13 @@ class Rule { this.selectors.forEach(selector => selector.apply(node, stack)); // TODO move the logic in here? } - isUsed() { + isUsed(dev: boolean) { if (this.parent && this.parent.node.type === 'Atrule' && this.parent.node.name === 'keyframes') return true; + if (this.declarations.length === 0) return dev; return this.selectors.some(s => s.used); } - minify(code: MagicString, cascade: boolean) { + minify(code: MagicString, cascade: boolean, dev: boolean) { let c = this.node.start; let started = false; @@ -177,11 +178,11 @@ class Atrule { } } - isUsed() { + isUsed(dev: boolean) { return true; // TODO } - minify(code: MagicString, cascade: boolean) { + minify(code: MagicString, cascade: boolean, dev: boolean) { if (this.node.name === 'media') { const expressionChar = code.original[this.node.expression.start]; let c = this.node.start + (expressionChar === '(' ? 6 : 7); @@ -206,9 +207,9 @@ class Atrule { let c = this.node.block.start + 1; this.children.forEach(child => { - if (cascade || child.isUsed()) { + if (cascade || child.isUsed(dev)) { code.remove(c, child.node.start); - child.minify(code, cascade); + child.minify(code, cascade, dev); c = child.node.end; } }); @@ -257,6 +258,7 @@ export default class Stylesheet { parsed: Parsed; cascade: boolean; filename: string; + dev: boolean; hasStyles: boolean; id: string; @@ -264,11 +266,12 @@ export default class Stylesheet { children: (Rule|Atrule)[]; keyframes: Map; - constructor(source: string, parsed: Parsed, filename: string, cascade: boolean) { + constructor(source: string, parsed: Parsed, filename: string, cascade: boolean, dev: boolean) { this.source = source; this.parsed = parsed; this.cascade = cascade; this.filename = filename; + this.dev = dev; this.children = []; this.keyframes = new Map(); @@ -374,9 +377,9 @@ export default class Stylesheet { let c = 0; this.children.forEach(child => { - if (this.cascade || child.isUsed()) { + if (this.cascade || child.isUsed(this.dev)) { code.remove(c, child.node.start); - child.minify(code, this.cascade); + child.minify(code, this.cascade, this.dev); c = child.node.end; } }); diff --git a/src/index.ts b/src/index.ts index 1caf5b5abc..be62f07763 100644 --- a/src/index.ts +++ b/src/index.ts @@ -116,7 +116,7 @@ export function compile(source: string, _options: CompileOptions) { return; } - const stylesheet = new Stylesheet(source, parsed, options.filename, options.cascade !== false); + const stylesheet = new Stylesheet(source, parsed, options.filename, options.cascade !== false, options.dev); validate(parsed, source, stylesheet, options); diff --git a/test/css/samples/cascade-false-empty-rule-dev/_config.js b/test/css/samples/cascade-false-empty-rule-dev/_config.js new file mode 100644 index 0000000000..8b2e3aa341 --- /dev/null +++ b/test/css/samples/cascade-false-empty-rule-dev/_config.js @@ -0,0 +1,4 @@ +export default { + cascade: false, + dev: true +}; \ No newline at end of file diff --git a/test/css/samples/cascade-false-empty-rule-dev/expected.css b/test/css/samples/cascade-false-empty-rule-dev/expected.css new file mode 100644 index 0000000000..5e2b654711 --- /dev/null +++ b/test/css/samples/cascade-false-empty-rule-dev/expected.css @@ -0,0 +1 @@ +.foo[svelte-xyz]{} \ No newline at end of file diff --git a/test/css/samples/cascade-false-empty-rule-dev/input.html b/test/css/samples/cascade-false-empty-rule-dev/input.html new file mode 100644 index 0000000000..ac2e43dceb --- /dev/null +++ b/test/css/samples/cascade-false-empty-rule-dev/input.html @@ -0,0 +1,7 @@ +
+ + \ No newline at end of file diff --git a/test/css/samples/cascade-false-empty-rule/_config.js b/test/css/samples/cascade-false-empty-rule/_config.js new file mode 100644 index 0000000000..b37866f9b6 --- /dev/null +++ b/test/css/samples/cascade-false-empty-rule/_config.js @@ -0,0 +1,3 @@ +export default { + cascade: false +}; \ No newline at end of file diff --git a/test/css/samples/cascade-false-empty-rule/expected.css b/test/css/samples/cascade-false-empty-rule/expected.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/css/samples/cascade-false-empty-rule/input.html b/test/css/samples/cascade-false-empty-rule/input.html new file mode 100644 index 0000000000..ac2e43dceb --- /dev/null +++ b/test/css/samples/cascade-false-empty-rule/input.html @@ -0,0 +1,7 @@ +
+ + \ No newline at end of file From 5d17248074c9103809dd681beb2c339b3a054598 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 3 Feb 2018 22:44:26 -0500 Subject: [PATCH 02/17] prevent await blocks using stale state - fixes #1131 --- src/generators/nodes/AwaitBlock.ts | 2 ++ .../samples/await-set-simultaneous/_config.js | 16 ++++++++++++++++ .../samples/await-set-simultaneous/main.html | 9 +++++++++ 3 files changed, 27 insertions(+) create mode 100644 test/runtime/samples/await-set-simultaneous/_config.js create mode 100644 test/runtime/samples/await-set-simultaneous/main.html diff --git a/src/generators/nodes/AwaitBlock.ts b/src/generators/nodes/AwaitBlock.ts index 3b928f7804..445767d27f 100644 --- a/src/generators/nodes/AwaitBlock.ts +++ b/src/generators/nodes/AwaitBlock.ts @@ -127,8 +127,10 @@ export default class AwaitBlock extends Node { if (@isPromise(${promise})) { ${promise}.then(function(${value}) { + var state = #component.get(); ${replace_await_block}(${token}, ${create_then_block}, ${value}, ${params}); }, function (${error}) { + var state = #component.get(); ${replace_await_block}(${token}, ${create_catch_block}, ${error}, ${params}); }); diff --git a/test/runtime/samples/await-set-simultaneous/_config.js b/test/runtime/samples/await-set-simultaneous/_config.js new file mode 100644 index 0000000000..97f79f6119 --- /dev/null +++ b/test/runtime/samples/await-set-simultaneous/_config.js @@ -0,0 +1,16 @@ +export default { + test(assert, component, target) { + const promise = Promise.resolve().then(() => component.set({ answer: 42 })); + + component.set({ promise }); + + assert.htmlEqual(target.innerHTML, `

wait for it...

`); + + return promise + .then(() => { + assert.htmlEqual(target.innerHTML, ` +

the answer is 42!

+ `); + }); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/await-set-simultaneous/main.html b/test/runtime/samples/await-set-simultaneous/main.html new file mode 100644 index 0000000000..3740a90480 --- /dev/null +++ b/test/runtime/samples/await-set-simultaneous/main.html @@ -0,0 +1,9 @@ +{{#if promise}} + {{#await promise}} +

wait for it...

+ {{then _}} +

the answer is {{answer}}!

+ {{catch error}} +

well that's odd

+ {{/await}} +{{/if}} \ No newline at end of file From 91e16dd411b2122eefdffe7fb06c48cd014dd8e9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 4 Feb 2018 10:51:39 -0500 Subject: [PATCH 03/17] component store bindings - fixes #1100 --- src/generators/nodes/Component.ts | 67 +++++++++++++------ .../store-component-binding/TextInput.html | 1 + .../store-component-binding/_config.js | 28 ++++++++ .../samples/store-component-binding/main.html | 10 +++ 4 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 test/runtime/samples/store-component-binding/TextInput.html create mode 100644 test/runtime/samples/store-component-binding/_config.js create mode 100644 test/runtime/samples/store-component-binding/main.html diff --git a/src/generators/nodes/Component.ts b/src/generators/nodes/Component.ts index 1b9b1bb8b6..493302c3d5 100644 --- a/src/generators/nodes/Component.ts +++ b/src/generators/nodes/Component.ts @@ -140,40 +140,47 @@ export default class Component extends Node { const setParentFromChildOnChange = new CodeBuilder(); const setParentFromChildOnInit = new CodeBuilder(); + const setStoreFromChildOnChange = new CodeBuilder(); + const setStoreFromChildOnInit = new CodeBuilder(); + bindings.forEach((binding: Binding) => { - let setParentFromChild; + let { name: key } = getObject(binding.value); + + const isStoreProp = generator.options.store && key[0] === '$'; + if (isStoreProp) key = key.slice(1); + const newState = isStoreProp ? 'newStoreState' : 'newState'; binding.contexts.forEach(context => { allContexts.add(context); }); - const { name: key } = getObject(binding.value); + let setFromChild; - if (block.contexts.has(key)) { + if (!isStoreProp && block.contexts.has(key)) { const prop = binding.dependencies[0]; const computed = isComputed(binding.value); const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : ''; - setParentFromChild = deindent` + setFromChild = deindent` var list = ${name_context}.${block.listNames.get(key)}; var index = ${name_context}.${block.indexNames.get(key)}; list[index]${tail} = childState.${binding.name}; ${binding.dependencies - .map((prop: string) => `newState.${prop} = state.${prop};`) + .map((prop: string) => `${newState}.${prop} = state.${prop};`) .join('\n')} `; } else if (binding.value.type === 'MemberExpression') { - setParentFromChild = deindent` + setFromChild = deindent` ${binding.snippet} = childState.${binding.name}; - ${binding.dependencies.map((prop: string) => `newState.${prop} = state.${prop};`).join('\n')} + ${binding.dependencies.map((prop: string) => `${newState}.${prop} = state.${prop};`).join('\n')} `; } else { - setParentFromChild = `newState.${binding.value.name} = childState.${binding.name};`; + setFromChild = `${newState}.${key} = childState.${binding.name};`; } statements.push(deindent` @@ -183,14 +190,14 @@ export default class Component extends Node { }` ); - setParentFromChildOnChange.addConditional( + (isStoreProp ? setStoreFromChildOnChange : setParentFromChildOnChange).addConditional( `!${name_updating}.${binding.name} && changed.${binding.name}`, - setParentFromChild + setFromChild ); - setParentFromChildOnInit.addConditional( + (isStoreProp ? setStoreFromChildOnInit : setParentFromChildOnInit).addConditional( `!${name_updating}.${binding.name}`, - setParentFromChild + setFromChild ); // TODO could binding.dependencies.length ever be 0? @@ -206,23 +213,45 @@ export default class Component extends Node { componentInitProperties.push(`data: ${name_initial_data}`); + const initialisers = [ + 'state = #component.get()', + !setParentFromChildOnChange.isEmpty() && 'newState = {}', + !setStoreFromChildOnChange.isEmpty() && 'newStoreState = {}', + ].filter(Boolean).join(', '); + componentInitProperties.push(deindent` _bind: function(changed, childState) { - var state = #component.get(), newState = {}; - ${setParentFromChildOnChange} - ${name_updating} = @assign({}, changed); - #component._set(newState); + var ${initialisers}; + ${!setStoreFromChildOnChange.isEmpty() && deindent` + ${setStoreFromChildOnChange} + ${name_updating} = @assign({}, changed); + #component.store.set(newStoreState); + `} + ${!setParentFromChildOnChange.isEmpty() && deindent` + ${setParentFromChildOnChange} + ${name_updating} = @assign({}, changed); + #component._set(newState); + `} ${name_updating} = {}; } `); + // TODO can `!childState` ever be true? beforecreate = deindent` #component.root._beforecreate.push(function() { - var state = #component.get(), childState = ${name}.get(), newState = {}; + var childState = ${name}.get(), ${initialisers}; if (!childState) return; ${setParentFromChildOnInit} - ${name_updating} = { ${bindings.map((binding: Binding) => `${binding.name}: true`).join(', ')} }; - #component._set(newState); + ${!setStoreFromChildOnInit.isEmpty() && deindent` + ${setStoreFromChildOnInit} + ${name_updating} = { ${bindings.map((binding: Binding) => `${binding.name}: true`).join(', ')} }; + #component.store.set(newStoreState); + `} + ${!setParentFromChildOnInit.isEmpty() && deindent` + ${setParentFromChildOnInit} + ${name_updating} = { ${bindings.map((binding: Binding) => `${binding.name}: true`).join(', ')} }; + #component._set(newState); + `} ${name_updating} = {}; }); `; diff --git a/test/runtime/samples/store-component-binding/TextInput.html b/test/runtime/samples/store-component-binding/TextInput.html new file mode 100644 index 0000000000..f24d608cd5 --- /dev/null +++ b/test/runtime/samples/store-component-binding/TextInput.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/runtime/samples/store-component-binding/_config.js b/test/runtime/samples/store-component-binding/_config.js new file mode 100644 index 0000000000..aefc4ec652 --- /dev/null +++ b/test/runtime/samples/store-component-binding/_config.js @@ -0,0 +1,28 @@ +import { Store } from '../../../../store.js'; + +const store = new Store({ + name: 'world' +}); + +export default { + store, + + html: ` +

Hello world!

+ + `, + + test(assert, component, target, window) { + const input = target.querySelector('input'); + const event = new window.Event('input'); + + input.value = 'everybody'; + input.dispatchEvent(event); + + assert.equal(store.get('name'), 'everybody'); + assert.htmlEqual(target.innerHTML, ` +

Hello everybody!

+ + `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/store-component-binding/main.html b/test/runtime/samples/store-component-binding/main.html new file mode 100644 index 0000000000..aaebf609d2 --- /dev/null +++ b/test/runtime/samples/store-component-binding/main.html @@ -0,0 +1,10 @@ +

Hello {{$name}}!

+ + + \ No newline at end of file From dd47757d3bd1b4fb1c7d8b4f91743b5580cf4f4b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 4 Feb 2018 11:26:20 -0500 Subject: [PATCH 04/17] prevent erroneous missing data warnings for custom elements - fixes #1065 --- src/generators/dom/index.ts | 6 +++++- test/custom-elements/index.js | 7 +++++-- .../no-missing-prop-warnings/_config.js | 3 +++ .../samples/no-missing-prop-warnings/main.html | 8 ++++++++ .../samples/no-missing-prop-warnings/test.js | 18 ++++++++++++++++++ 5 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 test/custom-elements/samples/no-missing-prop-warnings/_config.js create mode 100644 test/custom-elements/samples/no-missing-prop-warnings/main.html create mode 100644 test/custom-elements/samples/no-missing-prop-warnings/test.js diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index 776cf1ab0b..eb21dad34c 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -219,7 +219,11 @@ export default function dom( const message = generator.components.has(prop) ? `${debugName} expected to find '${prop}' in \`data\`, but found it in \`components\` instead` : `${debugName} was created without expected data property '${prop}'`; - return `if (!('${prop}' in this._state)) console.warn("${message}");` + + const conditions = [`!('${prop}' in this._state)`]; + if (generator.customElement) conditions.push(`!('${prop}' in this.attributes)`); + + return `if (${conditions.join(' && ')}) console.warn("${message}");` })} ${generator.bindingGroups.length && `this._bindingGroups = [${Array(generator.bindingGroups.length).fill('[]').join(', ')}];`} diff --git a/test/custom-elements/index.js b/test/custom-elements/index.js index a3287b5d10..ce394c6b6a 100644 --- a/test/custom-elements/index.js +++ b/test/custom-elements/index.js @@ -3,7 +3,7 @@ import * as http from 'http'; import { rollup } from 'rollup'; import virtual from 'rollup-plugin-virtual'; import Nightmare from 'nightmare'; -import { loadSvelte } from "../helpers.js"; +import { loadSvelte, loadConfig } from "../helpers.js"; const page = ` @@ -57,6 +57,8 @@ describe('custom-elements', function() { const solo = /\.solo$/.test(dir); (solo ? it.only : it)(dir, () => { + const config = loadConfig(`./custom-elements/samples/${dir}/_config.js`); + return rollup({ input: `test/custom-elements/samples/${dir}/test.js`, plugins: [ @@ -64,7 +66,8 @@ describe('custom-elements', function() { transform(code, id) { if (id.endsWith('.html')) { const compiled = svelte.compile(code, { - customElement: true + customElement: true, + dev: config.dev }); return { diff --git a/test/custom-elements/samples/no-missing-prop-warnings/_config.js b/test/custom-elements/samples/no-missing-prop-warnings/_config.js new file mode 100644 index 0000000000..e26996239d --- /dev/null +++ b/test/custom-elements/samples/no-missing-prop-warnings/_config.js @@ -0,0 +1,3 @@ +export default { + dev: true +}; \ No newline at end of file diff --git a/test/custom-elements/samples/no-missing-prop-warnings/main.html b/test/custom-elements/samples/no-missing-prop-warnings/main.html new file mode 100644 index 0000000000..088c96947a --- /dev/null +++ b/test/custom-elements/samples/no-missing-prop-warnings/main.html @@ -0,0 +1,8 @@ +

foo: {{foo}}

+

bar: {{bar}}

+ + \ No newline at end of file diff --git a/test/custom-elements/samples/no-missing-prop-warnings/test.js b/test/custom-elements/samples/no-missing-prop-warnings/test.js new file mode 100644 index 0000000000..675fa66485 --- /dev/null +++ b/test/custom-elements/samples/no-missing-prop-warnings/test.js @@ -0,0 +1,18 @@ +import * as assert from 'assert'; +import './main.html'; + +export default function (target) { + const warnings = []; + const warn = console.warn; + + console.warn = warning => { + warnings.push(warning); + }; + + target.innerHTML = ''; + + assert.equal(warnings.length, 1); + assert.equal(warnings[0], ` was created without expected data property 'bar'`); + + console.warn = warn; +} \ No newline at end of file From 3d238b0846e630dcdbf89b45b38e45c4a81f518e Mon Sep 17 00:00:00 2001 From: Conduitry Date: Tue, 6 Feb 2018 21:01:40 -0500 Subject: [PATCH 05/17] escape attribute values in SSR --- .../visitors/shared/stringifyAttributeValue.ts | 2 +- test/runtime/samples/attribute-dynamic-quotemarks/_config.js | 3 +++ test/runtime/samples/attribute-dynamic-quotemarks/main.html | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 test/runtime/samples/attribute-dynamic-quotemarks/_config.js create mode 100644 test/runtime/samples/attribute-dynamic-quotemarks/main.html diff --git a/src/generators/server-side-rendering/visitors/shared/stringifyAttributeValue.ts b/src/generators/server-side-rendering/visitors/shared/stringifyAttributeValue.ts index 22eb0eff76..d2df80beab 100644 --- a/src/generators/server-side-rendering/visitors/shared/stringifyAttributeValue.ts +++ b/src/generators/server-side-rendering/visitors/shared/stringifyAttributeValue.ts @@ -11,7 +11,7 @@ export default function stringifyAttributeValue(block: Block, chunks: Node[]) { block.contextualise(chunk.expression); const { snippet } = chunk.metadata; - return '${' + snippet + '}'; + return '${__escape(' + snippet + ')}'; }) .join(''); } \ No newline at end of file diff --git a/test/runtime/samples/attribute-dynamic-quotemarks/_config.js b/test/runtime/samples/attribute-dynamic-quotemarks/_config.js new file mode 100644 index 0000000000..b9f4364624 --- /dev/null +++ b/test/runtime/samples/attribute-dynamic-quotemarks/_config.js @@ -0,0 +1,3 @@ +export default { + html: `foo` +}; diff --git a/test/runtime/samples/attribute-dynamic-quotemarks/main.html b/test/runtime/samples/attribute-dynamic-quotemarks/main.html new file mode 100644 index 0000000000..b5c3be5bbd --- /dev/null +++ b/test/runtime/samples/attribute-dynamic-quotemarks/main.html @@ -0,0 +1 @@ +foo From 27fbf382ce1114982a6ca83866c03027e19f1f9c Mon Sep 17 00:00:00 2001 From: Conduitry Date: Thu, 8 Feb 2018 01:24:56 -0500 Subject: [PATCH 06/17] remove