diff --git a/CHANGELOG.md b/CHANGELOG.md index 41d70eedcc..566fde5005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # Svelte changelog -## Unreleased +## 3.14.1 + +* Deconflict block method names with other variables ([#3900](https://github.com/sveltejs/svelte/issues/3900)) +* Fix entity encoding issue in text nodes with constant expressions ([#3911](https://github.com/sveltejs/svelte/issues/3911)) +* Make code for unknown prop warnings compatible with older js engines ([#3914](https://github.com/sveltejs/svelte/issues/3914)) + +## 3.14.0 + +* Add `loopGuardTimeout` option that augments `for`/`while` loops to prevent infinite loops, primarily for use in the REPL ([#3887](https://github.com/sveltejs/svelte/pull/3887)) +* Keep component bindings in sync when changed in reactive statements ([#3382](https://github.com/sveltejs/svelte/issues/3382)) +* Update attributes before bindings ([#3857](https://github.com/sveltejs/svelte/issues/3857)) +* Prevent variable naming conflict ([#3899](https://github.com/sveltejs/svelte/issues/3899)) + + +## 3.13.0 * New structured code generation, which eliminates a number of edge cases and obscure bugs ([#3539](https://github.com/sveltejs/svelte/pull/3539)) @@ -24,6 +38,24 @@ Also: * Flush changes in newly attached block when using `{#await}` ([#3660](https://github.com/sveltejs/svelte/issues/3660)) * Throw exception immediately when calling `createEventDispatcher()` after component instantiation ([#3667](https://github.com/sveltejs/svelte/pull/3667)) * Fix globals shadowing contextual template scope ([#3674](https://github.com/sveltejs/svelte/issues/3674)) +* Fix `` bindings to stores ([#3832](https://github.com/sveltejs/svelte/issues/3832)) +* Deconflict generated var names with builtins ([#3724](https://github.com/sveltejs/svelte/issues/3724)) +* Allow spring/tweened values to be initially undefined ([#3761](https://github.com/sveltejs/svelte/issues/3761)) +* Warn if using `` without `customElement: true` option ([#3782](https://github.com/sveltejs/svelte/pull/3782)) +* Add `Event` to list of known globals ([#3810](https://github.com/sveltejs/svelte/pull/3810)) +* Throw helpful error on empty CSS declaration ([#3801](https://github.com/sveltejs/svelte/issues/3801)) +* Support `easing` param on `fade` transition ([#3823](https://github.com/sveltejs/svelte/pull/3823)) +* Generate valid names from filenames with unicode characters ([#3845](https://github.com/sveltejs/svelte/issues/3845)) +* Don't generate any code for markup-less components ([#2200](https://github.com/sveltejs/svelte/issues/2200)) +* Deconflict with internal name `block` ([#3854](https://github.com/sveltejs/svelte/issues/3854)) +* Set attributes before bindings, to prevent erroneous assignments to `input.files` ([#3828](https://github.com/sveltejs/svelte/issues/3828)) +* Smarter unused CSS detection ([#3825](https://github.com/sveltejs/svelte/pull/3825)) +* Allow dynamic event handlers ([#3040](https://github.com/sveltejs/svelte/issues/3040)) +* Prevent erroneous `"undefined"` class name ([#3876](https://github.com/sveltejs/svelte/pull/3876)) +* Prevent resetting of `src` attribute unless changed ([#3579](https://github.com/sveltejs/svelte/pull/3579)) +* Prevent hydration of void element 'children' ([#3882](https://github.com/sveltejs/svelte/issues/3882)) +* Hoist globals even if mentioned in ` ``` -> The `svelte/easing` module contains the [Penner easing equations](http://robertpenner.com/easing/), or you can supply your own `p => t` function where `p` and `t` are both values between 0 and 1. +> The `svelte/easing` module contains the [Penner easing equations](https://web.archive.org/web/20190805215728/http://robertpenner.com/easing/), or you can supply your own `p => t` function where `p` and `t` are both values between 0 and 1. The full set of options available to `tweened`: @@ -37,4 +37,4 @@ The full set of options available to `tweened`: * `easing` — a `p => t` function * `interpolate` — a custom `(from, to) => t => value` function for interpolating between arbitrary values. By default, Svelte will interpolate between numbers, dates, and identically-shaped arrays and objects (as long as they only contain numbers and dates or other valid arrays and objects). If you want to interpolate (for example) colour strings or transformation matrices, supply a custom interpolator -You can also pass these options to `progress.set` and `progress.update` as a second argument, in which case they will override the defaults. The `set` and `update` methods both return a promise that resolves when the tween completes. \ No newline at end of file +You can also pass these options to `progress.set` and `progress.update` as a second argument, in which case they will override the defaults. The `set` and `update` methods both return a promise that resolves when the tween completes. diff --git a/site/content/tutorial/16-special-elements/07-svelte-options/app-a/Todo.svelte b/site/content/tutorial/16-special-elements/07-svelte-options/app-a/Todo.svelte index dae595e7d0..5e0dbca300 100644 --- a/site/content/tutorial/16-special-elements/07-svelte-options/app-a/Todo.svelte +++ b/site/content/tutorial/16-special-elements/07-svelte-options/app-a/Todo.svelte @@ -3,7 +3,6 @@ import flash from './flash.js'; export let todo; - export let toggle; let div; diff --git a/site/content/tutorial/16-special-elements/07-svelte-options/app-b/Todo.svelte b/site/content/tutorial/16-special-elements/07-svelte-options/app-b/Todo.svelte index 447ddc601c..e7ddcedc47 100644 --- a/site/content/tutorial/16-special-elements/07-svelte-options/app-b/Todo.svelte +++ b/site/content/tutorial/16-special-elements/07-svelte-options/app-b/Todo.svelte @@ -5,7 +5,6 @@ import flash from './flash.js'; export let todo; - export let toggle; let div; diff --git a/site/package-lock.json b/site/package-lock.json index 526f53f163..09bd67cf1e 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -1291,17 +1291,23 @@ } }, "@sveltejs/svelte-repl": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@sveltejs/svelte-repl/-/svelte-repl-0.1.9.tgz", - "integrity": "sha512-OXDfHwT5O7UXVYnf4ndTk3dKMITTmWcMty4/lOFte80ui01i47QiVy3GEe9G8FkcU1YBe+c06MMnIgm7j0Ln7Q==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@sveltejs/svelte-repl/-/svelte-repl-0.1.13.tgz", + "integrity": "sha512-griMkrRRzAQq22awKaBN5cewGly4xeeo8qpDs8ZhQxmEk5TcX3dMhIGMLuPTSv3Yr0s2c6TDJXcN+ORJn//fpQ==", "dev": true, "requires": { - "codemirror": "^5.48.4", - "estree-walker": "^0.6.1", + "codemirror": "^5.49.2", + "estree-walker": "^0.9.0", "sourcemap-codec": "^1.4.6", "yootils": "0.0.16" }, "dependencies": { + "estree-walker": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.9.0.tgz", + "integrity": "sha512-12U47o7XHUX329+x3FzNVjCx3SHEzMF0nkDv7r/HnBzX/xNTKxajBk6gyygaxrAFtLj39219oMfbtxv4KpaOiA==", + "dev": true + }, "sourcemap-codec": { "version": "1.4.6", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz", @@ -1587,9 +1593,9 @@ "dev": true }, "codemirror": { - "version": "5.48.4", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.48.4.tgz", - "integrity": "sha512-pUhZXDQ6qXSpWdwlgAwHEkd4imA0kf83hINmUEzJpmG80T/XLtDDEzZo8f6PQLuRCcUQhmzqqIo3ZPTRaWByRA==", + "version": "5.49.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.49.2.tgz", + "integrity": "sha512-dwJ2HRPHm8w51WB5YTF9J7m6Z5dtkqbU9ntMZ1dqXyFB9IpjoUFDj80ahRVEoVanfIp6pfASJbOlbWdEf8FOzQ==", "dev": true }, "color-convert": { @@ -3368,12 +3374,6 @@ "requires": { "@babel/helper-module-imports": "^7.0.0", "rollup-pluginutils": "^2.8.1" - }, - "dependencies": { - "estree-walker": { - "version": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==" - } } }, "rollup-plugin-commonjs": { @@ -3411,10 +3411,6 @@ "rollup-pluginutils": "^2.8.1" }, "dependencies": { - "estree-walker": { - "version": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==" - }, "resolve": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", @@ -3458,12 +3454,6 @@ "rollup-pluginutils": "^2.8.1", "serialize-javascript": "^1.7.0", "terser": "^4.1.0" - }, - "dependencies": { - "estree-walker": { - "version": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==" - } } }, "rollup-pluginutils": { @@ -3473,14 +3463,6 @@ "dev": true, "requires": { "estree-walker": "^0.6.1" - }, - "dependencies": { - "estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true - } } }, "safe-buffer": { diff --git a/site/package.json b/site/package.json index e6c31f6bc8..d08aa1f742 100644 --- a/site/package.json +++ b/site/package.json @@ -36,7 +36,7 @@ "@babel/runtime": "^7.6.0", "@sindresorhus/slugify": "^0.9.1", "@sveltejs/site-kit": "^1.1.4", - "@sveltejs/svelte-repl": "^0.1.9", + "@sveltejs/svelte-repl": "^0.1.13", "degit": "^2.1.4", "dotenv": "^8.1.0", "esm": "^3.2.25", diff --git a/site/rollup.config.js b/site/rollup.config.js index c713be33c1..9cb963a87d 100644 --- a/site/rollup.config.js +++ b/site/rollup.config.js @@ -13,6 +13,9 @@ const mode = process.env.NODE_ENV; const dev = mode === 'development'; const legacy = !!process.env.SAPPER_LEGACY_BUILD; +const onwarn = (warning, onwarn) => (warning.code === 'CIRCULAR_DEPENDENCY' && /[/\\]@sapper[/\\]/.test(warning.message)) || onwarn(warning); +const dedupe = importee => importee === 'svelte' || importee.startsWith('svelte/'); + export default { client: { input: config.client.input(), @@ -28,7 +31,10 @@ export default { hydratable: true, emitCss: true }), - resolve(), + resolve({ + browser: true, + dedupe + }), commonjs(), json(), @@ -53,6 +59,7 @@ export default { module: true }) ], + onwarn }, server: { @@ -67,7 +74,9 @@ export default { generate: 'ssr', dev }), - resolve(), + resolve({ + dedupe + }), commonjs(), json() ], @@ -78,6 +87,7 @@ export default { require('module').builtinModules || Object.keys(process.binding('natives')) ) ], + onwarn }, serviceworker: { diff --git a/site/static/logo-mask.svg b/site/static/logo-mask.svg deleted file mode 100644 index d7919a61ab..0000000000 --- a/site/static/logo-mask.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index 4d8abadf34..2d696ad306 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -27,7 +27,7 @@ import Slot from './nodes/Slot'; import { Node, ImportDeclaration, Identifier, Program, ExpressionStatement, AssignmentExpression, Literal } from 'estree'; import add_to_set from './utils/add_to_set'; import check_graph_for_cycles from './utils/check_graph_for_cycles'; -import { print, x } from 'code-red'; +import { print, x, b } from 'code-red'; interface ComponentOptions { namespace?: string; @@ -240,8 +240,7 @@ export default class Component { const { compile_options, name } = this; const { format = 'esm' } = compile_options; - // TODO reinstate banner (along with fragment marker comments) - const banner = `/* ${this.file ? `${this.file} ` : ``}generated by Svelte v${'__VERSION__'} */`; + const banner = `${this.file ? `${this.file} ` : ``}generated by Svelte v${'__VERSION__'}`; const program: any = { type: 'Program', body: result }; @@ -718,11 +717,13 @@ export default class Component { let scope = instance_scope; - const toRemove = []; + const to_remove = []; const remove = (parent, prop, index) => { - toRemove.unshift([parent, prop, index]); + to_remove.unshift([parent, prop, index]); }; + const to_insert = new Map(); + walk(content, { enter(node, parent, prop, index) { if (map.has(node)) { @@ -748,16 +749,41 @@ export default class Component { } component.warn_on_undefined_store_value_references(node, parent, scope); + + if (component.compile_options.dev && component.compile_options.loopGuardTimeout > 0) { + const to_insert_for_loop_protect = component.loop_protect(node, prop, index, component.compile_options.loopGuardTimeout); + if (to_insert_for_loop_protect) { + if (!Array.isArray(parent[prop])) { + parent[prop] = { + type: 'BlockStatement', + body: [to_insert_for_loop_protect.node, node], + }; + } else { + // can't insert directly, will screw up the index in the for-loop of estree-walker + if (!to_insert.has(parent)) { + to_insert.set(parent, []); + } + to_insert.get(parent).push(to_insert_for_loop_protect); + } + } + } }, leave(node) { if (map.has(node)) { scope = scope.parent; } + if (to_insert.has(node)) { + const nodes_to_insert = to_insert.get(node); + for (const { index, prop, node: node_to_insert } of nodes_to_insert.reverse()) { + node[prop].splice(index, 0, node_to_insert); + } + to_insert.delete(node); + } }, }); - for (const [parent, prop, index] of toRemove) { + for (const [parent, prop, index] of to_remove) { if (parent) { if (index !== null) { parent[prop].splice(index, 1); @@ -837,6 +863,32 @@ export default class Component { } } + loop_protect(node, prop, index, timeout) { + if (node.type === 'WhileStatement' || + node.type === 'ForStatement' || + node.type === 'DoWhileStatement') { + const guard = this.get_unique_name('guard'); + this.add_var({ + name: guard.name, + internal: true, + }); + + const before = b`const ${guard} = @loop_guard(${timeout})`; + const inside = b`${guard}();`; + + // wrap expression statement with BlockStatement + if (node.body.type !== 'BlockStatement') { + node.body = { + type: 'BlockStatement', + body: [node.body], + }; + } + node.body.body.push(inside[0]); + return { index, prop, node: before[0] }; + } + return null; + } + invalidate(name, value?) { const variable = this.var_lookup.get(name); diff --git a/src/compiler/compile/create_module.ts b/src/compiler/compile/create_module.ts index 76e68e758f..ce6443e899 100644 --- a/src/compiler/compile/create_module.ts +++ b/src/compiler/compile/create_module.ts @@ -44,7 +44,7 @@ function edit_source(source, sveltePath) { function esm( program: any, name: Identifier, - _banner: string, + banner: string, sveltePath: string, internal_path: string, helpers: Array<{ name: string; alias: Identifier }>, @@ -98,6 +98,8 @@ function esm( }; program.body = b` + /* ${banner} */ + ${import_declaration} ${internal_globals} ${imports} @@ -112,7 +114,7 @@ function esm( function cjs( program: any, name: Identifier, - _banner: string, + banner: string, sveltePath: string, internal_path: string, helpers: Array<{ name: string; alias: Identifier }>, @@ -188,6 +190,8 @@ function cjs( const exports = module_exports.map(x => b`exports.${{ type: 'Identifier', name: x.as }} = ${{ type: 'Identifier', name: x.name }};`); program.body = b` + /* ${banner} */ + "use strict"; ${internal_requires} ${internal_globals} diff --git a/src/compiler/compile/css/Selector.ts b/src/compiler/compile/css/Selector.ts index ab19ebd1e1..d99af7a110 100644 --- a/src/compiler/compile/css/Selector.ts +++ b/src/compiler/compile/css/Selector.ts @@ -3,6 +3,7 @@ import Stylesheet from './Stylesheet'; import { gather_possible_values, UNKNOWN } from './gather_possible_values'; import { CssNode } from './interfaces'; import Component from '../Component'; +import Element from '../nodes/Element'; enum BlockAppliesToNode { NotPossible, @@ -34,8 +35,8 @@ export default class Selector { this.used = this.blocks[0].global; } - apply(node: CssNode, stack: CssNode[]) { - const to_encapsulate: CssNode[] = []; + apply(node: Element, stack: Element[]) { + const to_encapsulate: any[] = []; apply_selector(this.local_blocks.slice(), node, stack.slice(), to_encapsulate); @@ -132,7 +133,7 @@ export default class Selector { } } -function apply_selector(blocks: Block[], node: CssNode, stack: CssNode[], to_encapsulate: any[]): boolean { +function apply_selector(blocks: Block[], node: Element, stack: Element[], to_encapsulate: any[]): boolean { const block = blocks.pop(); if (!block) return false; @@ -259,16 +260,84 @@ function attribute_matches(node: CssNode, name: string, expected_value: string, const attr = node.attributes.find((attr: CssNode) => attr.name === name); if (!attr) return false; if (attr.is_true) return operator === null; - if (attr.chunks.length > 1) return true; if (!expected_value) return true; - const value = attr.chunks[0]; - - if (!value) return false; - if (value.type === 'Text') return test_attribute(operator, expected_value, case_insensitive, value.data); + if (attr.chunks.length === 1) { + const value = attr.chunks[0]; + if (!value) return false; + if (value.type === 'Text') return test_attribute(operator, expected_value, case_insensitive, value.data); + } const possible_values = new Set(); - gather_possible_values(value.node, possible_values); + + let prev_values = []; + for (const chunk of attr.chunks) { + const current_possible_values = new Set(); + if (chunk.type === 'Text') { + current_possible_values.add(chunk.data); + } else { + gather_possible_values(chunk.node, current_possible_values); + } + + // impossible to find out all combinations + if (current_possible_values.has(UNKNOWN)) return true; + + if (prev_values.length > 0) { + const start_with_space = []; + const remaining = []; + current_possible_values.forEach((current_possible_value: string) => { + if (/^\s/.test(current_possible_value)) { + start_with_space.push(current_possible_value); + } else { + remaining.push(current_possible_value); + } + }); + + if (remaining.length > 0) { + if (start_with_space.length > 0) { + prev_values.forEach(prev_value => possible_values.add(prev_value)); + } + + const combined = []; + prev_values.forEach((prev_value: string) => { + remaining.forEach((value: string) => { + combined.push(prev_value + value); + }); + }); + prev_values = combined; + + start_with_space.forEach((value: string) => { + if (/\s$/.test(value)) { + possible_values.add(value); + } else { + prev_values.push(value); + } + }); + continue; + } else { + prev_values.forEach(prev_value => possible_values.add(prev_value)); + prev_values = []; + } + } + + current_possible_values.forEach((current_possible_value: string) => { + if (/\s$/.test(current_possible_value)) { + possible_values.add(current_possible_value); + } else { + prev_values.push(current_possible_value); + } + }); + if (prev_values.length < current_possible_values.size) { + prev_values.push(' '); + } + + if (prev_values.length > 20) { + // might grow exponentially, bail out + return true; + } + } + prev_values.forEach(prev_value => possible_values.add(prev_value)); + if (possible_values.has(UNKNOWN)) return true; for (const value of possible_values) { diff --git a/src/compiler/compile/index.ts b/src/compiler/compile/index.ts index a0c831723b..3772cd3772 100644 --- a/src/compiler/compile/index.ts +++ b/src/compiler/compile/index.ts @@ -25,13 +25,14 @@ const valid_options = [ 'customElement', 'tag', 'css', + 'loopGuardTimeout', 'preserveComments', 'preserveWhitespace', 'optimiseAst', ]; function validate_options(options: CompileOptions, warnings: Warning[]) { - const { name, filename } = options; + const { name, filename, loopGuardTimeout, dev } = options; Object.keys(options).forEach(key => { if (valid_options.indexOf(key) === -1) { @@ -56,6 +57,16 @@ function validate_options(options: CompileOptions, warnings: Warning[]) { toString: () => message, }); } + + if (loopGuardTimeout && !dev) { + const message = 'options.loopGuardTimeout is for options.dev = true only'; + warnings.push({ + code: `options-loop-guard-timeout`, + message, + filename, + toString: () => message, + }); + } } export default function compile(source: string, options: CompileOptions = {}) { diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index d353d20158..555c772f23 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -151,6 +151,10 @@ export default class Element extends Node { } } + // Binding relies on Attribute, defer its evaluation + const order = ['Binding']; // everything else is -1 + info.attributes.sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)); + info.attributes.forEach(node => { switch (node.type) { case 'Action': diff --git a/src/compiler/compile/nodes/EventHandler.ts b/src/compiler/compile/nodes/EventHandler.ts index 7faf5d15dd..19da3d9dd7 100644 --- a/src/compiler/compile/nodes/EventHandler.ts +++ b/src/compiler/compile/nodes/EventHandler.ts @@ -1,8 +1,6 @@ import Node from './shared/Node'; import Expression from './shared/Expression'; import Component from '../Component'; -import { b, x } from 'code-red'; -import Block from '../render_dom/Block'; import { sanitize } from '../../utils/names'; import { Identifier } from 'estree'; @@ -14,6 +12,7 @@ export default class EventHandler extends Node { handler_name: Identifier; uses_context = false; can_make_passive = false; + reassigned?: boolean; constructor(component: Component, parent, template_scope, info) { super(component, parent, template_scope, info); @@ -22,7 +21,7 @@ export default class EventHandler extends Node { this.modifiers = new Set(info.modifiers); if (info.expression) { - this.expression = new Expression(component, this, template_scope, info.expression, true); + this.expression = new Expression(component, this, template_scope, info.expression); this.uses_context = this.expression.uses_context; if (/FunctionExpression/.test(info.expression.type) && info.expression.params.length === 0) { @@ -42,34 +41,12 @@ export default class EventHandler extends Node { if (node && (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration' || node.type === 'ArrowFunctionExpression') && node.params.length === 0) { this.can_make_passive = true; } + + this.reassigned = component.var_lookup.get(info.expression.name).reassigned; } } } else { - const id = component.get_unique_name(`${sanitize(this.name)}_handler`); - - component.add_var({ - name: id.name, - internal: true, - referenced: true - }); - - component.partly_hoisted.push(b` - function ${id}(event) { - @bubble($$self, event); - } - `); - - this.handler_name = id; + this.handler_name = component.get_unique_name(`${sanitize(this.name)}_handler`); } } - - // TODO move this? it is specific to render-dom - render(block: Block) { - if (this.expression) { - return this.expression.manipulate(block); - } - - // this.component.add_reference(this.handler_name); - return x`#ctx.${this.handler_name}`; - } } diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts index b268954dd0..74822ef9be 100644 --- a/src/compiler/compile/render_dom/Block.ts +++ b/src/compiler/compile/render_dom/Block.ts @@ -203,13 +203,11 @@ export default class Block { } add_variable(id: Identifier, init?: Node) { - this.variables.forEach(v => { - if (v.id.name === id.name) { - throw new Error( - `Variable '${id.name}' already initialised with a different value` - ); - } - }); + if (this.variables.has(id.name)) { + throw new Error( + `Variable '${id.name}' already initialised with a different value` + ); + } this.variables.set(id.name, { id, init }); } @@ -268,7 +266,7 @@ export default class Block { : this.chunks.hydrate ); - properties.create = x`function create() { + properties.create = x`function #create() { ${this.chunks.create} ${hydrate} }`; @@ -278,7 +276,7 @@ export default class Block { if (this.chunks.claim.length === 0 && this.chunks.hydrate.length === 0) { properties.claim = noop; } else { - properties.claim = x`function claim(#nodes) { + properties.claim = x`function #claim(#nodes) { ${this.chunks.claim} ${this.renderer.options.hydratable && this.chunks.hydrate.length > 0 && b`this.h();`} }`; @@ -286,7 +284,7 @@ export default class Block { } if (this.renderer.options.hydratable && this.chunks.hydrate.length > 0) { - properties.hydrate = x`function hydrate() { + properties.hydrate = x`function #hydrate() { ${this.chunks.hydrate} }`; } @@ -294,7 +292,7 @@ export default class Block { if (this.chunks.mount.length === 0) { properties.mount = noop; } else { - properties.mount = x`function mount(#target, anchor) { + properties.mount = x`function #mount(#target, anchor) { ${this.chunks.mount} }`; } @@ -304,7 +302,7 @@ export default class Block { properties.update = noop; } else { const ctx = this.maintain_context ? x`#new_ctx` : x`#ctx`; - properties.update = x`function update(#changed, ${ctx}) { + properties.update = x`function #update(#changed, ${ctx}) { ${this.maintain_context && b`#ctx = ${ctx};`} ${this.chunks.update} }`; @@ -312,15 +310,15 @@ export default class Block { } if (this.has_animation) { - properties.measure = x`function measure() { + properties.measure = x`function #measure() { ${this.chunks.measure} }`; - properties.fix = x`function fix() { + properties.fix = x`function #fix() { ${this.chunks.fix} }`; - properties.animate = x`function animate() { + properties.animate = x`function #animate() { ${this.chunks.animate} }`; } @@ -329,7 +327,7 @@ export default class Block { if (this.chunks.intro.length === 0) { properties.intro = noop; } else { - properties.intro = x`function intro(#local) { + properties.intro = x`function #intro(#local) { ${this.has_outros && b`if (#current) return;`} ${this.chunks.intro} }`; @@ -338,7 +336,7 @@ export default class Block { if (this.chunks.outro.length === 0) { properties.outro = noop; } else { - properties.outro = x`function outro(#local) { + properties.outro = x`function #outro(#local) { ${this.chunks.outro} }`; } @@ -347,7 +345,7 @@ export default class Block { if (this.chunks.destroy.length === 0) { properties.destroy = noop; } else { - properties.destroy = x`function destroy(detaching) { + properties.destroy = x`function #destroy(detaching) { ${this.chunks.destroy} }`; } @@ -376,6 +374,8 @@ export default class Block { d: ${properties.destroy} }`; + const block = dev && this.get_unique_name('block'); + const body = b` ${Array.from(this.variables.values()).map(({ id, init }) => { return init @@ -387,9 +387,15 @@ export default class Block { ${dev ? b` - const block = ${return_value}; - @dispatch_dev("SvelteRegisterBlock", { block, id: ${this.name || 'create_fragment'}.name, type: "${this.type}", source: "${this.comment ? this.comment.replace(/"/g, '\\"') : ''}", ctx: #ctx }); - return block;` + const ${block} = ${return_value}; + @dispatch_dev("SvelteRegisterBlock", { + block: ${block}, + id: ${this.name || 'create_fragment'}.name, + type: "${this.type}", + source: "${this.comment ? this.comment.replace(/"/g, '\\"') : ''}", + ctx: #ctx + }); + return ${block};` : b` return ${return_value};` } @@ -398,21 +404,36 @@ export default class Block { return body; } + has_content() { + return this.renderer.options.dev || + this.first || + this.event_listeners.length > 0 || + this.chunks.intro.length > 0 || + this.chunks.outro.length > 0 || + this.chunks.create.length > 0 || + this.chunks.hydrate.length > 0 || + this.chunks.claim.length > 0 || + this.chunks.mount.length > 0 || + this.chunks.update.length > 0 || + this.chunks.destroy.length > 0 || + this.has_animation; + } + render() { const key = this.key && this.get_unique_name('key'); const args: any[] = [x`#ctx`]; - if (key) args.unshift(key); - // TODO include this.comment - // ${this.comment && `// ${escape(this.comment, { only_escape_at_symbol: true })}`} + const fn = b`function ${this.name}(${args}) { + ${this.get_contents(key)} + }`; - return b` - function ${this.name}(${args}) { - ${this.get_contents(key)} - } - `; + return this.comment + ? b` + // ${this.comment} + ${fn}` + : fn; } render_listeners(chunk: string = '') { diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index a50a86e2f3..cc0c7dfe31 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -241,11 +241,16 @@ export default function dom( args.push(x`$$props`, x`$$invalidate`); } - body.push(b` - function create_fragment(#ctx) { - ${block.get_contents()} - } + const has_create_fragment = block.has_content(); + if (has_create_fragment) { + body.push(b` + function create_fragment(#ctx) { + ${block.get_contents()} + } + `); + } + body.push(b` ${component.extract_javascript(component.ast.module)} ${component.fully_hoisted} @@ -364,7 +369,7 @@ export default function dom( unknown_props_check = b` const writable_props = [${writable_props.map(prop => x`'${prop.export_name}'`)}]; @_Object.keys($$props).forEach(key => { - if (!writable_props.includes(key) && !key.startsWith('$$')) @_console.warn(\`<${component.tag}> was created with unknown prop '\${key}'\`); + if (!~writable_props.indexOf(key) && key.slice(0, 2) !== '$$') @_console.warn(\`<${component.tag}> was created with unknown prop '\${key}'\`); }); `; } @@ -437,7 +442,7 @@ export default function dom( ${css.code && b`this.shadowRoot.innerHTML = \`\`;`} - @init(this, { target: this.shadowRoot }, ${definition}, create_fragment, ${not_equal}, ${prop_names}); + @init(this, { target: this.shadowRoot }, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_names}); ${dev_props_check} @@ -489,7 +494,7 @@ export default function dom( constructor(options) { super(${options.dev && `options`}); ${should_add_css && b`if (!@_document.getElementById("${component.stylesheet.id}-style")) ${add_css}();`} - @init(this, options, ${definition}, create_fragment, ${not_equal}, ${prop_names}); + @init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_names}); ${options.dev && b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`} ${dev_props_check} diff --git a/src/compiler/compile/render_dom/wrappers/Body.ts b/src/compiler/compile/render_dom/wrappers/Body.ts index dc1f561b1f..e16ebc25bd 100644 --- a/src/compiler/compile/render_dom/wrappers/Body.ts +++ b/src/compiler/compile/render_dom/wrappers/Body.ts @@ -3,21 +3,24 @@ import Wrapper from './shared/Wrapper'; import { b } from 'code-red'; import Body from '../../nodes/Body'; import { Identifier } from 'estree'; +import EventHandler from './Element/EventHandler'; export default class BodyWrapper extends Wrapper { node: Body; render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) { - this.node.handlers.forEach(handler => { - const snippet = handler.render(block); + this.node.handlers + .map(handler => new EventHandler(handler, this)) + .forEach(handler => { + const snippet = handler.get_snippet(block); - block.chunks.init.push(b` - @_document.body.addEventListener("${handler.name}", ${snippet}); - `); + block.chunks.init.push(b` + @_document.body.addEventListener("${handler.node.name}", ${snippet}); + `); - block.chunks.destroy.push(b` - @_document.body.removeEventListener("${handler.name}", ${snippet}); - `); - }); + block.chunks.destroy.push(b` + @_document.body.removeEventListener("${handler.node.name}", ${snippet}); + `); + }); } } diff --git a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts index d50a8238d9..db3cf7a662 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts @@ -7,7 +7,6 @@ import { b, x } from 'code-red'; import Expression from '../../../nodes/shared/Expression'; import Text from '../../../nodes/Text'; import { changed } from '../shared/changed'; -import { Literal } from 'estree'; export default class AttributeWrapper { node: Attribute; @@ -72,89 +71,81 @@ export default class AttributeWrapper { const is_legacy_input_type = element.renderer.component.compile_options.legacy && name === 'type' && this.parent.node.name === 'input'; const dependencies = this.node.get_dependencies(); - if (dependencies.length > 0) { - let value; - - // TODO some of this code is repeated in Tag.ts — would be good to - // DRY it out if that's possible without introducing crazy indirection - if (this.node.chunks.length === 1) { - // single {tag} — may be a non-string - value = (this.node.chunks[0] as Expression).manipulate(block); - } else { - value = this.node.name === 'class' - ? this.get_class_name_text() - : this.render_chunks().reduce((lhs, rhs) => x`${lhs} + ${rhs}`); - - // '{foo} {bar}' — treat as string concatenation - if (this.node.chunks[0].type !== 'Text') { - value = x`"" + ${value}`; - } - } + const value = this.get_value(block); - const is_select_value_attribute = - name === 'value' && element.node.name === 'select'; + const is_src = this.node.name === 'src'; // TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750 + const is_select_value_attribute = + name === 'value' && element.node.name === 'select'; - const should_cache = is_select_value_attribute; // TODO is this necessary? + const should_cache = is_src || this.node.should_cache() || is_select_value_attribute; // TODO is this necessary? - const last = should_cache && block.get_unique_name( - `${element.var.name}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value` - ); + const last = should_cache && block.get_unique_name( + `${element.var.name}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value` + ); - if (should_cache) block.add_variable(last); - - let updater; - const init = should_cache ? x`${last} = ${value}` : value; - - if (is_legacy_input_type) { - block.chunks.hydrate.push( - b`@set_input_type(${element.var}, ${init});` - ); - updater = b`@set_input_type(${element.var}, ${should_cache ? last : value});`; - } else if (is_select_value_attribute) { - // annoying special case - const is_multiple_select = element.node.get_static_attribute_value('multiple'); - const i = block.get_unique_name('i'); - const option = block.get_unique_name('option'); - - const if_statement = is_multiple_select - ? b` - ${option}.selected = ~${last}.indexOf(${option}.__value);` - : b` - if (${option}.__value === ${last}) { - ${option}.selected = true; - ${{ type: 'BreakStatement' }}; - }`; // TODO the BreakStatement is gross, but it's unsyntactic otherwise... - - updater = b` - for (var ${i} = 0; ${i} < ${element.var}.options.length; ${i} += 1) { - var ${option} = ${element.var}.options[${i}]; - - ${if_statement} - } - `; - - block.chunks.mount.push(b` - ${last} = ${value}; - ${updater} - `); - } else if (property_name) { - block.chunks.hydrate.push( - b`${element.var}.${property_name} = ${init};` - ); - updater = block.renderer.options.dev - ? b`@prop_dev(${element.var}, "${property_name}", ${should_cache ? last : value});` - : b`${element.var}.${property_name} = ${should_cache ? last : value};`; - } else { - block.chunks.hydrate.push( - b`${method}(${element.var}, "${name}", ${init});` - ); - updater = b`${method}(${element.var}, "${name}", ${should_cache ? last : value});`; - } + if (should_cache) block.add_variable(last); + + let updater; + const init = should_cache ? x`${last} = ${value}` : value; + + if (is_legacy_input_type) { + block.chunks.hydrate.push( + b`@set_input_type(${element.var}, ${init});` + ); + updater = b`@set_input_type(${element.var}, ${should_cache ? last : value});`; + } else if (is_select_value_attribute) { + // annoying special case + const is_multiple_select = element.node.get_static_attribute_value('multiple'); + const i = block.get_unique_name('i'); + const option = block.get_unique_name('option'); + + const if_statement = is_multiple_select + ? b` + ${option}.selected = ~${last}.indexOf(${option}.__value);` + : b` + if (${option}.__value === ${last}) { + ${option}.selected = true; + ${{ type: 'BreakStatement' }}; + }`; // TODO the BreakStatement is gross, but it's unsyntactic otherwise... + + updater = b` + for (var ${i} = 0; ${i} < ${element.var}.options.length; ${i} += 1) { + var ${option} = ${element.var}.options[${i}]; + + ${if_statement} + } + `; + + block.chunks.mount.push(b` + ${last} = ${value}; + ${updater} + `); + } else if (is_src) { + block.chunks.hydrate.push( + b`if (${element.var}.src !== ${init}) ${method}(${element.var}, "${name}", ${last});` + ); + updater = b`${method}(${element.var}, "${name}", ${should_cache ? last : value});`; + } else if (property_name) { + block.chunks.hydrate.push( + b`${element.var}.${property_name} = ${init};` + ); + updater = block.renderer.options.dev + ? b`@prop_dev(${element.var}, "${property_name}", ${should_cache ? last : value});` + : b`${element.var}.${property_name} = ${should_cache ? last : value};`; + } else { + block.chunks.hydrate.push( + b`${method}(${element.var}, "${name}", ${init});` + ); + updater = b`${method}(${element.var}, "${name}", ${should_cache ? last : value});`; + } + if (dependencies.length > 0) { let condition = changed(dependencies); if (should_cache) { - condition = x`${condition} && (${last} !== (${last} = ${value}))`; + condition = is_src + ? x`${condition} && (${element.var}.src !== (${last} = ${value}))` + : x`${condition} && (${last} !== (${last} = ${value}))`; } if (block.has_outros) { @@ -165,23 +156,11 @@ export default class AttributeWrapper { if (${condition}) { ${updater} }`); - } else { - const value = this.node.get_value(block); - - const statement = ( - is_legacy_input_type - ? b`@set_input_type(${element.var}, ${value});` - : property_name - ? b`${element.var}.${property_name} = ${value};` - : b`${method}(${element.var}, "${name}", ${value.type === 'Literal' && (value as Literal).value === true ? x`""` : value});` - ); - - block.chunks.hydrate.push(statement); + } - // special case – autofocus. has to be handled in a bit of a weird way - if (this.node.is_true && name === 'autofocus') { - block.autofocus = element.var; - } + // special case – autofocus. has to be handled in a bit of a weird way + if (this.node.is_true && name === 'autofocus') { + block.autofocus = element.var; } if (is_indirectly_bound_value) { @@ -199,6 +178,36 @@ export default class AttributeWrapper { return metadata; } + get_value(block) { + if (this.node.is_true) { + const metadata = this.get_metadata(); + if (metadata && boolean_attribute.has(metadata.property_name.toLowerCase())) { + return x`true`; + } + return x`""`; + } + if (this.node.chunks.length === 0) return x`""`; + + // TODO some of this code is repeated in Tag.ts — would be good to + // DRY it out if that's possible without introducing crazy indirection + if (this.node.chunks.length === 1) { + return this.node.chunks[0].type === 'Text' + ? string_literal((this.node.chunks[0] as Text).data) + : (this.node.chunks[0] as Expression).manipulate(block); + } + + let value = this.node.name === 'class' + ? this.get_class_name_text() + : this.render_chunks().reduce((lhs, rhs) => x`${lhs} + ${rhs}`); + + // '{foo} {bar}' — treat as string concatenation + if (this.node.chunks[0].type !== 'Text') { + value = x`"" + ${value}`; + } + + return value; + } + get_class_name_text() { const scoped_css = this.node.chunks.some((chunk: Text) => chunk.synthetic); const rendered = this.render_chunks(); @@ -292,3 +301,32 @@ Object.keys(attribute_lookup).forEach(name => { const metadata = attribute_lookup[name]; if (!metadata.property_name) metadata.property_name = name; }); + +// source: https://html.spec.whatwg.org/multipage/indices.html +const boolean_attribute = new Set([ + 'allowfullscreen', + 'allowpaymentrequest', + 'async', + 'autofocus', + 'autoplay', + 'checked', + 'controls', + 'default', + 'defer', + 'disabled', + 'formnovalidate', + 'hidden', + 'ismap', + 'itemscope', + 'loop', + 'multiple', + 'muted', + 'nomodule', + 'novalidate', + 'open', + 'playsinline', + 'readonly', + 'required', + 'reversed', + 'selected' +]); \ No newline at end of file diff --git a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts new file mode 100644 index 0000000000..37089e7493 --- /dev/null +++ b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts @@ -0,0 +1,69 @@ +import EventHandler from '../../../nodes/EventHandler'; +import Wrapper from '../shared/Wrapper'; +import Block from '../../Block'; +import { b, x, p } from 'code-red'; + +const TRUE = x`true`; +const FALSE = x`false`; + +export default class EventHandlerWrapper { + node: EventHandler; + parent: Wrapper; + + constructor(node: EventHandler, parent: Wrapper) { + this.node = node; + this.parent = parent; + + if (!node.expression) { + this.parent.renderer.component.add_var({ + name: node.handler_name.name, + internal: true, + referenced: true, + }); + + this.parent.renderer.component.partly_hoisted.push(b` + function ${node.handler_name.name}(event) { + @bubble($$self, event); + } + `); + } + } + + get_snippet(block) { + const snippet = this.node.expression ? this.node.expression.manipulate(block) : x`#ctx.${this.node.handler_name}`; + + if (this.node.reassigned) { + block.maintain_context = true; + return x`function () { ${snippet}.apply(this, arguments); }`; + } + return snippet; + } + + render(block: Block, target: string) { + let snippet = this.get_snippet(block); + + if (this.node.modifiers.has('preventDefault')) snippet = x`@prevent_default(${snippet})`; + if (this.node.modifiers.has('stopPropagation')) snippet = x`@stop_propagation(${snippet})`; + if (this.node.modifiers.has('self')) snippet = x`@self(${snippet})`; + + const args = []; + + const opts = ['passive', 'once', 'capture'].filter(mod => this.node.modifiers.has(mod)); + if (opts.length) { + args.push((opts.length === 1 && opts[0] === 'capture') + ? TRUE + : x`{ ${opts.map(opt => p`${opt}: true`)} }`); + } else if (block.renderer.options.dev) { + args.push(FALSE); + } + + if (block.renderer.options.dev) { + args.push(this.node.modifiers.has('stopPropagation') ? TRUE : FALSE); + args.push(this.node.modifiers.has('preventDefault') ? TRUE : FALSE); + } + + block.event_listeners.push( + x`@listen(${target}, "${this.node.name}", ${snippet}, ${args})` + ); + } +} diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 245b55ae49..b36660072e 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -24,6 +24,7 @@ import bind_this from '../shared/bind_this'; import { changed } from '../shared/changed'; import { is_head } from '../shared/is_head'; import { Identifier } from 'estree'; +import EventHandler from './EventHandler'; const events = [ { @@ -113,12 +114,14 @@ export default class ElementWrapper extends Wrapper { fragment: FragmentWrapper; attributes: AttributeWrapper[]; bindings: Binding[]; + event_handlers: EventHandler[]; class_dependencies: string[]; slot_block: Block; select_binding_dependencies?: Set; var: any; + void: boolean; constructor( renderer: Renderer, @@ -134,6 +137,8 @@ export default class ElementWrapper extends Wrapper { name: node.name.replace(/[^a-zA-Z0-9_$]/g, '_') }; + this.void = is_void(node.name); + this.class_dependencies = []; this.attributes = this.node.attributes.map(attribute => { @@ -194,6 +199,8 @@ export default class ElementWrapper extends Wrapper { // e.g.