diff --git a/CHANGELOG.md b/CHANGELOG.md index 63f4f0fa2a..fd590c71aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,18 @@ ## Unreleased +* In SSR mode, do not automatically declare variables for reactive assignments to member expressions ([#5247](https://github.com/sveltejs/svelte/issues/5247)) + +## 3.24.1 + +* Prevent duplicate invalidation with certain two-way component bindings ([#3180](https://github.com/sveltejs/svelte/issues/3180), [#5117](https://github.com/sveltejs/svelte/issues/5117), [#5144](https://github.com/sveltejs/svelte/issues/5144)) * Fix reactivity when passing `$$props` to a `` ([#3364](https://github.com/sveltejs/svelte/issues/3364)) +* Fix transitions on `{#each}` `{:else}` ([#4970](https://github.com/sveltejs/svelte/issues/4970)) +* Fix unneeded invalidation of `$$props` and `$$restProps` ([#4993](https://github.com/sveltejs/svelte/issues/4993), [#5118](https://github.com/sveltejs/svelte/issues/5118)) +* Provide better compiler error message when mismatched tags are due to autoclosing of tags ([#5049](https://github.com/sveltejs/svelte/issues/5049)) +* Add `a11y-label-has-associated-control` warning ([#5074](https://github.com/sveltejs/svelte/pull/5074)) +* Add `a11y-media-has-caption` warning ([#5075](https://github.com/sveltejs/svelte/pull/5075)) +* Fix `bind:group` when using contextual reference ([#5174](https://github.com/sveltejs/svelte/issues/5174)) ## 3.24.0 diff --git a/package-lock.json b/package-lock.json index 68f251c605..0110595c76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "3.24.0", + "version": "3.24.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2527,9 +2527,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, "lodash.sortby": { diff --git a/package.json b/package.json index 6927dc3d0d..e0de8670ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "3.24.0", + "version": "3.24.1", "description": "Cybernetically enhanced web apps", "module": "index.mjs", "main": "index", diff --git a/site/README.md b/site/README.md index 64ae22f9a8..892b8e1249 100644 --- a/site/README.md +++ b/site/README.md @@ -53,6 +53,26 @@ To build the website, run `npm run sapper`. The output can be found in `__sapper Tests can be run using `npm run test`. + +## Linking `@sveltejs/site-kit` and `@sveltejs/site-repl` + +This site depends on `@sveltejs/site-kit`, a collection of styles, components and icons used in common by *.svelte.dev websites, and `@sveltejs/site-repl`. + +In order to work on features that depend on those packages, you need to [link](https://docs.npmjs.com/cli/link) their repositories: + +- `cd ` +- `git clone https://github.com/sveltejs/site-kit` +- `git clone https://github.com/sveltejs/svelte-repl` +- `cd /site-kit` +- `npm link` +- `cd /svelte-repl` +- `npm link` +- `cd /site` +- `npm link @sveltejs/site-kit` +- `npm link @sveltejs/svelte-repl` + + + ## Translating the API docs Anchors are automatically generated using headings in the documentation and by default (for the english language) they are latinised to make sure the URL is always conforming to RFC3986. diff --git a/site/content/blog/2016-11-26-frameworks-without-the-framework.md b/site/content/blog/2016-11-26-frameworks-without-the-framework.md index 4efac90b52..76c8b90ab3 100644 --- a/site/content/blog/2016-11-26-frameworks-without-the-framework.md +++ b/site/content/blog/2016-11-26-frameworks-without-the-framework.md @@ -30,7 +30,7 @@ Given that, what if the framework *didn't actually run in the browser*? What if, Svelte is a new framework that does exactly that. You write your components using HTML, CSS and JavaScript (plus a few extra bits you can [learn in under 5 minutes](https://v2.svelte.dev/guide)), and during your build process Svelte compiles them into tiny standalone JavaScript modules. By statically analysing the component template, we can make sure that the browser does as little work as possible. -The [Svelte implementation of TodoMVC](http://svelte-todomvc.surge.sh/) weighs 3.6kb zipped. For comparison, React plus ReactDOM *without any app code* weighs about 45kb zipped. It takes about 10x as long for the browser just to evaluate React as it does for Svelte to be up and running with an interactive TodoMVC. +The [Svelte implementation of TodoMVC](https://svelte-todomvc.surge.sh/) weighs 3.6kb zipped. For comparison, React plus ReactDOM *without any app code* weighs about 45kb zipped. It takes about 10x as long for the browser just to evaluate React as it does for Svelte to be up and running with an interactive TodoMVC. And once your app *is* up and running, according to [js-framework-benchmark](https://github.com/krausest/js-framework-benchmark) **Svelte is fast as heck**. It's faster than React. It's faster than Vue. It's faster than Angular, or Ember, or Ractive, or Preact, or Riot, or Mithril. It's competitive with Inferno, which is probably the fastest UI framework in the world, for now, because [Dominic Gannaway](https://twitter.com/trueadm) is a wizard. (Svelte is slower at removing elements. We're [working on it](https://github.com/sveltejs/svelte/issues/26).) diff --git a/site/content/blog/2017-12-31-sapper-towards-the-ideal-web-app-framework.md b/site/content/blog/2017-12-31-sapper-towards-the-ideal-web-app-framework.md index 67e732c230..60609609f7 100644 --- a/site/content/blog/2017-12-31-sapper-towards-the-ideal-web-app-framework.md +++ b/site/content/blog/2017-12-31-sapper-towards-the-ideal-web-app-framework.md @@ -5,7 +5,7 @@ author: Rich Harris authorURL: https://twitter.com/Rich_Harris --- -> Quickstart for the impatient: [the Sapper docs](https://sapper.svelte.technology), and the [starter template](https://github.com/sveltejs/sapper-template) +> Quickstart for the impatient: [the Sapper docs](https://sapper.svelte.dev), and the [starter template](https://github.com/sveltejs/sapper-template) If you had to list the characteristics of the perfect Node.js web application framework, you'd probably come up with something like this: @@ -47,9 +47,9 @@ What happens if we use the new model as a starting point? ## Introducing Sapper - + -[Sapper](https://sapper.svelte.technology) is the answer to that question. **Sapper is a Next.js-style framework that aims to meet the eleven criteria at the top of this article while dramatically reducing the amount of code that gets sent to the browser.** It's implemented as Express-compatible middleware, meaning it's easy to understand and customise. +[Sapper](https://sapper.svelte.dev) is the answer to that question. **Sapper is a Next.js-style framework that aims to meet the eleven criteria at the top of this article while dramatically reducing the amount of code that gets sent to the browser.** It's implemented as Express-compatible middleware, meaning it's easy to understand and customise. The same 'hello world' app that took 204kb with React and Next weighs just 7kb with Sapper. That number is likely to fall further in the future as we explore the space of optimisation possibilities, such as not shipping any JavaScript *at all* for pages that aren't interactive, beyond the tiny Sapper runtime that handles client-side routing. diff --git a/site/content/blog/2019-04-16-svelte-for-new-developers.md b/site/content/blog/2019-04-16-svelte-for-new-developers.md index fd7a88e6f1..921e699842 100644 --- a/site/content/blog/2019-04-16-svelte-for-new-developers.md +++ b/site/content/blog/2019-04-16-svelte-for-new-developers.md @@ -51,7 +51,7 @@ Once installed, you'll have access to three new commands: ## Installing a text editor -To write code, you need a good editor. The most popular choice is [Visual Studio Code](https://code.visualstudio.com/) or VSCode, and justifiably so — it's well-designed and fully-featured, and has a wealth of extensions ([including one for Svelte](https://marketplace.visualstudio.com/items?itemName=JamesBirtles.svelte-vscode), which provides syntax highlighting and diagnostic messages when you're writing components). +To write code, you need a good editor. The most popular choice is [Visual Studio Code](https://code.visualstudio.com/) or VSCode, and justifiably so — it's well-designed and fully-featured, and has a wealth of extensions ([including one for Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), which provides syntax highlighting and diagnostic messages when you're writing components). ## Creating a project diff --git a/site/content/blog/2020-07-17-svelte-and-typescript.md b/site/content/blog/2020-07-17-svelte-and-typescript.md new file mode 100644 index 0000000000..9c228a17da --- /dev/null +++ b/site/content/blog/2020-07-17-svelte-and-typescript.md @@ -0,0 +1,141 @@ +--- +title: Svelte <3 TypeScript +description: Typernetically enhanced web apps +author: Orta Therox +authorURL: https://twitter.com/orta +--- + +It's been by far the most requested feature for a while, and it's finally here: Svelte officially supports TypeScript. + +We think it'll give you a much nicer development experience — one that also scales beautifully to larger Svelte code bases — regardless of whether you use TypeScript or JavaScript. + +
+ Screenshot of TypeScript in Svelte +
Image of TypeScript + Svelte in VS Code (theme is Kary Pro.)
+
+ + +## Try it now + +You can start a new Svelte TypeScript project using the [normal template](https://github.com/sveltejs/template) and by running `node scripts/setupTypeScript.js` before you do anything else: + +```bash +npx degit sveltejs/template svelte-typescript-app +cd svelte-typescript-app +node scripts/setupTypeScript.js +``` + +If you're a VS Code user, make sure you're using the (new) [official extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), which replaces the popular extension by James Birtles. +Later in this blog post, we'll detail the individual steps involved in using TypeScript in an existing Svelte project. + +## What does it mean to support TypeScript in Svelte? + +TypeScript support in Svelte has been possible for a long time, but you had to mix a lot of disparate tools together and each project ran independently. Today, nearly all of these tools live under the Svelte organization and are maintained by a set of people who take responsibility over the whole pipeline and have common goals. + +A week before COVID was declared a pandemic, [I pitched a consolidation](https://github.com/sveltejs/svelte/issues/4518) of the best Svelte tools and ideas from similar dev-ecosystems and provided a set of steps to get first class TypeScript support. Since then, many people have pitched in and written the code to get us there. + +When we say that Svelte now supports TypeScript, we mean a few different things: + +* You can use TypeScript inside your ` - -

