Merge branch 'master' into gh-1117

pull/1151/head
Rich Harris 7 years ago committed by GitHub
commit d6dc60285e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,14 @@
# Svelte changelog
## 1.54.2
* Prevent `await` blocks using stale state ([#1131](https://github.com/sveltejs/svelte/issues/1131))
* Prevent erroneous missing data warnings for custom elements ([#1065](https://github.com/sveltejs/svelte/issues/1065))
* Remove empty selectors in prod mode ([#1138](https://github.com/sveltejs/svelte/issues/1138))
* Escape attribute values in SSR mode ([#1155](https://github.com/sveltejs/svelte/pull/1155))
* Remove `<noscript>` elements in DOM mode ([#1108](https://github.com/sveltejs/svelte/issues/1108))
* Allow hydration of non-root `<script>`/`<style>` tags ([#1163](https://github.com/sveltejs/svelte/pull/1163))
## 1.54.1
* Hoist destructured references ([#1139](https://github.com/sveltejs/svelte/issues/1139))

@ -1,6 +1,6 @@
{
"name": "svelte",
"version": "1.54.1",
"version": "1.54.2",
"description": "The magical disappearing UI framework",
"main": "compiler/svelte.js",
"files": [

@ -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<string, string>;
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;
}
});

@ -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(', ')}];`}

@ -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});
});

@ -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} = {};
});
`;

@ -162,6 +162,8 @@ export default class Element extends Node {
this.generator.slots.add(slotName);
}
if (this.name === 'noscript') return;
const childState = {
parentNode: this.var,
parentNodes: parentNodes && block.getUniqueName(`${this.var}_nodes`) // if we're in unclaimable territory, i.e. <head>, parentNodes is null
@ -415,7 +417,15 @@ export default class Element extends Node {
}
function toHTML(node: Element | Text) {
if (node.type === 'Text') return escapeHTML(node.data);
if (node.type === 'Text') {
return node.parent &&
node.parent.type === 'Element' &&
(node.parent.name === 'script' || node.parent.name === 'style')
? node.data
: escapeHTML(node.data);
}
if (node.name === 'noscript') return '';
let open = `<${node.name}`;
@ -433,10 +443,6 @@ export default class Element extends Node {
if (isVoidElementName(node.name)) return open + '>';
if (node.name === 'script' || node.name === 'style') {
return `${open}>${node.data}</${node.name}>`;
}
return `${open}>${node.children.map(toHTML).join('')}</${node.name}>`;
}
}

@ -6,7 +6,7 @@ import { AppendTarget } from '../interfaces';
import { Node } from '../../../interfaces';
import getObject from '../../../utils/getObject';
import getTailSnippet from '../../../utils/getTailSnippet';
import { stringify } from '../../../utils/stringify';
import { escape, escapeTemplate, stringify } from '../../../utils/stringify';
export default function visitComponent(
generator: SsrGenerator,
@ -14,7 +14,9 @@ export default function visitComponent(
node: Node
) {
function stringifyAttribute(chunk: Node) {
if (chunk.type === 'Text') return chunk.data;
if (chunk.type === 'Text') {
return escapeTemplate(escape(chunk.data));
}
if (chunk.type === 'MustacheTag') {
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;

@ -68,8 +68,6 @@ export default function visitElement(
if (node.name === 'textarea' && textareaContents !== undefined) {
generator.append(textareaContents);
} else if (node.name === 'script' || node.name === 'style') {
generator.append(escape(node.data));
} else {
node.children.forEach((child: Node) => {
visit(generator, block, child);

@ -10,5 +10,11 @@ export default function visitMustacheTag(
block.contextualise(node.expression);
const { snippet } = node.metadata;
generator.append('${__escape(' + snippet + ')}');
generator.append(
node.parent &&
node.parent.type === 'Element' &&
node.parent.name === 'style'
? '${' + snippet + '}'
: '${__escape(' + snippet + ')}'
);
}

@ -1,6 +1,6 @@
import { SsrGenerator } from '../index';
import Block from '../Block';
import { escape, escapeHTML } from '../../../utils/stringify';
import { escape, escapeHTML, escapeTemplate } from '../../../utils/stringify';
import { Node } from '../../../interfaces';
export default function visitText(
@ -8,5 +8,14 @@ export default function visitText(
block: Block,
node: Node
) {
generator.append(escapeHTML(escape(node.data).replace(/(\${|`|\\)/g, '\\$1')));
let text = node.data;
if (
!node.parent ||
node.parent.type !== 'Element' ||
(node.parent.name !== 'script' && node.parent.name !== 'style')
) {
// unless this Text node is inside a <script> or <style> element, escape &,<,>
text = escapeHTML(text);
}
generator.append(escape(escapeTemplate(text)));
}

@ -1,17 +1,17 @@
import Block from '../../Block';
import { escape } from '../../../../utils/stringify';
import { escape, escapeTemplate } from '../../../../utils/stringify';
import { Node } from '../../../../interfaces';
export default function stringifyAttributeValue(block: Block, chunks: Node[]) {
return chunks
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return escape(chunk.data).replace(/"/g, '&quot;');
return escapeTemplate(escape(chunk.data).replace(/"/g, '&quot;'));
}
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
return '${' + snippet + '}';
return '${__escape(' + snippet + ')}';
})
.join('');
}

@ -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);

@ -224,10 +224,22 @@ export default function tag(parser: Parser) {
);
parser.read(/<\/textarea>/);
element.end = parser.index;
} else if (name === 'script' || name === 'style') {
} else if (name === 'script') {
// special case
element.data = parser.readUntil(new RegExp(`</${name}>`));
parser.eat(`</${name}>`, true);
const start = parser.index;
const data = parser.readUntil(/<\/script>/);
const end = parser.index;
element.children.push({ start, end, type: 'Text', data });
parser.eat('</script>', true);
element.end = parser.index;
} else if (name === 'style') {
// special case
element.children = readSequence(
parser,
() =>
parser.template.slice(parser.index, parser.index + 8) === '</style>'
);
parser.read(/<\/style>/);
element.end = parser.index;
} else {
parser.stack.push(element);

@ -17,3 +17,7 @@ const escaped = {
export function escapeHTML(html) {
return String(html).replace(/[&<>]/g, match => escaped[match]);
}
export function escapeTemplate(str) {
return str.replace(/(\${|`|\\)/g, '\\$1');
}

@ -0,0 +1,4 @@
export default {
cascade: false,
dev: true
};

@ -0,0 +1,7 @@
<div class='foo'></div>
<style>
.foo {
/* empty */
}
</style>

@ -0,0 +1,3 @@
export default {
cascade: false
};

@ -0,0 +1,7 @@
<div class='foo'></div>
<style>
.foo {
/* empty */
}
</style>

@ -3,7 +3,7 @@ import * as http from 'http';
import { rollup } from 'rollup';
import virtual from 'rollup-plugin-virtual';
import Nightmare from 'nightmare';
import { addLineNumbers, loadSvelte } from "../helpers.js";
import { addLineNumbers, loadConfig, loadSvelte } from "../helpers.js";
const page = `
<body>
@ -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 {

@ -0,0 +1,8 @@
<p>foo: {{foo}}</p>
<p>bar: {{bar}}</p>
<script>
export default {
tag: 'my-app'
};
</script>

@ -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 = '<my-app foo=yes />';
assert.equal(warnings.length, 1);
assert.equal(warnings[0], `<my-app> was created without expected data property 'bar'`);
console.warn = warn;
}

@ -0,0 +1,3 @@
export default {
html: `<span title='"foo"'>foo</span>`
};

@ -0,0 +1 @@
<span title='{{"\"foo\""}}'>foo</span>

@ -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, `<p>wait for it...</p>`);
return promise
.then(() => {
assert.htmlEqual(target.innerHTML, `
<p>the answer is 42!</p>
`);
});
}
};

@ -0,0 +1,9 @@
{{#if promise}}
{{#await promise}}
<p>wait for it...</p>
{{then _}}
<p>the answer is {{answer}}!</p>
{{catch error}}
<p>well that's odd</p>
{{/await}}
{{/if}}

@ -1,3 +1,3 @@
export default {
html: '<code>`${foo}\\n`</code>'
html: '<code>`${foo}\\n`</code>\n<div title="`${foo}\\n`">foo</div>\n<div>`${foo}\\n`</div>',
};

@ -1 +1,8 @@
<code>`${foo}\n`</code>
<div title="`${foo}\n`">foo</div>
<Widget value="`${foo}\n`"/>
<script>
import Widget from './Widget.html';
export default { components: { Widget } };
</script>

@ -0,0 +1,52 @@
export default {
data: {
color: 'red',
foo: '/* < & > */',
},
html: `
<div>
<style>
/* something with < and > */
div {
color: blue;
}
</style>
foo
</div>
<div>
<div>
<style>
div > div {
color: blue;
}
</style>
foo
</div>
</div>
<div>
<style>
/* something with < and > */
/* < & > */
div {
color: red;
}
</style>
foo
</div>
<div>
<div>
<style>
/* < & > */
div > div {
color: red;
}
</style>
foo
</div>
</div>
`,
};

@ -0,0 +1,43 @@
<div>
<style>
/* something with < and > */
div {
color: blue;
}
</style>
foo
</div>
<div>
<div>
<style>
div > div {
color: blue;
}
</style>
foo
</div>
</div>
<div>
<style>
/* something with < and > */
{{foo}}
div {
color: {{color}};
}
</style>
foo
</div>
<div>
<div>
<style>
{{foo}}
div > div {
color: {{color}};
}
</style>
foo
</div>
</div>

@ -0,0 +1,9 @@
export default {
'skip-ssr': true,
html: `
<div>foo</div>
<div>foo<div>foo</div></div>
`,
};

@ -0,0 +1,5 @@
<noscript>foo</noscript>
<div>foo<noscript>foo</noscript></div>
<div>foo<div>foo<noscript>foo</noscript></div></div>

@ -0,0 +1,8 @@
export default {
html: `
<div>
<style>div { color: red; }</style>
<script>alert('<>');</script>
</div>
`
};

@ -0,0 +1,4 @@
<div>
<style>div { color: red; }</style>
<script>alert('<>');</script>
</div>

@ -0,0 +1,4 @@
export default {
data: { foo: 'foo' },
html: `<div>foo @ foo # foo</div>`,
};

@ -0,0 +1,6 @@
<Widget value='foo @ {{foo}} # foo'/>
<script>
import Widget from './Widget.html';
export default { components: { Widget } };
</script>

@ -0,0 +1,28 @@
import { Store } from '../../../../store.js';
const store = new Store({
name: 'world'
});
export default {
store,
html: `
<h1>Hello world!</h1>
<input>
`,
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, `
<h1>Hello everybody!</h1>
<input>
`);
}
};

@ -0,0 +1,10 @@
<h1>Hello {{$name}}!</h1>
<TextInput bind:value=$name/>
<script>
import TextInput from './TextInput.html';
export default {
components: { TextInput }
};
</script>
Loading…
Cancel
Save