Todos

{#each todos as todo} -
+
{/each} diff --git a/site/content/faq/100-im-new-to-svelte.md b/site/content/faq/100-im-new-to-svelte.md index 18e4b6742f..0f1df9cff5 100644 --- a/site/content/faq/100-im-new-to-svelte.md +++ b/site/content/faq/100-im-new-to-svelte.md @@ -2,6 +2,6 @@ question: I'm new to Svelte. Where should I start? --- -We think the best way to get started is playing through the interactive [Tutorial](https://svelte.dev/tutorial). Each step there is mainly focused on one specific aspect and is easy to follow. You'll be editing and running real Svelte components right in your browser. +We think the best way to get started is playing through the interactive [Tutorial](tutorial). Each step there is mainly focused on one specific aspect and is easy to follow. You'll be editing and running real Svelte components right in your browser. Five to ten minutes should be enough to get you up and running. An hour and a half should get you through the entire tutorial. \ No newline at end of file diff --git a/site/content/faq/500-what-about-typescript-support.md b/site/content/faq/500-what-about-typescript-support.md index adfd63764e..98838bab3d 100644 --- a/site/content/faq/500-what-about-typescript-support.md +++ b/site/content/faq/500-what-about-typescript-support.md @@ -10,3 +10,11 @@ To declare the type of a reactive variable in a Svelte template, you should use let x: number; $: x = count + 1; ``` + +To import a type or interface make sure to use [TypeScript's `type` modifier](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export): + +``` +import type { SomeInterface } from './SomeFile'; +``` + +You must use the `type` modifier because `svelte-preprocess` doesn't know whether an import is a type or a value — it only transpiles one file at a time without knowledge of the other files and therefore can't safely erase imports which only contain types without this modifier present. diff --git a/site/content/tutorial/16-special-elements/01-svelte-self/text.md b/site/content/tutorial/16-special-elements/01-svelte-self/text.md index 773dded83e..c7946f6c6d 100644 --- a/site/content/tutorial/16-special-elements/01-svelte-self/text.md +++ b/site/content/tutorial/16-special-elements/01-svelte-self/text.md @@ -14,7 +14,7 @@ It's useful for things like this folder tree view, where folders can contain *ot {/if} ``` -...but that's impossible, because a file can't import itself. Instead, we use ``: +...but that's impossible, because a module can't import itself. Instead, we use ``: ```html {#if file.type === 'folder'} @@ -22,4 +22,4 @@ It's useful for things like this folder tree view, where folders can contain *ot {:else} {/if} -``` \ No newline at end of file +``` diff --git a/site/package-lock.json b/site/package-lock.json index b3d12343f1..caeb17a9bc 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -1281,9 +1281,9 @@ } }, "@sveltejs/site-kit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@sveltejs/site-kit/-/site-kit-1.2.0.tgz", - "integrity": "sha512-C7puq+1so3fKPPZAnQJQlKfyCG6FsnSSFSS2LRIhWD8VK2FL5j8Eq7DIKSxUvWbGw1AsxnzO3dIHJWPB7fwjKg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@sveltejs/site-kit/-/site-kit-1.2.4.tgz", + "integrity": "sha512-W+/PthWX4R8UvKr+IyWIITGoY3cl/54ePZr3dU9ZlyP9r/weEvvKDBvjmW8tAKQFRfbxyySmXUxEGBoPhF8XAA==", "dev": true, "requires": { "@sindresorhus/slugify": "^0.9.1", @@ -2404,9 +2404,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, "lodash.deburr": { diff --git a/site/package.json b/site/package.json index 30075c9f37..fd5be85976 100644 --- a/site/package.json +++ b/site/package.json @@ -36,7 +36,7 @@ "@babel/preset-env": "^7.6.0", "@babel/runtime": "^7.6.0", "@sindresorhus/slugify": "^0.9.1", - "@sveltejs/site-kit": "^1.2.0", + "@sveltejs/site-kit": "^1.2.4", "@sveltejs/svelte-repl": "^0.2.0", "degit": "^2.1.4", "dotenv": "^8.1.0", diff --git a/site/src/routes/blog/[slug].svelte b/site/src/routes/blog/[slug].svelte index d5fd6c0676..b97b5ef136 100644 --- a/site/src/routes/blog/[slug].svelte +++ b/site/src/routes/blog/[slug].svelte @@ -137,6 +137,15 @@ top: calc((var(--h3) - 24px) / 2); } + .post :global(a) { + padding: 0; + transition: none; + } + + .post :global(a):not(:hover) { + border: none; + } + @media (max-width: 768px) { .post :global(.anchor) { transform: scale(0.6); diff --git a/site/src/routes/blog/rss.xml.js b/site/src/routes/blog/rss.xml.js index 974806ad44..544474b274 100644 --- a/site/src/routes/blog/rss.xml.js +++ b/site/src/routes/blog/rss.xml.js @@ -8,6 +8,18 @@ function formatPubdate(str) { return `${d} ${months[+m]} ${y} 12:00 +0000`; } +function escapeHTML(html) { + const chars = { + '"' : 'quot', + "'": '#39', + '&': 'amp', + '<' : 'lt', + '>' : 'gt' + }; + + return html.replace(/["'&<>]/g, c => `&${chars[c]};`); +} + const rss = ` @@ -23,9 +35,9 @@ const rss = ` ${get_posts().filter(post => !post.metadata.draft).map(post => ` - ${post.metadata.title} + ${escapeHTML(post.metadata.title)} https://svelte.dev/blog/${post.slug} - ${post.metadata.description} + ${escapeHTML(post.metadata.description)} ${formatPubdate(post.metadata.pubdate)} `).join('')} diff --git a/site/src/routes/repl/[id]/_components/AppControls/index.svelte b/site/src/routes/repl/[id]/_components/AppControls/index.svelte index 10639e91c0..1d4e6b3f03 100644 --- a/site/src/routes/repl/[id]/_components/AppControls/index.svelte +++ b/site/src/routes/repl/[id]/_components/AppControls/index.svelte @@ -227,6 +227,7 @@ export default app;` }); padding: .6rem var(--side-nav); background-color: var(--second); color: white; + white-space: nowrap; } .icon { diff --git a/site/src/utils/highlight.js b/site/src/utils/highlight.js index d9e4fe5ec8..124af2e6be 100644 --- a/site/src/utils/highlight.js +++ b/site/src/utils/highlight.js @@ -1,6 +1,7 @@ import { langs } from '@sveltejs/site-kit/utils/markdown.js'; import PrismJS from 'prismjs'; import 'prismjs/components/prism-bash'; +import 'prismjs/components/prism-diff'; import 'prism-svelte'; export function highlight(source, lang) { diff --git a/site/static/media/svelte-ts.png b/site/static/media/svelte-ts.png new file mode 100644 index 0000000000..ed562822c3 Binary files /dev/null and b/site/static/media/svelte-ts.png differ diff --git a/src/compiler/compile/nodes/Binding.ts b/src/compiler/compile/nodes/Binding.ts index bcb07280b0..c561e72b99 100644 --- a/src/compiler/compile/nodes/Binding.ts +++ b/src/compiler/compile/nodes/Binding.ts @@ -41,7 +41,8 @@ export default class Binding extends Node { this.raw_expression = JSON.parse(JSON.stringify(info.expression)); const { name } = get_object(this.expression.node); - this.is_contextual = scope.names.has(name); + + this.is_contextual = Array.from(this.expression.references).some(name => scope.names.has(name)); // make sure we track this as a mutable ref if (scope.is_let(name)) { @@ -49,7 +50,7 @@ export default class Binding extends Node { code: 'invalid-binding', message: 'Cannot bind to a variable declared with the let: directive' }); - } else if (this.is_contextual) { + } else if (scope.names.has(name)) { if (scope.is_await(name)) { component.error(this, { code: 'invalid-binding', diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 7a70e603a7..145866cf4f 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -61,6 +61,17 @@ const a11y_no_onchange = new Set([ 'option' ]); +const a11y_labelable = new Set([ + "button", + "input", + "keygen", + "meter", + "output", + "progress", + "select", + "textarea" +]); + const invisible_elements = new Set(['meta', 'html', 'script', 'style']); const valid_modifiers = new Set([ @@ -507,6 +518,35 @@ export default class Element extends Node { } } + if (this.name === 'label') { + const has_input_child = this.children.some(i => (i instanceof Element && a11y_labelable.has(i.name) )); + if (!attribute_map.has('for') && !has_input_child) { + component.warn(this, { + code: `a11y-label-has-associated-control`, + message: `A11y: A form label must be associated with a control.` + }); + } + } + + if (this.is_media_node()) { + if (attribute_map.has('muted')) { + return; + } + + let has_caption; + const track = this.children.find((i: Element) => i.name === 'track'); + if (track) { + has_caption = track.attributes.find(a => a.name === 'kind' && a.get_static_value() === 'captions'); + } + + if (!has_caption) { + component.warn(this, { + code: `a11y-media-has-caption`, + message: `A11y: Media elements must have a ` + }); + } + } + if (a11y_no_onchange.has(this.name)) { if (handlers_map.has('change') && !handlers_map.has('blur')) { component.warn(this, { diff --git a/src/compiler/compile/nodes/shared/Expression.ts b/src/compiler/compile/nodes/shared/Expression.ts index 8bfc2bffce..bbbc1b2f2d 100644 --- a/src/compiler/compile/nodes/shared/Expression.ts +++ b/src/compiler/compile/nodes/shared/Expression.ts @@ -24,7 +24,7 @@ export default class Expression { component: Component; owner: Owner; node: any; - references: Set; + references: Set = new Set(); dependencies: Set = new Set(); contextual_dependencies: Set = new Set(); @@ -50,7 +50,7 @@ export default class Expression { this.template_scope = template_scope; this.owner = owner; - const { dependencies, contextual_dependencies } = this; + const { dependencies, contextual_dependencies, references } = this; let { map, scope } = create_scopes(info); this.scope = scope; @@ -75,6 +75,7 @@ export default class Expression { if (is_reference(node, parent)) { const { name, nodes } = flatten_reference(node); + references.add(name); if (scope.has(name)) return; diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index 3b5001d483..7d0dce8315 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -420,7 +420,7 @@ export default function dom( ${component.partly_hoisted} - ${set && b`$$self.$set = ${set};`} + ${set && b`$$self.$$set = ${set};`} ${capture_state && b`$$self.$capture_state = ${capture_state};`} diff --git a/src/compiler/compile/render_dom/wrappers/EachBlock.ts b/src/compiler/compile/render_dom/wrappers/EachBlock.ts index bd981a0603..126b114487 100644 --- a/src/compiler/compile/render_dom/wrappers/EachBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/EachBlock.ts @@ -298,6 +298,19 @@ export default class EachBlockWrapper extends Wrapper { } `); + const has_transitions = !!(this.else.block.has_intro_method || this.else.block.has_outro_method); + + const destroy_block_else = this.else.block.has_outro_method + ? b` + @group_outros(); + @transition_out(${each_block_else}, 1, 1, () => { + ${each_block_else} = null; + }); + @check_outros();` + : b` + ${each_block_else}.d(1); + ${each_block_else} = null;`; + if (this.else.block.has_update_method) { this.updates.push(b` if (!${this.vars.data_length} && ${each_block_else}) { @@ -305,22 +318,22 @@ export default class EachBlockWrapper extends Wrapper { } else if (!${this.vars.data_length}) { ${each_block_else} = ${this.else.block.name}(#ctx); ${each_block_else}.c(); + ${has_transitions && b`@transition_in(${each_block_else}, 1);`} ${each_block_else}.m(${update_mount_node}, ${update_anchor_node}); } else if (${each_block_else}) { - ${each_block_else}.d(1); - ${each_block_else} = null; + ${destroy_block_else}; } `); } else { this.updates.push(b` if (${this.vars.data_length}) { if (${each_block_else}) { - ${each_block_else}.d(1); - ${each_block_else} = null; + ${destroy_block_else}; } } else if (!${each_block_else}) { ${each_block_else} = ${this.else.block.name}(#ctx); ${each_block_else}.c(); + ${has_transitions && b`@transition_in(${each_block_else}, 1);`} ${each_block_else}.m(${update_mount_node}, ${update_anchor_node}); } `); diff --git a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts index 271b3de1e1..814f365a84 100644 --- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts +++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts @@ -468,7 +468,7 @@ export default class InlineComponentWrapper extends Wrapper { ${name} = null; } } else if (${switch_value}) { - ${updates.length && b`${name}.$set(${name_changes});`} + ${updates.length > 0 && b`${name}.$set(${name_changes});`} } `); diff --git a/src/compiler/compile/render_dom/wrappers/shared/mark_each_block_bindings.ts b/src/compiler/compile/render_dom/wrappers/shared/mark_each_block_bindings.ts index 490d52bd9d..884d4b88b8 100644 --- a/src/compiler/compile/render_dom/wrappers/shared/mark_each_block_bindings.ts +++ b/src/compiler/compile/render_dom/wrappers/shared/mark_each_block_bindings.ts @@ -2,7 +2,6 @@ import EachBlock from "../../../nodes/EachBlock"; import InlineComponentWrapper from "../InlineComponent"; import ElementWrapper from "../Element"; import Binding from "../../../nodes/Binding"; -import get_object from "../../../utils/get_object"; export default function mark_each_block_bindings( parent: ElementWrapper | InlineComponentWrapper, @@ -10,9 +9,12 @@ export default function mark_each_block_bindings( ) { // we need to ensure that the each block creates a context including // the list and the index, if they're not otherwise referenced - const object = get_object(binding.expression.node).name; - const each_block = parent.node.scope.get_owner(object); - (each_block as EachBlock).has_binding = true; + binding.expression.references.forEach(name => { + const each_block = parent.node.scope.get_owner(name); + if (each_block) { + (each_block as EachBlock).has_binding = true; + } + }); if (binding.name === "group") { // for ``, we make sure that all the each blocks creates context with `index` diff --git a/src/compiler/compile/render_ssr/index.ts b/src/compiler/compile/render_ssr/index.ts index c87fe3bdd9..a72b8b35ae 100644 --- a/src/compiler/compile/render_ssr/index.ts +++ b/src/compiler/compile/render_ssr/index.ts @@ -5,8 +5,7 @@ import { string_literal } from '../utils/stringify'; import Renderer from './Renderer'; import { INode as TemplateNode } from '../nodes/interfaces'; // TODO import Text from '../nodes/Text'; -import { extract_names } from '../utils/scope'; -import { LabeledStatement, Statement, ExpressionStatement, AssignmentExpression, Node } from 'estree'; +import { LabeledStatement, Statement, Node } from 'estree'; export default function ssr( component: Component, @@ -72,37 +71,17 @@ export default function ssr( }) : []; + const injected = Array.from(component.injected_reactive_declaration_vars).filter(name => { + const variable = component.var_lookup.get(name); + return variable.injected; + }); + const reactive_declarations = component.reactive_declarations.map(d => { const body: Statement = (d.node as LabeledStatement).body; let statement = b`${body}`; - if (d.declaration) { - const declared = extract_names(d.declaration); - const injected = declared.filter(name => { - return name[0] !== '$' && component.var_lookup.get(name).injected; - }); - - const self_dependencies = injected.filter(name => d.dependencies.has(name)); - - if (injected.length) { - // in some cases we need to do `let foo; [expression]`, in - // others we can do `let [expression]` - const separate = ( - self_dependencies.length > 0 || - declared.length > injected.length - ); - - const { left, right } = (body as ExpressionStatement).expression as AssignmentExpression; - - statement = separate - ? b` - ${injected.map(name => b`let ${name};`)} - ${statement}` - : b` - let ${left} = ${right}`; - } - } else { // TODO do not add label if it's not referenced + if (!d.declaration) { // TODO do not add label if it's not referenced statement = b`$: { ${statement} }`; } @@ -119,6 +98,8 @@ export default function ssr( ${reactive_store_values} + ${injected.map(name => b`let ${name};`)} + ${reactive_declarations} $$rendered = ${literal}; @@ -129,6 +110,8 @@ export default function ssr( : b` ${reactive_store_values} + ${injected.map(name => b`let ${name};`)} + ${reactive_declarations} return ${literal};`; diff --git a/src/compiler/parse/index.ts b/src/compiler/parse/index.ts index a809eeebeb..52795c02a9 100644 --- a/src/compiler/parse/index.ts +++ b/src/compiler/parse/index.ts @@ -8,6 +8,12 @@ import error from '../utils/error'; type ParserState = (parser: Parser) => (ParserState | void); +interface LastAutoClosedTag { + tag: string; + reason: string; + depth: number; +} + export class Parser { readonly template: string; readonly filename?: string; @@ -20,6 +26,7 @@ export class Parser { css: Style[] = []; js: Script[] = []; meta_tags = {}; + last_auto_closed_tag?: LastAutoClosedTag; constructor(template: string, options: ParserOptions) { if (typeof template !== 'string') { diff --git a/src/compiler/parse/state/tag.ts b/src/compiler/parse/state/tag.ts index ce8a0a9aa7..311bf852e2 100644 --- a/src/compiler/parse/state/tag.ts +++ b/src/compiler/parse/state/tag.ts @@ -133,11 +133,15 @@ export default function tag(parser: Parser) { // close any elements that don't have their own closing tags, e.g.

while (parent.name !== name) { - if (parent.type !== 'Element') + if (parent.type !== 'Element') { + const message = parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name + ? ` attempted to close <${name}> that was already automatically closed by <${parser.last_auto_closed_tag.reason}>` + : ` attempted to close an element that was not open`; parser.error({ code: `invalid-closing-tag`, - message: ` attempted to close an element that was not open` + message }, start); + } parent.end = start; parser.stack.pop(); @@ -148,10 +152,19 @@ export default function tag(parser: Parser) { parent.end = parser.index; parser.stack.pop(); + if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) { + parser.last_auto_closed_tag = null; + } + return; } else if (closing_tag_omitted(parent.name, name)) { parent.end = start; parser.stack.pop(); + parser.last_auto_closed_tag = { + tag: parent.name, + reason: name, + depth: parser.stack.length, + }; } const unique_names: Set = new Set(); diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index c0f6facdd2..459a78031a 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -1,6 +1,6 @@ import { add_render_callback, flush, schedule_update, dirty_components } from './scheduler'; import { current_component, set_current_component } from './lifecycle'; -import { blank_object, is_function, run, run_all, noop } from './utils'; +import { blank_object, is_empty, is_function, run, run_all, noop } from './utils'; import { children, detach } from './dom'; import { transition_in } from './transitions'; @@ -33,6 +33,7 @@ interface T$$ { context: Map; on_mount: any[]; on_destroy: any[]; + skip_bound: boolean; } export function bind(component, name, callback) { @@ -120,7 +121,8 @@ export function init(component, options, instance, create_fragment, not_equal, p // everything else callbacks: blank_object(), - dirty + dirty, + skip_bound: false }; let ready = false; @@ -129,7 +131,7 @@ export function init(component, options, instance, create_fragment, not_equal, p ? instance(component, prop_values, (i, ret, ...rest) => { const value = rest.length ? rest[0] : ret; if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) { - if ($$.bound[i]) $$.bound[i](value); + if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value); if (ready) make_dirty(component, i); } return ret; @@ -166,6 +168,7 @@ export let SvelteElement; if (typeof HTMLElement === 'function') { SvelteElement = class extends HTMLElement { $$: T$$; + $$set?: ($$props: any) => void; constructor() { super(); this.attachShadow({ mode: 'open' }); @@ -199,14 +202,19 @@ if (typeof HTMLElement === 'function') { }; } - $set() { - // overridden by instance, if it has props + $set($$props) { + if (this.$$set && !is_empty($$props)) { + this.$$.skip_bound = true; + this.$$set($$props); + this.$$.skip_bound = false; + } } }; } export class SvelteComponent { $$: T$$; + $$set?: ($$props: any) => void; $destroy() { destroy_component(this, 1); @@ -223,7 +231,11 @@ export class SvelteComponent { }; } - $set() { - // overridden by instance, if it has props + $set($$props) { + if (this.$$set && !is_empty($$props)) { + this.$$.skip_bound = true; + this.$$set($$props); + this.$$.skip_bound = false; + } } } diff --git a/src/runtime/internal/lifecycle.ts b/src/runtime/internal/lifecycle.ts index a8e37e9632..34d52672a2 100644 --- a/src/runtime/internal/lifecycle.ts +++ b/src/runtime/internal/lifecycle.ts @@ -44,11 +44,11 @@ export function createEventDispatcher() { }; } -export function setContext(key, context) { +export function setContext(key, context: T) { get_current_component().$$.context.set(key, context); } -export function getContext(key) { +export function getContext(key): T { return get_current_component().$$.context.get(key); } diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts index d752c9de9d..3fd0a2b701 100644 --- a/src/runtime/internal/utils.ts +++ b/src/runtime/internal/utils.ts @@ -42,6 +42,10 @@ export function not_equal(a, b) { return a != a ? b == b : a !== b; } +export function is_empty(obj) { + return Object.keys(obj).length === 0; +} + export function validate_store(store, name) { if (store != null && typeof store.subscribe !== 'function') { throw new Error(`'${name}' is not a store with a 'subscribe' method`); diff --git a/test/css/samples/omit-scoping-attribute-attribute-selector/expected.html b/test/css/samples/omit-scoping-attribute-attribute-selector/expected.html index 52175f98b6..f585d6bcdd 100644 --- a/test/css/samples/omit-scoping-attribute-attribute-selector/expected.html +++ b/test/css/samples/omit-scoping-attribute-attribute-selector/expected.html @@ -1,2 +1,2 @@ -
-
\ No newline at end of file +
+
diff --git a/test/css/samples/omit-scoping-attribute-attribute-selector/input.svelte b/test/css/samples/omit-scoping-attribute-attribute-selector/input.svelte index 6f4549ead8..af06836d29 100644 --- a/test/css/samples/omit-scoping-attribute-attribute-selector/input.svelte +++ b/test/css/samples/omit-scoping-attribute-attribute-selector/input.svelte @@ -1,10 +1,10 @@
- - + +
\ No newline at end of file + diff --git a/test/js/samples/action-custom-event-handler/expected.js b/test/js/samples/action-custom-event-handler/expected.js index cac2f61b44..51656290d6 100644 --- a/test/js/samples/action-custom-event-handler/expected.js +++ b/test/js/samples/action-custom-event-handler/expected.js @@ -55,7 +55,7 @@ function instance($$self, $$props, $$invalidate) { let { bar } = $$props; const foo_function = () => handleFoo(bar); - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("bar" in $$props) $$invalidate(0, bar = $$props.bar); }; diff --git a/test/js/samples/bind-open/expected.js b/test/js/samples/bind-open/expected.js index 30387d505d..56ff302845 100644 --- a/test/js/samples/bind-open/expected.js +++ b/test/js/samples/bind-open/expected.js @@ -52,7 +52,7 @@ function instance($$self, $$props, $$invalidate) { $$invalidate(0, open); } - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("open" in $$props) $$invalidate(0, open = $$props.open); }; diff --git a/test/js/samples/bind-width-height/expected.js b/test/js/samples/bind-width-height/expected.js index 4848704c4b..f23c20b683 100644 --- a/test/js/samples/bind-width-height/expected.js +++ b/test/js/samples/bind-width-height/expected.js @@ -46,7 +46,7 @@ function instance($$self, $$props, $$invalidate) { $$invalidate(1, h); } - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("w" in $$props) $$invalidate(0, w = $$props.w); if ("h" in $$props) $$invalidate(1, h = $$props.h); }; diff --git a/test/js/samples/bindings-readonly-order/expected.js b/test/js/samples/bindings-readonly-order/expected.js index 0e845c65b8..78a71dcd84 100644 --- a/test/js/samples/bindings-readonly-order/expected.js +++ b/test/js/samples/bindings-readonly-order/expected.js @@ -68,7 +68,7 @@ function instance($$self, $$props, $$invalidate) { $$invalidate(0, files); } - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("files" in $$props) $$invalidate(0, files = $$props.files); }; diff --git a/test/js/samples/capture-inject-state/expected.js b/test/js/samples/capture-inject-state/expected.js index cd719ac5d2..6aa93b9c5a 100644 --- a/test/js/samples/capture-inject-state/expected.js +++ b/test/js/samples/capture-inject-state/expected.js @@ -118,7 +118,7 @@ function instance($$self, $$props, $$invalidate) { let { $$slots = {}, $$scope } = $$props; validate_slots("Component", $$slots, []); - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("prop" in $$props) $$subscribe_prop($$invalidate(0, prop = $$props.prop)); if ("alias" in $$props) $$invalidate(1, realName = $$props.alias); }; diff --git a/test/js/samples/collapses-text-around-comments/expected.js b/test/js/samples/collapses-text-around-comments/expected.js index 6fef0f9490..67335ce246 100644 --- a/test/js/samples/collapses-text-around-comments/expected.js +++ b/test/js/samples/collapses-text-around-comments/expected.js @@ -48,7 +48,7 @@ function create_fragment(ctx) { function instance($$self, $$props, $$invalidate) { let { foo = 42 } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("foo" in $$props) $$invalidate(0, foo = $$props.foo); }; diff --git a/test/js/samples/computed-collapsed-if/expected.js b/test/js/samples/computed-collapsed-if/expected.js index 8e5964f8a6..3e70d6a7ae 100644 --- a/test/js/samples/computed-collapsed-if/expected.js +++ b/test/js/samples/computed-collapsed-if/expected.js @@ -12,7 +12,7 @@ function instance($$self, $$props, $$invalidate) { return x * 3; } - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("x" in $$props) $$invalidate(0, x = $$props.x); }; diff --git a/test/js/samples/data-attribute/expected.js b/test/js/samples/data-attribute/expected.js index 49ad2f2626..8c30e6f6db 100644 --- a/test/js/samples/data-attribute/expected.js +++ b/test/js/samples/data-attribute/expected.js @@ -47,7 +47,7 @@ function create_fragment(ctx) { function instance($$self, $$props, $$invalidate) { let { bar } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("bar" in $$props) $$invalidate(0, bar = $$props.bar); }; diff --git a/test/js/samples/debug-empty/expected.js b/test/js/samples/debug-empty/expected.js index dd142adb26..6781e5333c 100644 --- a/test/js/samples/debug-empty/expected.js +++ b/test/js/samples/debug-empty/expected.js @@ -79,7 +79,7 @@ function instance($$self, $$props, $$invalidate) { let { $$slots = {}, $$scope } = $$props; validate_slots("Component", $$slots, []); - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("name" in $$props) $$invalidate(0, name = $$props.name); }; diff --git a/test/js/samples/debug-foo-bar-baz-things/expected.js b/test/js/samples/debug-foo-bar-baz-things/expected.js index 977702b99f..087d2e399d 100644 --- a/test/js/samples/debug-foo-bar-baz-things/expected.js +++ b/test/js/samples/debug-foo-bar-baz-things/expected.js @@ -183,7 +183,7 @@ function instance($$self, $$props, $$invalidate) { let { $$slots = {}, $$scope } = $$props; validate_slots("Component", $$slots, []); - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("things" in $$props) $$invalidate(0, things = $$props.things); if ("foo" in $$props) $$invalidate(1, foo = $$props.foo); if ("bar" in $$props) $$invalidate(2, bar = $$props.bar); diff --git a/test/js/samples/debug-foo/expected.js b/test/js/samples/debug-foo/expected.js index fe62ff77bf..9f12bfb807 100644 --- a/test/js/samples/debug-foo/expected.js +++ b/test/js/samples/debug-foo/expected.js @@ -175,7 +175,7 @@ function instance($$self, $$props, $$invalidate) { let { $$slots = {}, $$scope } = $$props; validate_slots("Component", $$slots, []); - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("things" in $$props) $$invalidate(0, things = $$props.things); if ("foo" in $$props) $$invalidate(1, foo = $$props.foo); }; diff --git a/test/js/samples/deconflict-builtins/expected.js b/test/js/samples/deconflict-builtins/expected.js index fb98844ef7..6bc60194aa 100644 --- a/test/js/samples/deconflict-builtins/expected.js +++ b/test/js/samples/deconflict-builtins/expected.js @@ -104,7 +104,7 @@ function create_fragment(ctx) { function instance($$self, $$props, $$invalidate) { let { createElement } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("createElement" in $$props) $$invalidate(0, createElement = $$props.createElement); }; diff --git a/test/js/samples/deconflict-globals/expected.js b/test/js/samples/deconflict-globals/expected.js index 7e83c21f0e..7168eba6a5 100644 --- a/test/js/samples/deconflict-globals/expected.js +++ b/test/js/samples/deconflict-globals/expected.js @@ -10,7 +10,7 @@ function instance($$self, $$props, $$invalidate) { alert(JSON.stringify(data())); }); - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("foo" in $$props) $$invalidate(0, foo = $$props.foo); }; diff --git a/test/js/samples/dev-warning-missing-data-computed/expected.js b/test/js/samples/dev-warning-missing-data-computed/expected.js index 0a50e2cd97..fd34778f8d 100644 --- a/test/js/samples/dev-warning-missing-data-computed/expected.js +++ b/test/js/samples/dev-warning-missing-data-computed/expected.js @@ -76,7 +76,7 @@ function instance($$self, $$props, $$invalidate) { let { $$slots = {}, $$scope } = $$props; validate_slots("Component", $$slots, []); - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("foo" in $$props) $$invalidate(0, foo = $$props.foo); }; diff --git a/test/js/samples/each-block-array-literal/expected.js b/test/js/samples/each-block-array-literal/expected.js index 10d835cf78..fe51ac5bc3 100644 --- a/test/js/samples/each-block-array-literal/expected.js +++ b/test/js/samples/each-block-array-literal/expected.js @@ -106,7 +106,7 @@ function instance($$self, $$props, $$invalidate) { let { d } = $$props; let { e } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("a" in $$props) $$invalidate(0, a = $$props.a); if ("b" in $$props) $$invalidate(1, b = $$props.b); if ("c" in $$props) $$invalidate(2, c = $$props.c); diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js index f4f9df0de9..63bc1d8607 100644 --- a/test/js/samples/each-block-changed-check/expected.js +++ b/test/js/samples/each-block-changed-check/expected.js @@ -152,7 +152,7 @@ function instance($$self, $$props, $$invalidate) { let { time } = $$props; let { foo } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("comments" in $$props) $$invalidate(0, comments = $$props.comments); if ("elapsed" in $$props) $$invalidate(1, elapsed = $$props.elapsed); if ("time" in $$props) $$invalidate(2, time = $$props.time); diff --git a/test/js/samples/each-block-keyed-animated/expected.js b/test/js/samples/each-block-keyed-animated/expected.js index 7fb81c27a2..46ef63ee7f 100644 --- a/test/js/samples/each-block-keyed-animated/expected.js +++ b/test/js/samples/each-block-keyed-animated/expected.js @@ -128,7 +128,7 @@ function foo(node, animation, params) { function instance($$self, $$props, $$invalidate) { let { things } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("things" in $$props) $$invalidate(0, things = $$props.things); }; diff --git a/test/js/samples/each-block-keyed/expected.js b/test/js/samples/each-block-keyed/expected.js index ad8c074e99..71853cf295 100644 --- a/test/js/samples/each-block-keyed/expected.js +++ b/test/js/samples/each-block-keyed/expected.js @@ -97,7 +97,7 @@ function create_fragment(ctx) { function instance($$self, $$props, $$invalidate) { let { things } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("things" in $$props) $$invalidate(0, things = $$props.things); }; diff --git a/test/js/samples/if-block-no-update/expected.js b/test/js/samples/if-block-no-update/expected.js index f225c221bf..c67b33fa85 100644 --- a/test/js/samples/if-block-no-update/expected.js +++ b/test/js/samples/if-block-no-update/expected.js @@ -88,7 +88,7 @@ function create_fragment(ctx) { function instance($$self, $$props, $$invalidate) { let { foo } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("foo" in $$props) $$invalidate(0, foo = $$props.foo); }; diff --git a/test/js/samples/if-block-simple/expected.js b/test/js/samples/if-block-simple/expected.js index eb6c8e8949..4cdd73cddb 100644 --- a/test/js/samples/if-block-simple/expected.js +++ b/test/js/samples/if-block-simple/expected.js @@ -66,7 +66,7 @@ function create_fragment(ctx) { function instance($$self, $$props, $$invalidate) { let { foo } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("foo" in $$props) $$invalidate(0, foo = $$props.foo); }; diff --git a/test/js/samples/inline-style-optimized-multiple/expected.js b/test/js/samples/inline-style-optimized-multiple/expected.js index 84a38abd7b..0a9d0a1e8e 100644 --- a/test/js/samples/inline-style-optimized-multiple/expected.js +++ b/test/js/samples/inline-style-optimized-multiple/expected.js @@ -44,7 +44,7 @@ function instance($$self, $$props, $$invalidate) { let { x } = $$props; let { y } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("color" in $$props) $$invalidate(0, color = $$props.color); if ("x" in $$props) $$invalidate(1, x = $$props.x); if ("y" in $$props) $$invalidate(2, y = $$props.y); diff --git a/test/js/samples/inline-style-optimized-url/expected.js b/test/js/samples/inline-style-optimized-url/expected.js index 77870348a5..0debb03585 100644 --- a/test/js/samples/inline-style-optimized-url/expected.js +++ b/test/js/samples/inline-style-optimized-url/expected.js @@ -37,7 +37,7 @@ function create_fragment(ctx) { function instance($$self, $$props, $$invalidate) { let { data } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("data" in $$props) $$invalidate(0, data = $$props.data); }; diff --git a/test/js/samples/inline-style-optimized/expected.js b/test/js/samples/inline-style-optimized/expected.js index 5bef284f09..b7db0f1cf3 100644 --- a/test/js/samples/inline-style-optimized/expected.js +++ b/test/js/samples/inline-style-optimized/expected.js @@ -37,7 +37,7 @@ function create_fragment(ctx) { function instance($$self, $$props, $$invalidate) { let { color } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("color" in $$props) $$invalidate(0, color = $$props.color); }; diff --git a/test/js/samples/inline-style-unoptimized/expected.js b/test/js/samples/inline-style-unoptimized/expected.js index fdff685ead..0688f14b9b 100644 --- a/test/js/samples/inline-style-unoptimized/expected.js +++ b/test/js/samples/inline-style-unoptimized/expected.js @@ -54,7 +54,7 @@ function instance($$self, $$props, $$invalidate) { let { key } = $$props; let { value } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("style" in $$props) $$invalidate(0, style = $$props.style); if ("key" in $$props) $$invalidate(1, key = $$props.key); if ("value" in $$props) $$invalidate(2, value = $$props.value); diff --git a/test/js/samples/input-files/expected.js b/test/js/samples/input-files/expected.js index 0069c2e5f8..8adc7443f5 100644 --- a/test/js/samples/input-files/expected.js +++ b/test/js/samples/input-files/expected.js @@ -49,7 +49,7 @@ function instance($$self, $$props, $$invalidate) { $$invalidate(0, files); } - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("files" in $$props) $$invalidate(0, files = $$props.files); }; diff --git a/test/js/samples/input-range/expected.js b/test/js/samples/input-range/expected.js index 770baa29ed..a855ca3653 100644 --- a/test/js/samples/input-range/expected.js +++ b/test/js/samples/input-range/expected.js @@ -60,7 +60,7 @@ function instance($$self, $$props, $$invalidate) { $$invalidate(0, value); } - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("value" in $$props) $$invalidate(0, value = $$props.value); }; diff --git a/test/js/samples/input-without-blowback-guard/expected.js b/test/js/samples/input-without-blowback-guard/expected.js index f19f74dc1e..6c5b215623 100644 --- a/test/js/samples/input-without-blowback-guard/expected.js +++ b/test/js/samples/input-without-blowback-guard/expected.js @@ -53,7 +53,7 @@ function instance($$self, $$props, $$invalidate) { $$invalidate(0, foo); } - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("foo" in $$props) $$invalidate(0, foo = $$props.foo); }; diff --git a/test/js/samples/media-bindings/expected.js b/test/js/samples/media-bindings/expected.js index bcbc0647b8..867d4a7dad 100644 --- a/test/js/samples/media-bindings/expected.js +++ b/test/js/samples/media-bindings/expected.js @@ -173,7 +173,7 @@ function instance($$self, $$props, $$invalidate) { $$invalidate(10, ended); } - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("buffered" in $$props) $$invalidate(0, buffered = $$props.buffered); if ("seekable" in $$props) $$invalidate(1, seekable = $$props.seekable); if ("played" in $$props) $$invalidate(2, played = $$props.played); diff --git a/test/js/samples/optional-chaining/expected.js b/test/js/samples/optional-chaining/expected.js index a28dc129aa..8aa94796c1 100644 --- a/test/js/samples/optional-chaining/expected.js +++ b/test/js/samples/optional-chaining/expected.js @@ -167,7 +167,7 @@ function instance($$self, $$props, $$invalidate) { let { f } = $$props; let Component; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("a" in $$props) $$invalidate(0, a = $$props.a); if ("b" in $$props) $$invalidate(1, b = $$props.b); if ("c" in $$props) $$invalidate(2, c = $$props.c); diff --git a/test/js/samples/reactive-values-non-topologically-ordered/expected.js b/test/js/samples/reactive-values-non-topologically-ordered/expected.js index 3d266f10ac..15290496d5 100644 --- a/test/js/samples/reactive-values-non-topologically-ordered/expected.js +++ b/test/js/samples/reactive-values-non-topologically-ordered/expected.js @@ -6,7 +6,7 @@ function instance($$self, $$props, $$invalidate) { let a; let b; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("x" in $$props) $$invalidate(0, x = $$props.x); }; diff --git a/test/js/samples/reactive-values-non-writable-dependencies/expected.js b/test/js/samples/reactive-values-non-writable-dependencies/expected.js index 38bd356d85..5196a770d9 100644 --- a/test/js/samples/reactive-values-non-writable-dependencies/expected.js +++ b/test/js/samples/reactive-values-non-writable-dependencies/expected.js @@ -5,7 +5,7 @@ function instance($$self, $$props, $$invalidate) { let { a = 1 } = $$props; let { b = 2 } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("a" in $$props) $$invalidate(0, a = $$props.a); if ("b" in $$props) $$invalidate(1, b = $$props.b); }; diff --git a/test/js/samples/select-dynamic-value/expected.js b/test/js/samples/select-dynamic-value/expected.js index aa4e5004fd..8777cd2600 100644 --- a/test/js/samples/select-dynamic-value/expected.js +++ b/test/js/samples/select-dynamic-value/expected.js @@ -50,7 +50,7 @@ function create_fragment(ctx) { function instance($$self, $$props, $$invalidate) { let { current } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("current" in $$props) $$invalidate(0, current = $$props.current); }; diff --git a/test/js/samples/src-attribute-check/expected.js b/test/js/samples/src-attribute-check/expected.js index e03b3a6ba7..93638edfb4 100644 --- a/test/js/samples/src-attribute-check/expected.js +++ b/test/js/samples/src-attribute-check/expected.js @@ -67,7 +67,7 @@ function instance($$self, $$props, $$invalidate) { let { url } = $$props; let { slug } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("url" in $$props) $$invalidate(0, url = $$props.url); if ("slug" in $$props) $$invalidate(1, slug = $$props.slug); }; diff --git a/test/js/samples/title/expected.js b/test/js/samples/title/expected.js index d4e7e1a584..b10f569759 100644 --- a/test/js/samples/title/expected.js +++ b/test/js/samples/title/expected.js @@ -22,7 +22,7 @@ function create_fragment(ctx) { function instance($$self, $$props, $$invalidate) { let { custom } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("custom" in $$props) $$invalidate(0, custom = $$props.custom); }; diff --git a/test/js/samples/transition-local/expected.js b/test/js/samples/transition-local/expected.js index 25a03f026f..ea3d9db3d7 100644 --- a/test/js/samples/transition-local/expected.js +++ b/test/js/samples/transition-local/expected.js @@ -124,7 +124,7 @@ function instance($$self, $$props, $$invalidate) { let { x } = $$props; let { y } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("x" in $$props) $$invalidate(0, x = $$props.x); if ("y" in $$props) $$invalidate(1, y = $$props.y); }; diff --git a/test/js/samples/transition-repeated-outro/expected.js b/test/js/samples/transition-repeated-outro/expected.js index 1f76a93666..12483ab91a 100644 --- a/test/js/samples/transition-repeated-outro/expected.js +++ b/test/js/samples/transition-repeated-outro/expected.js @@ -102,7 +102,7 @@ function create_fragment(ctx) { function instance($$self, $$props, $$invalidate) { let { num = 1 } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("num" in $$props) $$invalidate(0, num = $$props.num); }; diff --git a/test/js/samples/use-elements-as-anchors/expected.js b/test/js/samples/use-elements-as-anchors/expected.js index 5be8808edb..d07411518e 100644 --- a/test/js/samples/use-elements-as-anchors/expected.js +++ b/test/js/samples/use-elements-as-anchors/expected.js @@ -243,7 +243,7 @@ function instance($$self, $$props, $$invalidate) { let { d } = $$props; let { e } = $$props; - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("a" in $$props) $$invalidate(0, a = $$props.a); if ("b" in $$props) $$invalidate(1, b = $$props.b); if ("c" in $$props) $$invalidate(2, c = $$props.c); diff --git a/test/js/samples/video-bindings/expected.js b/test/js/samples/video-bindings/expected.js index d3920ef8c2..8afa670bbb 100644 --- a/test/js/samples/video-bindings/expected.js +++ b/test/js/samples/video-bindings/expected.js @@ -93,7 +93,7 @@ function instance($$self, $$props, $$invalidate) { $$invalidate(3, offsetWidth); } - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("currentTime" in $$props) $$invalidate(0, currentTime = $$props.currentTime); if ("videoHeight" in $$props) $$invalidate(1, videoHeight = $$props.videoHeight); if ("videoWidth" in $$props) $$invalidate(2, videoWidth = $$props.videoWidth); diff --git a/test/js/samples/window-binding-scroll/expected.js b/test/js/samples/window-binding-scroll/expected.js index 45d992c721..09a4d3737d 100644 --- a/test/js/samples/window-binding-scroll/expected.js +++ b/test/js/samples/window-binding-scroll/expected.js @@ -78,7 +78,7 @@ function instance($$self, $$props, $$invalidate) { $$invalidate(0, y = window.pageYOffset) } - $$self.$set = $$props => { + $$self.$$set = $$props => { if ("y" in $$props) $$invalidate(0, y = $$props.y); }; diff --git a/test/parser/samples/error-unmatched-closing-tag-autoclose-2/error.json b/test/parser/samples/error-unmatched-closing-tag-autoclose-2/error.json new file mode 100644 index 0000000000..d24296bd96 --- /dev/null +++ b/test/parser/samples/error-unmatched-closing-tag-autoclose-2/error.json @@ -0,0 +1,10 @@ +{ + "code": "invalid-closing-tag", + "message": "

attempted to close an element that was not open", + "pos": 38, + "start": { + "character": 38, + "column": 0, + "line": 5 + } +} diff --git a/test/parser/samples/error-unmatched-closing-tag-autoclose-2/input.svelte b/test/parser/samples/error-unmatched-closing-tag-autoclose-2/input.svelte new file mode 100644 index 0000000000..5182577921 --- /dev/null +++ b/test/parser/samples/error-unmatched-closing-tag-autoclose-2/input.svelte @@ -0,0 +1,5 @@ +
+

+

pre tag
+
+

\ No newline at end of file diff --git a/test/parser/samples/error-unmatched-closing-tag-autoclose/error.json b/test/parser/samples/error-unmatched-closing-tag-autoclose/error.json new file mode 100644 index 0000000000..e6532d747e --- /dev/null +++ b/test/parser/samples/error-unmatched-closing-tag-autoclose/error.json @@ -0,0 +1,10 @@ +{ + "code": "invalid-closing-tag", + "message": "

attempted to close

that was already automatically closed by

",
+	"pos": 24,
+	"start": {
+		"character": 24,
+		"column": 0,
+		"line": 3
+	}
+}
diff --git a/test/parser/samples/error-unmatched-closing-tag-autoclose/input.svelte b/test/parser/samples/error-unmatched-closing-tag-autoclose/input.svelte
new file mode 100644
index 0000000000..0bfd609736
--- /dev/null
+++ b/test/parser/samples/error-unmatched-closing-tag-autoclose/input.svelte
@@ -0,0 +1,3 @@
+

+

pre tag
+

\ No newline at end of file diff --git a/test/runtime/samples/binding-input-group-each-6/_config.js b/test/runtime/samples/binding-input-group-each-6/_config.js new file mode 100644 index 0000000000..9eb251bf5d --- /dev/null +++ b/test/runtime/samples/binding-input-group-each-6/_config.js @@ -0,0 +1,87 @@ +export default { + html: ` + + + +

+ + + +

+ + + +

+ `, + + async test({ assert, component, target, window }) { + const inputs = target.querySelectorAll('input'); + assert.equal(inputs[0].checked, false); + assert.equal(inputs[1].checked, false); + assert.equal(inputs[2].checked, false); + + assert.equal(inputs[3].checked, false); + assert.equal(inputs[4].checked, false); + assert.equal(inputs[5].checked, false); + + assert.equal(inputs[6].checked, false); + assert.equal(inputs[7].checked, false); + assert.equal(inputs[8].checked, false); + + const event = new window.Event('change'); + + inputs[2].checked = true; + await inputs[2].dispatchEvent(event); + + assert.htmlEqual(target.innerHTML, ` + + + +

z

+ + + +

+ + + +

+ `); + + inputs[4].checked = true; + await inputs[4].dispatchEvent(event); + + assert.htmlEqual(target.innerHTML, ` + + + +

z

+ + + +

y

+ + + +

+ `); + + inputs[5].checked = true; + await inputs[5].dispatchEvent(event); + + assert.htmlEqual(target.innerHTML, ` + + + +

z

+ + + +

y, z

+ + + +

+ `); + } +}; diff --git a/test/runtime/samples/binding-input-group-each-6/main.svelte b/test/runtime/samples/binding-input-group-each-6/main.svelte new file mode 100644 index 0000000000..85be939e8a --- /dev/null +++ b/test/runtime/samples/binding-input-group-each-6/main.svelte @@ -0,0 +1,22 @@ + + +{#each Object.keys(list) as key} + {#each values as value} + + {/each} +

{list[key].join(', ')}

+{/each} diff --git a/test/runtime/samples/component-binding-store/Input.svelte b/test/runtime/samples/component-binding-store/Input.svelte new file mode 100644 index 0000000000..792104bec8 --- /dev/null +++ b/test/runtime/samples/component-binding-store/Input.svelte @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/test/runtime/samples/component-binding-store/_config.js b/test/runtime/samples/component-binding-store/_config.js new file mode 100644 index 0000000000..4dec41459f --- /dev/null +++ b/test/runtime/samples/component-binding-store/_config.js @@ -0,0 +1,61 @@ +export default { + html: ` + + +
+ `, + + async test({ assert, component, target, window }) { + let count = 0; + component.callback = () => { + count++; + }; + + const [input1, input2] = target.querySelectorAll("input"); + + input1.value = "1"; + await input1.dispatchEvent(new window.Event("input")); + + assert.htmlEqual( + target.innerHTML, + ` + + +
1
+ ` + ); + assert.equal(input1.value, "1"); + assert.equal(input2.value, "1"); + assert.equal(count, 1); + + input2.value = "123"; + await input2.dispatchEvent(new window.Event("input")); + + assert.htmlEqual( + target.innerHTML, + ` + + +
123
+ ` + ); + assert.equal(input1.value, "123"); + assert.equal(input2.value, "123"); + assert.equal(count, 2); + + input1.value = "456"; + await input1.dispatchEvent(new window.Event("input")); + + assert.htmlEqual( + target.innerHTML, + ` + + +
456
+ ` + ); + assert.equal(input1.value, "456"); + assert.equal(input2.value, "456"); + assert.equal(count, 3); + }, +}; diff --git a/test/runtime/samples/component-binding-store/main.svelte b/test/runtime/samples/component-binding-store/main.svelte new file mode 100644 index 0000000000..dba08e5276 --- /dev/null +++ b/test/runtime/samples/component-binding-store/main.svelte @@ -0,0 +1,18 @@ + + + + + + +
{$value.value}
\ No newline at end of file diff --git a/test/runtime/samples/props-reactive-only-with-change/Comp.svelte b/test/runtime/samples/props-reactive-only-with-change/Comp.svelte new file mode 100644 index 0000000000..0eaf8a40d4 --- /dev/null +++ b/test/runtime/samples/props-reactive-only-with-change/Comp.svelte @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/test/runtime/samples/props-reactive-only-with-change/_config.js b/test/runtime/samples/props-reactive-only-with-change/_config.js new file mode 100644 index 0000000000..04170620c8 --- /dev/null +++ b/test/runtime/samples/props-reactive-only-with-change/_config.js @@ -0,0 +1,30 @@ +let callbacks = []; + +export default { + props: { + callback: (value) => callbacks.push(value), + val1: "1", + val2: "2", + }, + + before_test() { + callbacks = []; + }, + + async test({ assert, component, target }) { + assert.equal(callbacks.length, 2); + assert.equal(JSON.stringify(callbacks), '["1","2"]'); + + component.val1 = "3"; + assert.equal(callbacks.length, 3); + assert.equal(JSON.stringify(callbacks), '["1","2","1"]'); + + component.val1 = "4"; + assert.equal(callbacks.length, 4); + assert.equal(JSON.stringify(callbacks), '["1","2","1","1"]'); + + component.val2 = "5"; + assert.equal(callbacks.length, 5); + assert.equal(JSON.stringify(callbacks), '["1","2","1","1","2"]'); + }, +}; diff --git a/test/runtime/samples/props-reactive-only-with-change/main.svelte b/test/runtime/samples/props-reactive-only-with-change/main.svelte new file mode 100644 index 0000000000..73ddd137f5 --- /dev/null +++ b/test/runtime/samples/props-reactive-only-with-change/main.svelte @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/test/runtime/samples/reactive-value-assign-property/_config.js b/test/runtime/samples/reactive-value-assign-property/_config.js new file mode 100644 index 0000000000..7b6c1d2965 --- /dev/null +++ b/test/runtime/samples/reactive-value-assign-property/_config.js @@ -0,0 +1,5 @@ +export default { + html: ` +

Hello world!

+ `, +}; diff --git a/test/runtime/samples/reactive-value-assign-property/main.svelte b/test/runtime/samples/reactive-value-assign-property/main.svelte new file mode 100644 index 0000000000..58e0fdb03c --- /dev/null +++ b/test/runtime/samples/reactive-value-assign-property/main.svelte @@ -0,0 +1,6 @@ + + +

Hello {user.name}!

\ No newline at end of file diff --git a/test/runtime/samples/transition-js-each-else-block-intro-outro/_config.js b/test/runtime/samples/transition-js-each-else-block-intro-outro/_config.js new file mode 100644 index 0000000000..13d0cf0b23 --- /dev/null +++ b/test/runtime/samples/transition-js-each-else-block-intro-outro/_config.js @@ -0,0 +1,54 @@ +export default { + props: { + things: ['a', 'b', 'c'] + }, + + test({ assert, component, target, window, raf }) { + component.things = []; + let div = target.querySelector('div'); + assert.equal(div.foo, 0); + + raf.tick(200); + assert.equal(div.foo, 0.5); + + raf.tick(300); + assert.equal(div.foo, 0.75); + + raf.tick(400); + assert.equal(div.foo, 1); + + raf.tick(600); + component.things = ['a', 'b', 'c']; + + raf.tick(700); + assert.equal(div.foo, 1); + assert.equal(div.bar, 0.75); + + raf.tick(800); + assert.equal(div.foo, 1); + assert.equal(div.bar, 0.5); + + raf.tick(900); + assert.equal(div.foo, 1); + assert.equal(div.bar, 0.25); + + // test outro before intro complete + raf.tick(1000); + component.things = []; + div = target.querySelector('div'); + + raf.tick(1200); + assert.equal(div.foo, 0.5); + + component.things = ['a', 'b', 'c']; + raf.tick(1300); + assert.equal(div.foo, 0.75); + assert.equal(div.bar, 0.75); + + raf.tick(1400); + assert.equal(div.foo, 1); + assert.equal(div.bar, 0.5); + + raf.tick(2000); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/transition-js-each-else-block-intro-outro/main.svelte b/test/runtime/samples/transition-js-each-else-block-intro-outro/main.svelte new file mode 100644 index 0000000000..1691e77f52 --- /dev/null +++ b/test/runtime/samples/transition-js-each-else-block-intro-outro/main.svelte @@ -0,0 +1,27 @@ + + +{#each things as thing} +

{thing}

+{:else} +
else
+{/each} \ No newline at end of file diff --git a/test/runtime/samples/transition-js-each-else-block-intro/_config.js b/test/runtime/samples/transition-js-each-else-block-intro/_config.js new file mode 100644 index 0000000000..f061e312ff --- /dev/null +++ b/test/runtime/samples/transition-js-each-else-block-intro/_config.js @@ -0,0 +1,22 @@ +export default { + props: { + things: ['a', 'b', 'c'] + }, + + test({ assert, component, target, window, raf }) { + component.things = []; + const div = target.querySelector('div'); + assert.equal(div.foo, 0); + + raf.tick(200); + assert.equal(div.foo, 0.5); + + raf.tick(300); + assert.equal(div.foo, 0.75); + + raf.tick(400); + assert.equal(div.foo, 1); + + raf.tick(500); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/transition-js-each-else-block-intro/main.svelte b/test/runtime/samples/transition-js-each-else-block-intro/main.svelte new file mode 100644 index 0000000000..bda8746c46 --- /dev/null +++ b/test/runtime/samples/transition-js-each-else-block-intro/main.svelte @@ -0,0 +1,18 @@ + + +{#each things as thing} +

{thing}

+{:else} +
else
+{/each} \ No newline at end of file diff --git a/test/runtime/samples/transition-js-each-else-block-outro/_config.js b/test/runtime/samples/transition-js-each-else-block-outro/_config.js new file mode 100644 index 0000000000..863753b34a --- /dev/null +++ b/test/runtime/samples/transition-js-each-else-block-outro/_config.js @@ -0,0 +1,20 @@ +export default { + props: { + things: [] + }, + test({ assert, component, target, window, raf }) { + const div = target.querySelector('div'); + component.things = ['a', 'b', 'c']; + + raf.tick(200); + assert.equal(div.foo, 0.5); + + raf.tick(300); + assert.equal(div.foo, 0.25); + + raf.tick(400); + assert.equal(div.foo, 0); + + raf.tick(500); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/transition-js-each-else-block-outro/main.svelte b/test/runtime/samples/transition-js-each-else-block-outro/main.svelte new file mode 100644 index 0000000000..246c146127 --- /dev/null +++ b/test/runtime/samples/transition-js-each-else-block-outro/main.svelte @@ -0,0 +1,18 @@ + + +{#each things as thing} +

{thing}

+{:else} +
else
+{/each} \ No newline at end of file diff --git a/test/validator/samples/a11y-label-has-associated-control/input.svelte b/test/validator/samples/a11y-label-has-associated-control/input.svelte new file mode 100644 index 0000000000..43304689dc --- /dev/null +++ b/test/validator/samples/a11y-label-has-associated-control/input.svelte @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/validator/samples/a11y-label-has-associated-control/warnings.json b/test/validator/samples/a11y-label-has-associated-control/warnings.json new file mode 100644 index 0000000000..b70a1a47de --- /dev/null +++ b/test/validator/samples/a11y-label-has-associated-control/warnings.json @@ -0,0 +1,32 @@ +[ + { + "code": "a11y-label-has-associated-control", + "end": { + "character": 16, + "column": 16, + "line": 1 + }, + "message": "A11y: A form label must be associated with a control.", + "pos": 0, + "start": { + "character": 0, + "column": 0, + "line": 1 + } + }, + { + "code": "a11y-label-has-associated-control", + "end": { + "character": 149, + "column": 30, + "line": 6 + }, + "message": "A11y: A form label must be associated with a control.", + "pos": 119, + "start": { + "character": 119, + "column": 0, + "line": 6 + } + } +] diff --git a/test/validator/samples/a11y-media-has-caption/input.svelte b/test/validator/samples/a11y-media-has-caption/input.svelte new file mode 100644 index 0000000000..105269cddb --- /dev/null +++ b/test/validator/samples/a11y-media-has-caption/input.svelte @@ -0,0 +1,4 @@ + + + + diff --git a/test/validator/samples/a11y-media-has-caption/warnings.json b/test/validator/samples/a11y-media-has-caption/warnings.json new file mode 100644 index 0000000000..a8c894b1d4 --- /dev/null +++ b/test/validator/samples/a11y-media-has-caption/warnings.json @@ -0,0 +1,32 @@ +[ + { + "code": "a11y-media-has-caption", + "end": { + "character": 55, + "column": 15, + "line": 2 + }, + "message": "A11y: Media elements must have a ", + "pos": 40, + "start": { + "character": 40, + "column": 0, + "line": 2 + } + }, + { + "code": "a11y-media-has-caption", + "end": { + "character": 80, + "column": 24, + "line": 3 + }, + "message": "A11y: Media elements must have a ", + "pos": 56, + "start": { + "character": 56, + "column": 0, + "line": 3 + } + } +]