Merge branch 'master' into pr/5847

pull/5847/head
Conduitry 6 years ago
commit 9038c39fda

3
.gitattributes vendored

@ -0,0 +1,3 @@
/site/** -linguist-detectable
/test/**/samples/** -linguist-detectable
/**/*.svelte linguist-detectable

2
.gitignore vendored

@ -34,4 +34,4 @@ _output
/site/scripts/svelte-app /site/scripts/svelte-app
/site/scripts/community /site/scripts/community
/site/src/routes/_contributors.js /site/src/routes/_contributors.js
/site/src/routes/_components/WhosUsingSvelte.svelte /site/src/routes/_components/WhosUsingSvelte.*

@ -1,5 +1,28 @@
# Svelte changelog # Svelte changelog
## Unreleased
* Throw a parser error for `class:` directives with an empty class name ([#5858](https://github.com/sveltejs/svelte/issues/5858))
* Fix extraneous store subscription in SSR mode ([#5883](https://github.com/sveltejs/svelte/issues/5883))
* Don't emit update code for `class:` directives whose expression is not dynamic ([#5919](https://github.com/sveltejs/svelte/issues/5919))
* Fix type inference for derived stores ([#5935](https://github.com/sveltejs/svelte/pull/5935))
* Make parameters of built-in animations and transitions optional ([#5936](https://github.com/sveltejs/svelte/pull/5936))
* Make `SvelteComponentDev` typings more forgiving ([#5937](https://github.com/sveltejs/svelte/pull/5937))
* Fix `foreign` elements incorrectly disallowing `bind:this` ([#5942](https://github.com/sveltejs/svelte/pull/5942))
## 3.32.0
* Allow multiple instances of the same action on an element ([#5516](https://github.com/sveltejs/svelte/issues/5516))
* Support `foreign` namespace, which disables certain HTML5-specific behaviour and checks ([#5652](https://github.com/sveltejs/svelte/pull/5652))
* Support inline comment sourcemaps in code from preprocessors ([#5854](https://github.com/sveltejs/svelte/pull/5854))
## 3.31.2
* Rework SSR store handling to subscribe and unsubscribe as in DOM mode ([#3375](https://github.com/sveltejs/svelte/issues/3375), [#3582](https://github.com/sveltejs/svelte/issues/3582), [#3636](https://github.com/sveltejs/svelte/issues/3636))
* Fix error when removing elements that are already transitioning out ([#5789](https://github.com/sveltejs/svelte/issues/5789), [#5808](https://github.com/sveltejs/svelte/issues/5808))
* Fix duplicate content race condition with `{#await}` blocks and out transitions ([#5815](https://github.com/sveltejs/svelte/issues/5815))
* Deconflict variable names used for contextual actions ([#5834](https://github.com/sveltejs/svelte/issues/5834))
## 3.31.1 ## 3.31.1
* Fix scrolling of element with resize listener by making the `<iframe>` have `z-index: -1` ([#5448](https://github.com/sveltejs/svelte/issues/5448)) * Fix scrolling of element with resize listener by making the `<iframe>` have `z-index: -1` ([#5448](https://github.com/sveltejs/svelte/issues/5448))

@ -23,7 +23,7 @@ Learn more at the [Svelte website](https://svelte.dev), or stop by the [Discord
## Supporting Svelte ## Supporting Svelte
Svelte is an MIT-licensed open source project with its ongoing development made possible entirely by the support of awesome volunteers. If you'd like to support their efforts, please consider: Svelte is an MIT-licensed open source project with its ongoing development made possible entirely by fantastic volunteers. If you'd like to support their efforts, please consider:
- [Becoming a backer on Open Collective](https://opencollective.com/svelte). - [Becoming a backer on Open Collective](https://opencollective.com/svelte).
@ -44,7 +44,7 @@ npm install
> Do not use Yarn to install the dependencies, as the specific package versions in `package-lock.json` are used to build and test Svelte. > Do not use Yarn to install the dependencies, as the specific package versions in `package-lock.json` are used to build and test Svelte.
To build the compiler, and all the other modules included in the package: To build the compiler and all the other modules included in the package:
```bash ```bash
npm run build npm run build
@ -56,7 +56,7 @@ To watch for changes and continually rebuild the package (this is useful if you'
npm run dev npm run dev
``` ```
The compiler is written in [TypeScript](https://www.typescriptlang.org/), but don't let that put you off — it's basically just JavaScript with type annotations. You'll pick it up in no time. If you're using an editor other than [Visual Studio Code](https://code.visualstudio.com/) you may need to install a plugin in order to get syntax highlighting and code hints etc. The compiler is written in [TypeScript](https://www.typescriptlang.org/), but don't let that put you off — it's basically just JavaScript with type annotations. You'll pick it up in no time. If you're using an editor other than [Visual Studio Code](https://code.visualstudio.com/), you may need to install a plugin in order to get syntax highlighting and code hints, etc.
### Running Tests ### Running Tests

2
package-lock.json generated

@ -1,6 +1,6 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.31.1", "version": "3.32.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

@ -1,6 +1,6 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.31.1", "version": "3.32.0",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"module": "index.mjs", "module": "index.mjs",
"main": "index", "main": "index",

@ -43,12 +43,17 @@ In the terminal, you can instantly create a new project like so:
```bash ```bash
npx degit sveltejs/template my-svelte-project npx degit sveltejs/template my-svelte-project
cd my-svelte-project cd my-svelte-project
# to use TypeScript run:
# node scripts/setupTypeScript.js
npm install npm install
npm run dev npm run dev
``` ```
This will create a new project in the `my-svelte-project` directory, install its dependencies, and start a server on http://localhost:5000. This will create a new project in the `my-svelte-project` directory, install its dependencies, and start a server on http://localhost:5000.
You can find more information about using TypeScript [here](blog/svelte-and-typescript).
Once you've tinkered a bit and understood how everything fits together, you can fork [sveltejs/template](https://github.com/sveltejs/template) and start doing this instead: Once you've tinkered a bit and understood how everything fits together, you can fork [sveltejs/template](https://github.com/sveltejs/template) and start doing this instead:
```bash ```bash

@ -1292,19 +1292,19 @@ Components can have child content, in the same way that elements can.
The content is exposed in the child component using the `<slot>` element, which can contain fallback content that is rendered if no children are provided. The content is exposed in the child component using the `<slot>` element, which can contain fallback content that is rendered if no children are provided.
```sv ```sv
<!-- App.svelte -->
<Widget></Widget>
<Widget>
<p>this is some child content that will overwrite the default slot content</p>
</Widget>
<!-- Widget.svelte --> <!-- Widget.svelte -->
<div> <div>
<slot> <slot>
this fallback content will be rendered when no content is provided, like in the first example this fallback content will be rendered when no content is provided, like in the first example
</slot> </slot>
</div> </div>
<!-- App.svelte -->
<Widget></Widget> <!-- this component will render the default content -->
<Widget>
<p>this is some child content that will overwrite the default slot content</p>
</Widget>
``` ```
#### [`<slot name="`*name*`">`](slot_name) #### [`<slot name="`*name*`">`](slot_name)
@ -1314,18 +1314,18 @@ The content is exposed in the child component using the `<slot>` element, which
Named slots allow consumers to target specific areas. They can also have fallback content. Named slots allow consumers to target specific areas. They can also have fallback content.
```sv ```sv
<!-- App.svelte -->
<Widget>
<h1 slot="header">Hello</h1>
<p slot="footer">Copyright (c) 2019 Svelte Industries</p>
</Widget>
<!-- Widget.svelte --> <!-- Widget.svelte -->
<div> <div>
<slot name="header">No header was provided</slot> <slot name="header">No header was provided</slot>
<p>Some content between header and footer</p> <p>Some content between header and footer</p>
<slot name="footer"></slot> <slot name="footer"></slot>
</div> </div>
<!-- App.svelte -->
<Widget>
<h1 slot="header">Hello</h1>
<p slot="footer">Copyright (c) 2019 Svelte Industries</p>
</Widget>
``` ```
#### [`$$slots`](slots_object) #### [`$$slots`](slots_object)
@ -1337,20 +1337,21 @@ Named slots allow consumers to target specific areas. They can also have fallbac
Note that explicitly passing in an empty named slot will add that slot's name to `$$slots`. For example, if a parent passes `<div slot="title" />` to a child component, `$$slots.title` will be truthy within the child. Note that explicitly passing in an empty named slot will add that slot's name to `$$slots`. For example, if a parent passes `<div slot="title" />` to a child component, `$$slots.title` will be truthy within the child.
```sv ```sv
<!-- App.svelte -->
<Card>
<h1 slot="title">Blog Post Title</h1>
</Card>
<!-- Card.svelte --> <!-- Card.svelte -->
<div> <div>
<slot name="title"></slot> <slot name="title"></slot>
{#if $$slots.description} {#if $$slots.description}
<!-- This slot and the <hr> before it will not render. --> <!-- This <hr> and slot will render only if a slot named "description" is provided. -->
<hr> <hr>
<slot name="description"></slot> <slot name="description"></slot>
{/if} {/if}
</div> </div>
<!-- App.svelte -->
<Card>
<h1 slot="title">Blog Post Title</h1>
<!-- No slot named "description" was provided so the optional slot will not be rendered. -->
</Card>
``` ```
#### [`<slot let:`*name*`={`*value*`}>`](slot_let) #### [`<slot let:`*name*`={`*value*`}>`](slot_let)
@ -1362,11 +1363,6 @@ Slots can be rendered zero or more times, and can pass values *back* to the pare
The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}`, and `<slot {item}>` is equivalent to `<slot item={item}>`. The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}`, and `<slot {item}>` is equivalent to `<slot item={item}>`.
```sv ```sv
<!-- App.svelte -->
<FancyList {items} let:prop={thing}>
<div>{thing.text}</div>
</FancyList>
<!-- FancyList.svelte --> <!-- FancyList.svelte -->
<ul> <ul>
{#each items as item} {#each items as item}
@ -1375,6 +1371,11 @@ The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}
</li> </li>
{/each} {/each}
</ul> </ul>
<!-- App.svelte -->
<FancyList {items} let:prop={thing}>
<div>{thing.text}</div>
</FancyList>
``` ```
--- ---
@ -1382,12 +1383,6 @@ The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}
Named slots can also expose values. The `let:` directive goes on the element with the `slot` attribute. Named slots can also expose values. The `let:` directive goes on the element with the `slot` attribute.
```sv ```sv
<!-- App.svelte -->
<FancyList {items}>
<div slot="item" let:item>{item.text}</div>
<p slot="footer">Copyright (c) 2019 Svelte Industries</p>
</FancyList>
<!-- FancyList.svelte --> <!-- FancyList.svelte -->
<ul> <ul>
{#each items as item} {#each items as item}
@ -1398,6 +1393,12 @@ Named slots can also expose values. The `let:` directive goes on the element wit
</ul> </ul>
<slot name="footer"></slot> <slot name="footer"></slot>
<!-- App.svelte -->
<FancyList {items}>
<div slot="item" let:item>{item.text}</div>
<p slot="footer">Copyright (c) 2019 Svelte Industries</p>
</FancyList>
``` ```
@ -1530,7 +1531,7 @@ The `<svelte:options>` element provides a place to specify per-component compile
* `immutable={false}` — the default. Svelte will be more conservative about whether or not mutable objects have changed * `immutable={false}` — the default. Svelte will be more conservative about whether or not mutable objects have changed
* `accessors={true}` — adds getters and setters for the component's props * `accessors={true}` — adds getters and setters for the component's props
* `accessors={false}` — the default * `accessors={false}` — the default
* `namespace="..."` — the namespace where this component will be used, most commonly "svg" * `namespace="..."` — the namespace where this component will be used, most commonly "svg"; use the "foreign" namespace to opt out of case-insensitive attribute names and HTML-specific warnings
* `tag="..."` — the name to use when compiling this component as a custom element * `tag="..."` — the name to use when compiling this component as a custom element
```sv ```sv

@ -80,7 +80,7 @@ The following options can be passed to the compiler. None are required:
| `outputFilename` | `null` | A `string` used for your JavaScript sourcemap. | `outputFilename` | `null` | A `string` used for your JavaScript sourcemap.
| `cssOutputFilename` | `null` | A `string` used for your CSS sourcemap. | `cssOutputFilename` | `null` | A `string` used for your CSS sourcemap.
| `sveltePath` | `"svelte"` | The location of the `svelte` package. Any imports from `svelte` or `svelte/[module]` will be modified accordingly. | `sveltePath` | `"svelte"` | The location of the `svelte` package. Any imports from `svelte` or `svelte/[module]` will be modified accordingly.
| `namespace` | `"html"` | The namespace of the element; e.g., `"mathml"`, `"svg"`, `"foreign"`.
--- ---

@ -8,5 +8,6 @@ sh.exec('npx degit sveltejs/community scripts/community');
// copy over relevant files // copy over relevant files
sh.cp('scripts/community/whos-using-svelte/WhosUsingSvelte.svelte', 'src/routes/_components/WhosUsingSvelte.svelte'); sh.cp('scripts/community/whos-using-svelte/WhosUsingSvelte.svelte', 'src/routes/_components/WhosUsingSvelte.svelte');
sh.cp('scripts/community/whos-using-svelte/WhosUsingSvelte.js', 'src/routes/_components/WhosUsingSvelte.js');
sh.rm('-rf', 'static/organisations'); sh.rm('-rf', 'static/organisations');
sh.cp('-r', 'scripts/community/whos-using-svelte/organisations', 'static'); sh.cp('-r', 'scripts/community/whos-using-svelte/organisations', 'static');

@ -77,6 +77,8 @@
npx degit <a href="https://github.com/sveltejs/template" style="user-select: initial;">sveltejs/template</a> my-svelte-project npx degit <a href="https://github.com/sveltejs/template" style="user-select: initial;">sveltejs/template</a> my-svelte-project
<span class="token comment"># or download and extract <a href="https://github.com/sveltejs/template/archive/master.zip">this .zip file</a></span> <span class="token comment"># or download and extract <a href="https://github.com/sveltejs/template/archive/master.zip">this .zip file</a></span>
cd my-svelte-project cd my-svelte-project
<span class="token comment"># to use <a href="blog/svelte-and-typescript">TypeScript</a> run:</span>
<span class="token comment"># node scripts/setupTypeScript.js</span>
npm install npm install
npm run dev npm run dev

@ -29,7 +29,7 @@ import add_to_set from './utils/add_to_set';
import check_graph_for_cycles from './utils/check_graph_for_cycles'; import check_graph_for_cycles from './utils/check_graph_for_cycles';
import { print, x, b } from 'code-red'; import { print, x, b } from 'code-red';
import { is_reserved_keyword } from './utils/reserved_keywords'; import { is_reserved_keyword } from './utils/reserved_keywords';
import { apply_preprocessor_sourcemap } from '../utils/string_with_sourcemap'; import { apply_preprocessor_sourcemap } from '../utils/mapped_code';
import Element from './nodes/Element'; import Element from './nodes/Element';
import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types'; import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types';
@ -1366,7 +1366,8 @@ function process_component_options(component: Component, nodes) {
'accessors' in component.compile_options 'accessors' in component.compile_options
? component.compile_options.accessors ? component.compile_options.accessors
: !!component.compile_options.customElement, : !!component.compile_options.customElement,
preserveWhitespace: !!component.compile_options.preserveWhitespace preserveWhitespace: !!component.compile_options.preserveWhitespace,
namespace: component.compile_options.namespace
}; };
const node = nodes.find(node => node.name === 'svelte:options'); const node = nodes.find(node => node.name === 'svelte:options');

@ -6,6 +6,7 @@ import { CompileOptions, Warning } from '../interfaces';
import Component from './Component'; import Component from './Component';
import fuzzymatch from '../utils/fuzzymatch'; import fuzzymatch from '../utils/fuzzymatch';
import get_name_from_filename from './utils/get_name_from_filename'; import get_name_from_filename from './utils/get_name_from_filename';
import { valid_namespaces } from '../utils/namespaces';
const valid_options = [ const valid_options = [
'format', 'format',
@ -22,6 +23,7 @@ const valid_options = [
'hydratable', 'hydratable',
'legacy', 'legacy',
'customElement', 'customElement',
'namespace',
'tag', 'tag',
'css', 'css',
'loopGuardTimeout', 'loopGuardTimeout',
@ -30,7 +32,7 @@ const valid_options = [
]; ];
function validate_options(options: CompileOptions, warnings: Warning[]) { function validate_options(options: CompileOptions, warnings: Warning[]) {
const { name, filename, loopGuardTimeout, dev } = options; const { name, filename, loopGuardTimeout, dev, namespace } = options;
Object.keys(options).forEach(key => { Object.keys(options).forEach(key => {
if (!valid_options.includes(key)) { if (!valid_options.includes(key)) {
@ -65,6 +67,15 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
toString: () => message toString: () => message
}); });
} }
if (namespace && valid_namespaces.indexOf(namespace) === -1) {
const match = fuzzymatch(namespace, valid_namespaces);
if (match) {
throw new Error(`Invalid namespace '${namespace}' (did you mean '${match}'?)`);
} else {
throw new Error(`Invalid namespace '${namespace}'`);
}
}
} }
export default function compile(source: string, options: CompileOptions = {}) { export default function compile(source: string, options: CompileOptions = {}) {

@ -2,15 +2,16 @@ import Node from './shared/Node';
import Expression from './shared/Expression'; import Expression from './shared/Expression';
import Component from '../Component'; import Component from '../Component';
import TemplateScope from './shared/TemplateScope'; import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces'; import { Directive } from '../../interfaces';
export default class Action extends Node { export default class Action extends Node {
type: 'Action'; type: 'Action';
name: string; name: string;
expression: Expression; expression: Expression;
uses_context: boolean; uses_context: boolean;
template_scope: TemplateScope;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Node, scope: TemplateScope, info: Directive) {
super(component, parent, scope, info); super(component, parent, scope, info);
const object = info.name.split('.')[0]; const object = info.name.split('.')[0];
@ -23,6 +24,8 @@ export default class Action extends Node {
? new Expression(component, this, scope, info.expression) ? new Expression(component, this, scope, info.expression)
: null; : null;
this.template_scope = scope;
this.uses_context = this.expression && this.expression.uses_context; this.uses_context = this.expression && this.expression.uses_context;
} }
} }

@ -136,44 +136,45 @@ export default class Element extends Node {
this.namespace = get_namespace(parent as Element, this, component.namespace); this.namespace = get_namespace(parent as Element, this, component.namespace);
if (this.name === 'textarea') { if (this.namespace !== namespaces.foreign) {
if (info.children.length > 0) { if (this.name === 'textarea') {
const value_attribute = info.attributes.find(node => node.name === 'value'); if (info.children.length > 0) {
if (value_attribute) { const value_attribute = info.attributes.find(node => node.name === 'value');
component.error(value_attribute, { if (value_attribute) {
code: 'textarea-duplicate-value', component.error(value_attribute, {
message: 'A <textarea> can have either a value attribute or (equivalently) child content, but not both' code: 'textarea-duplicate-value',
}); message: 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
} });
}
// this is an egregious hack, but it's the easiest way to get <textarea> // this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute // children treated the same way as a value attribute
info.attributes.push({ info.attributes.push({
type: 'Attribute', type: 'Attribute',
name: 'value', name: 'value',
value: info.children value: info.children
}); });
info.children = []; info.children = [];
}
} }
}
if (this.name === 'option') { if (this.name === 'option') {
// Special case — treat these the same way: // Special case — treat these the same way:
// <option>{foo}</option> // <option>{foo}</option>
// <option value={foo}>{foo}</option> // <option value={foo}>{foo}</option>
const value_attribute = info.attributes.find(attribute => attribute.name === 'value'); const value_attribute = info.attributes.find(attribute => attribute.name === 'value');
if (!value_attribute) { if (!value_attribute) {
info.attributes.push({ info.attributes.push({
type: 'Attribute', type: 'Attribute',
name: 'value', name: 'value',
value: info.children, value: info.children,
synthetic: true synthetic: true
}); });
}
} }
} }
const has_let = info.attributes.some(node => node.type === 'Let'); const has_let = info.attributes.some(node => node.type === 'Let');
if (has_let) { if (has_let) {
scope = scope.child(); scope = scope.child();
@ -253,65 +254,83 @@ export default class Element extends Node {
}); });
} }
if (a11y_distracting_elements.has(this.name)) { this.validate_attributes();
// no-distracting-elements this.validate_event_handlers();
this.component.warn(this, { if (this.namespace === namespaces.foreign) {
code: 'a11y-distracting-elements', this.validate_bindings_foreign();
message: `A11y: Avoid <${this.name}> elements` } else {
}); this.validate_attributes_a11y();
this.validate_special_cases();
this.validate_bindings();
this.validate_content();
} }
if (this.name === 'figcaption') { }
let { parent } = this;
let is_figure_parent = false;
while (parent) { validate_attributes() {
if ((parent as Element).name === 'figure') { const { component, parent } = this;
is_figure_parent = true;
break;
}
if (parent.type === 'Element') {
break;
}
parent = parent.parent;
}
if (!is_figure_parent) { this.attributes.forEach(attribute => {
this.component.warn(this, { if (attribute.is_spread) return;
code: 'a11y-structure',
message: 'A11y: <figcaption> must be an immediate child of <figure>' const name = attribute.name.toLowerCase();
// Errors
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
component.error(attribute, {
code: 'illegal-attribute',
message: `'${name}' is not a valid attribute name`
}); });
} }
}
if (this.name === 'figure') { if (name === 'slot') {
const children = this.children.filter(node => { if (!attribute.is_static) {
if (node.type === 'Comment') return false; component.error(attribute, {
if (node.type === 'Text') return /\S/.test(node.data); code: 'invalid-slot-attribute',
return true; message: 'slot attribute cannot have a dynamic value'
}); });
}
const index = children.findIndex(child => (child as Element).name === 'figcaption'); if (component.slot_outlets.has(name)) {
component.error(attribute, {
code: 'duplicate-slot-attribute',
message: `Duplicate '${name}' slot`
});
if (index !== -1 && (index !== 0 && index !== children.length - 1)) { component.slot_outlets.add(name);
this.component.warn(children[index], { }
code: 'a11y-structure',
message: 'A11y: <figcaption> must be first or last child of <figure>' if (!(parent.type === 'InlineComponent' || within_custom_element(parent))) {
}); component.error(attribute, {
code: 'invalid-slotted-content',
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
});
}
} }
}
this.validate_attributes(); // Warnings
this.validate_special_cases();
this.validate_bindings();
this.validate_content();
this.validate_event_handlers();
}
validate_attributes() { if (this.namespace !== namespaces.foreign) {
const { component, parent } = this; if (name === 'is') {
component.warn(attribute, {
code: 'avoid-is',
message: 'The \'is\' attribute is not supported cross-browser and should be avoided'
});
}
const attribute_map = new Map(); if (react_attributes.has(attribute.name)) {
component.warn(attribute, {
code: 'invalid-html-attribute',
message: `'${attribute.name}' is not a valid HTML attribute. Did you mean '${react_attributes.get(attribute.name)}'?`
});
}
}
});
}
validate_attributes_a11y() {
const { component } = this;
this.attributes.forEach(attribute => { this.attributes.forEach(attribute => {
if (attribute.is_spread) return; if (attribute.is_spread) return;
@ -408,60 +427,13 @@ export default class Element extends Node {
}); });
} }
} }
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
component.error(attribute, {
code: 'illegal-attribute',
message: `'${name}' is not a valid attribute name`
});
}
if (name === 'slot') {
if (!attribute.is_static) {
component.error(attribute, {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
});
}
if (component.slot_outlets.has(name)) {
component.error(attribute, {
code: 'duplicate-slot-attribute',
message: `Duplicate '${name}' slot`
});
component.slot_outlets.add(name);
}
if (!(parent.type === 'InlineComponent' || within_custom_element(parent))) {
component.error(attribute, {
code: 'invalid-slotted-content',
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
});
}
}
if (name === 'is') {
component.warn(attribute, {
code: 'avoid-is',
message: 'The \'is\' attribute is not supported cross-browser and should be avoided'
});
}
if (react_attributes.has(attribute.name)) {
component.warn(attribute, {
code: 'invalid-html-attribute',
message: `'${attribute.name}' is not a valid HTML attribute. Did you mean '${react_attributes.get(attribute.name)}'?`
});
}
attribute_map.set(attribute.name, attribute);
}); });
} }
validate_special_cases() { validate_special_cases() {
const { component, attributes, handlers } = this; const { component, attributes, handlers } = this;
const attribute_map = new Map(); const attribute_map = new Map();
const handlers_map = new Map(); const handlers_map = new Map();
@ -576,6 +548,65 @@ export default class Element extends Node {
}); });
} }
} }
if (a11y_distracting_elements.has(this.name)) {
// no-distracting-elements
component.warn(this, {
code: 'a11y-distracting-elements',
message: `A11y: Avoid <${this.name}> elements`
});
}
if (this.name === 'figcaption') {
let { parent } = this;
let is_figure_parent = false;
while (parent) {
if ((parent as Element).name === 'figure') {
is_figure_parent = true;
break;
}
if (parent.type === 'Element') {
break;
}
parent = parent.parent;
}
if (!is_figure_parent) {
component.warn(this, {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be an immediate child of <figure>'
});
}
}
if (this.name === 'figure') {
const children = this.children.filter(node => {
if (node.type === 'Comment') return false;
if (node.type === 'Text') return /\S/.test(node.data);
return true;
});
const index = children.findIndex(child => (child as Element).name === 'figcaption');
if (index !== -1 && (index !== 0 && index !== children.length - 1)) {
component.warn(children[index], {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be first or last child of <figure>'
});
}
}
}
validate_bindings_foreign() {
this.bindings.forEach(binding => {
if (binding.name !== 'this') {
this.component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding. Foreign elements only support bind:this`
});
}
});
} }
validate_bindings() { validate_bindings() {

@ -14,6 +14,7 @@ import { Node, FunctionExpression, Identifier } from 'estree';
import { INode } from '../interfaces'; import { INode } from '../interfaces';
import { is_reserved_keyword } from '../../utils/reserved_keywords'; import { is_reserved_keyword } from '../../utils/reserved_keywords';
import replace_object from '../../utils/replace_object'; import replace_object from '../../utils/replace_object';
import is_contextual from './is_contextual';
import EachBlock from '../EachBlock'; import EachBlock from '../EachBlock';
type Owner = INode; type Owner = INode;
@ -409,18 +410,3 @@ function get_function_name(_node, parent) {
return 'func'; return 'func';
} }
function is_contextual(component: Component, scope: TemplateScope, name: string) {
if (is_reserved_keyword(name)) return true;
// if it's a name below root scope, it's contextual
if (!scope.is_top_level(name)) return true;
const variable = component.var_lookup.get(name);
// hoistables, module declarations, and imports are non-contextual
if (!variable || variable.hoistable) return false;
// assume contextual
return true;
}

@ -0,0 +1,18 @@
import Component from '../../Component';
import TemplateScope from './TemplateScope';
import { is_reserved_keyword } from '../../utils/reserved_keywords';
export default function is_contextual(component: Component, scope: TemplateScope, name: string) {
if (is_reserved_keyword(name)) return true;
// if it's a name below root scope, it's contextual
if (!scope.is_top_level(name)) return true;
const variable = component.var_lookup.get(name);
// hoistables, module declarations, and imports are non-contextual
if (!variable || variable.hoistable) return false;
// assume contextual
return true;
}

@ -6,6 +6,7 @@ import { x } from 'code-red';
import { Node, Identifier, MemberExpression, Literal, Expression, BinaryExpression } from 'estree'; import { Node, Identifier, MemberExpression, Literal, Expression, BinaryExpression } from 'estree';
import flatten_reference from '../utils/flatten_reference'; import flatten_reference from '../utils/flatten_reference';
import { reserved_keywords } from '../utils/reserved_keywords'; import { reserved_keywords } from '../utils/reserved_keywords';
import { renderer_invalidate } from './invalidate';
interface ContextMember { interface ContextMember {
name: string; name: string;
@ -168,57 +169,7 @@ export default class Renderer {
} }
invalidate(name: string, value?, main_execution_context: boolean = false) { invalidate(name: string, value?, main_execution_context: boolean = false) {
const variable = this.component.var_lookup.get(name); return renderer_invalidate(this, name, value, main_execution_context);
const member = this.context_lookup.get(name);
if (variable && (variable.subscribable && (variable.reassigned || variable.export_name))) {
return main_execution_context
? x`${`$$subscribe_${name}`}(${value || name})`
: x`${`$$subscribe_${name}`}($$invalidate(${member.index}, ${value || name}))`;
}
if (name[0] === '$' && name[1] !== '$') {
return x`${name.slice(1)}.set(${value || name})`;
}
if (
variable && (
variable.module || (
!variable.referenced &&
!variable.is_reactive_dependency &&
!variable.export_name &&
!name.startsWith('$$')
)
)
) {
return value || name;
}
if (value) {
return x`$$invalidate(${member.index}, ${value})`;
}
// if this is a reactive declaration, invalidate dependencies recursively
const deps = new Set([name]);
deps.forEach(name => {
const reactive_declarations = this.component.reactive_declarations.filter(x =>
x.assignees.has(name)
);
reactive_declarations.forEach(declaration => {
declaration.dependencies.forEach(name => {
deps.add(name);
});
});
});
// TODO ideally globals etc wouldn't be here in the first place
const filtered = Array.from(deps).filter(n => this.context_lookup.has(n));
if (!filtered.length) return null;
return filtered
.map(n => x`$$invalidate(${this.context_lookup.get(n).index}, ${n})`)
.reduce((lhs, rhs) => x`${lhs}, ${rhs}`);
} }
dirty(names: string[], is_reactive_declaration = false): Expression { dirty(names: string[], is_reactive_declaration = false): Expression {

@ -7,7 +7,7 @@ import { extract_names, Scope } from '../utils/scope';
import { invalidate } from './invalidate'; import { invalidate } from './invalidate';
import Block from './Block'; import Block from './Block';
import { ClassDeclaration, FunctionExpression, Node, Statement, ObjectExpression, Expression } from 'estree'; import { ClassDeclaration, FunctionExpression, Node, Statement, ObjectExpression, Expression } from 'estree';
import { apply_preprocessor_sourcemap } from '../../utils/string_with_sourcemap'; import { apply_preprocessor_sourcemap } from '../../utils/mapped_code';
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types'; import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
export default function dom( export default function dom(
@ -485,7 +485,7 @@ export default function dom(
${css.code && b`this.shadowRoot.innerHTML = \`<style>${css.code.replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`} ${css.code && b`this.shadowRoot.innerHTML = \`<style>${css.code.replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
@init(this, { target: this.shadowRoot, props: ${init_props} }, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_indexes}, ${dirty}); @init(this, { target: this.shadowRoot, props: ${init_props} }, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${dirty});
${dev_props_check} ${dev_props_check}
@ -537,7 +537,7 @@ export default function dom(
constructor(options) { constructor(options) {
super(${options.dev && 'options'}); super(${options.dev && 'options'});
${should_add_css && b`if (!@_document.getElementById("${component.stylesheet.id}-style")) ${add_css}();`} ${should_add_css && b`if (!@_document.getElementById("${component.stylesheet.id}-style")) ${add_css}();`}
@init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_indexes}, ${dirty}); @init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${dirty});
${options.dev && b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`} ${options.dev && b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`}
${dev_props_check} ${dev_props_check}

@ -33,7 +33,7 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
if (main_execution_context && !variable.subscribable && variable.name[0] !== '$') { if (main_execution_context && !variable.subscribable && variable.name[0] !== '$') {
return node; return node;
} }
return renderer.invalidate(variable.name, undefined, main_execution_context); return renderer_invalidate(renderer, variable.name, undefined, main_execution_context);
} }
if (!head) { if (!head) {
@ -79,3 +79,66 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
return invalidate; return invalidate;
} }
export function renderer_invalidate(renderer: Renderer, name: string, value?, main_execution_context: boolean = false) {
const variable = renderer.component.var_lookup.get(name);
if (variable && (variable.subscribable && (variable.reassigned || variable.export_name))) {
if (main_execution_context) {
return x`${`$$subscribe_${name}`}(${value || name})`;
} else {
const member = renderer.context_lookup.get(name);
return x`${`$$subscribe_${name}`}($$invalidate(${member.index}, ${value || name}))`;
}
}
if (name[0] === '$' && name[1] !== '$') {
return x`${name.slice(1)}.set(${value || name})`;
}
if (
variable && (
variable.module || (
!variable.referenced &&
!variable.is_reactive_dependency &&
!variable.export_name &&
!name.startsWith('$$')
)
)
) {
return value || name;
}
if (value) {
if (main_execution_context) {
return x`${value}`;
} else {
const member = renderer.context_lookup.get(name);
return x`$$invalidate(${member.index}, ${value})`;
}
}
if (main_execution_context) return;
// if this is a reactive declaration, invalidate dependencies recursively
const deps = new Set([name]);
deps.forEach(name => {
const reactive_declarations = renderer.component.reactive_declarations.filter(x =>
x.assignees.has(name)
);
reactive_declarations.forEach(declaration => {
declaration.dependencies.forEach(name => {
deps.add(name);
});
});
});
// TODO ideally globals etc wouldn't be here in the first place
const filtered = Array.from(deps).filter(n => renderer.context_lookup.has(n));
if (!filtered.length) return null;
return filtered
.map(n => x`$$invalidate(${renderer.context_lookup.get(n).index}, ${n})`)
.reduce((lhs, rhs) => x`${lhs}, ${rhs}`);
}

@ -8,6 +8,7 @@ import Expression from '../../../nodes/shared/Expression';
import Text from '../../../nodes/Text'; import Text from '../../../nodes/Text';
import handle_select_value_binding from './handle_select_value_binding'; import handle_select_value_binding from './handle_select_value_binding';
import { Identifier, Node } from 'estree'; import { Identifier, Node } from 'estree';
import { namespaces } from '../../../../utils/namespaces';
export class BaseAttributeWrapper { export class BaseAttributeWrapper {
node: Attribute; node: Attribute;
@ -67,15 +68,26 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
} }
} }
this.name = fix_attribute_casing(this.node.name); if (this.parent.node.namespace == namespaces.foreign) {
this.metadata = this.get_metadata(); // leave attribute case alone for elements in the "foreign" namespace
this.is_indirectly_bound_value = is_indirectly_bound_value(this); this.name = this.node.name;
this.property_name = this.is_indirectly_bound_value this.metadata = this.get_metadata();
? '__value' this.is_indirectly_bound_value = false;
: this.metadata && this.metadata.property_name; this.property_name = null;
this.is_select_value_attribute = false;
this.is_input_value = false;
} else {
this.name = fix_attribute_casing(this.node.name);
this.metadata = this.get_metadata();
this.is_indirectly_bound_value = is_indirectly_bound_value(this);
this.property_name = this.is_indirectly_bound_value
? '__value'
: this.metadata && this.metadata.property_name;
this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select';
this.is_input_value = this.name === 'value' && this.parent.node.name === 'input';
}
this.is_src = this.name === 'src'; // TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750 this.is_src = this.name === 'src'; // TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select';
this.is_input_value = this.name === 'value' && this.parent.node.name === 'input';
this.should_cache = should_cache(this); this.should_cache = should_cache(this);
} }

@ -26,6 +26,7 @@ import Action from '../../../nodes/Action';
import MustacheTagWrapper from '../MustacheTag'; import MustacheTagWrapper from '../MustacheTag';
import RawMustacheTagWrapper from '../RawMustacheTag'; import RawMustacheTagWrapper from '../RawMustacheTag';
import create_slot_block from './create_slot_block'; import create_slot_block from './create_slot_block';
import is_dynamic from '../shared/is_dynamic';
interface BindingGroup { interface BindingGroup {
events: string[]; events: string[];
@ -321,7 +322,7 @@ export default class ElementWrapper extends Wrapper {
literal.quasis.push(state.quasi); literal.quasis.push(state.quasi);
block.chunks.create.push( block.chunks.create.push(
b`${node}.${this.can_use_innerhtml ? 'innerHTML': 'textContent'} = ${literal};` b`${node}.${this.can_use_innerhtml ? 'innerHTML' : 'textContent'} = ${literal};`
); );
} }
} else { } else {
@ -898,10 +899,19 @@ export default class ElementWrapper extends Wrapper {
const all_dependencies = this.class_dependencies.concat(...dependencies); const all_dependencies = this.class_dependencies.concat(...dependencies);
const condition = block.renderer.dirty(all_dependencies); const condition = block.renderer.dirty(all_dependencies);
block.chunks.update.push(b` // If all of the dependencies are non-dynamic (don't get updated) then there is no reason
if (${condition}) { // to add an updater for this.
${updater} const any_dynamic_dependencies = all_dependencies.some((dep) => {
}`); const variable = this.renderer.component.var_lookup.get(dep);
return !variable || is_dynamic(variable);
});
if (any_dynamic_dependencies) {
block.chunks.update.push(b`
if (${condition}) {
${updater}
}
`);
}
} }
}); });
} }

@ -448,7 +448,7 @@ export default class IfBlockWrapper extends Wrapper {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#ctx); ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#ctx);
${name}.c(); ${name}.c();
} else { } else {
${name}.p(#ctx, #dirty); ${dynamic && b`${name}.p(#ctx, #dirty);`}
} }
${has_transitions && b`@transition_in(${name}, 1);`} ${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor}); ${name}.m(${update_mount_node}, ${anchor});
@ -472,10 +472,13 @@ export default class IfBlockWrapper extends Wrapper {
} }
`; `;
block.chunks.update.push(b`
let ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(#ctx, #dirty);
`);
if (dynamic) { if (dynamic) {
block.chunks.update.push(b` block.chunks.update.push(b`
let ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(#ctx, #dirty);
if (${current_block_type_index} === ${previous_block_index}) { if (${current_block_type_index} === ${previous_block_index}) {
${if_current_block_type_index(b`${if_blocks}[${current_block_type_index}].p(#ctx, #dirty);`)} ${if_current_block_type_index(b`${if_blocks}[${current_block_type_index}].p(#ctx, #dirty);`)}
} else { } else {
@ -484,8 +487,6 @@ export default class IfBlockWrapper extends Wrapper {
`); `);
} else { } else {
block.chunks.update.push(b` block.chunks.update.push(b`
let ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(#ctx, #dirty);
if (${current_block_type_index} !== ${previous_block_index}) { if (${current_block_type_index} !== ${previous_block_index}) {
${change_block} ${change_block}
} }

@ -173,7 +173,7 @@ export default class SlotWrapper extends Wrapper {
if (${slot}.p && ${renderer.dirty(dynamic_dependencies)}) { if (${slot}.p && ${renderer.dirty(dynamic_dependencies)}) {
@update_slot_spread(${slot}, ${slot_definition}, #ctx, ${renderer.reference('$$scope')}, #dirty, ${get_slot_changes_fn}, ${get_slot_spread_changes_fn}, ${get_slot_context_fn}); @update_slot_spread(${slot}, ${slot_definition}, #ctx, ${renderer.reference('$$scope')}, #dirty, ${get_slot_changes_fn}, ${get_slot_spread_changes_fn}, ${get_slot_context_fn});
} }
`: b` ` : b`
if (${slot}.p && ${renderer.dirty(dynamic_dependencies)}) { if (${slot}.p && ${renderer.dirty(dynamic_dependencies)}) {
@update_slot(${slot}, ${slot_definition}, #ctx, ${renderer.reference('$$scope')}, #dirty, ${get_slot_changes_fn}, ${get_slot_context_fn}); @update_slot(${slot}, ${slot_definition}, #ctx, ${renderer.reference('$$scope')}, #dirty, ${get_slot_changes_fn}, ${get_slot_context_fn});
} }

@ -1,6 +1,7 @@
import { b, x } from 'code-red'; import { b, x } from 'code-red';
import Block from '../../Block'; import Block from '../../Block';
import Action from '../../../nodes/Action'; import Action from '../../../nodes/Action';
import is_contextual from '../../../nodes/shared/is_contextual';
export default function add_actions( export default function add_actions(
block: Block, block: Block,
@ -11,7 +12,7 @@ export default function add_actions(
} }
export function add_action(block: Block, target: string, action: Action) { export function add_action(block: Block, target: string, action: Action) {
const { expression } = action; const { expression, template_scope } = action;
let snippet; let snippet;
let dependencies; let dependencies;
@ -28,7 +29,9 @@ export function add_action(block: Block, target: string, action: Action) {
const [obj, ...properties] = action.name.split('.'); const [obj, ...properties] = action.name.split('.');
const fn = block.renderer.reference(obj); const fn = is_contextual(action.component, template_scope, obj)
? block.renderer.reference(obj)
: obj;
if (properties.length) { if (properties.length) {
const member_expression = properties.reduce((lhs, rhs) => x`${lhs}.${rhs}`, fn); const member_expression = properties.reduce((lhs, rhs) => x`${lhs}.${rhs}`, fn);

@ -6,6 +6,9 @@ import Renderer from './Renderer';
import { INode as TemplateNode } from '../nodes/interfaces'; // TODO import { INode as TemplateNode } from '../nodes/interfaces'; // TODO
import Text from '../nodes/Text'; import Text from '../nodes/Text';
import { LabeledStatement, Statement, Node } from 'estree'; import { LabeledStatement, Statement, Node } from 'estree';
import { walk } from 'estree-walker';
import { extract_names } from 'periscopic';
import { invalidate } from '../render_dom/invalidate';
export default function ssr( export default function ssr(
component: Component, component: Component,
@ -38,24 +41,93 @@ export default function ssr(
const slots = uses_slots ? b`let $$slots = @compute_slots(#slots);` : null; const slots = uses_slots ? b`let $$slots = @compute_slots(#slots);` : null;
const reactive_stores = component.vars.filter(variable => variable.name[0] === '$' && variable.name[1] !== '$'); const reactive_stores = component.vars.filter(variable => variable.name[0] === '$' && variable.name[1] !== '$');
const reactive_store_values = reactive_stores const reactive_store_subscriptions = reactive_stores
.filter(store => {
const variable = component.var_lookup.get(store.name.slice(1));
return !variable || variable.hoistable;
})
.map(({ name }) => {
const store_name = name.slice(1);
return b`
${component.compile_options.dev && b`@validate_store(${store_name}, '${store_name}');`}
${`$$unsubscribe_${store_name}`} = @subscribe(${store_name}, #value => ${name} = #value)
`;
});
const reactive_store_unsubscriptions = reactive_stores.map(
({ name }) => b`${`$$unsubscribe_${name.slice(1)}`}()`
);
const reactive_store_declarations = reactive_stores
.map(({ name }) => { .map(({ name }) => {
const store_name = name.slice(1); const store_name = name.slice(1);
const store = component.var_lookup.get(store_name); const store = component.var_lookup.get(store_name);
if (store && store.hoistable) return null;
const assignment = b`${name} = @get_store_value(${store_name});`; if (store && store.reassigned) {
const unsubscribe = `$$unsubscribe_${store_name}`;
const subscribe = `$$subscribe_${store_name}`;
return component.compile_options.dev return b`let ${name}, ${unsubscribe} = @noop, ${subscribe} = () => (${unsubscribe}(), ${unsubscribe} = @subscribe(${store_name}, $$value => ${name} = $$value), ${store_name})`;
? b`@validate_store(${store_name}, '${store_name}'); ${assignment}` }
: assignment; return b`let ${name}, ${`$$unsubscribe_${store_name}`};`;
}) });
.filter(Boolean);
component.rewrite_props(({ name }) => { // instrument get/set store value
if (component.ast.instance) {
let scope = component.instance_scope;
const map = component.instance_scope_map;
walk(component.ast.instance.content, {
enter(node: Node) {
if (map.has(node)) {
scope = map.get(node);
}
},
leave(node: Node) {
if (map.has(node)) {
scope = scope.parent;
}
if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument;
const names = new Set(extract_names(assignee));
const to_invalidate = new Set<string>();
for (const name of names) {
const variable = component.var_lookup.get(name);
if (variable &&
!variable.hoistable &&
!variable.global &&
!variable.module &&
(
variable.subscribable || variable.name[0] === '$'
)) {
to_invalidate.add(variable.name);
}
}
if (to_invalidate.size) {
this.replace(
invalidate(
{ component } as any,
scope,
node,
to_invalidate,
true
)
);
}
}
}
});
}
component.rewrite_props(({ name, reassigned }) => {
const value = `$${name}`; const value = `$${name}`;
let insert = b`${value} = @get_store_value(${name})`; let insert = reassigned
? b`${`$$subscribe_${name}`}()`
: b`${`$$unsubscribe_${name}`} = @subscribe(${name}, #value => $${value} = #value)`;
if (component.compile_options.dev) { if (component.compile_options.dev) {
insert = b`@validate_store(${name}, '${name}'); ${insert}`; insert = b`@validate_store(${name}, '${name}'); ${insert}`;
} }
@ -99,35 +171,28 @@ export default function ssr(
do { do {
$$settled = true; $$settled = true;
${reactive_store_values}
${reactive_declarations} ${reactive_declarations}
$$rendered = ${literal}; $$rendered = ${literal};
} while (!$$settled); } while (!$$settled);
${reactive_store_unsubscriptions}
return $$rendered; return $$rendered;
` `
: b` : b`
${reactive_store_values}
${reactive_declarations} ${reactive_declarations}
${reactive_store_unsubscriptions}
return ${literal};`; return ${literal};`;
const blocks = [ const blocks = [
...injected.map(name => b`let ${name};`), ...injected.map(name => b`let ${name};`),
rest, rest,
slots, slots,
...reactive_stores.map(({ name }) => { ...reactive_store_declarations,
const store_name = name.slice(1); ...reactive_store_subscriptions,
const store = component.var_lookup.get(store_name);
if (store && store.hoistable) {
return b`let ${name} = @get_store_value(${store_name});`;
}
return b`let ${name};`;
}),
instance_javascript, instance_javascript,
...parent_bindings, ...parent_bindings,
css.code && b`$$result.css.add(#css);`, css.code && b`$$result.css.add(#css);`,

@ -124,6 +124,7 @@ export interface CompileOptions {
tag?: string; tag?: string;
css?: boolean; css?: boolean;
loopGuardTimeout?: number; loopGuardTimeout?: number;
namespace?: string;
preserveComments?: boolean; preserveComments?: boolean;
preserveWhitespace?: boolean; preserveWhitespace?: boolean;

@ -196,7 +196,7 @@ export default function mustache(parser: Parser) {
if (!parser.eat('}')) { if (!parser.eat('}')) {
parser.require_whitespace(); parser.require_whitespace();
await_block[is_then ? 'value': 'error'] = read_context(parser); await_block[is_then ? 'value' : 'error'] = read_context(parser);
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
} }
@ -204,7 +204,7 @@ export default function mustache(parser: Parser) {
const new_block: TemplateNode = { const new_block: TemplateNode = {
start, start,
end: null, end: null,
type: is_then ? 'ThenBlock': 'CatchBlock', type: is_then ? 'ThenBlock' : 'CatchBlock',
children: [], children: [],
skip: false skip: false
}; };

@ -376,7 +376,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
if (type === 'Binding' && directive_name !== 'this') { if (type === 'Binding' && directive_name !== 'this') {
check_unique(directive_name); check_unique(directive_name);
} else if (type !== 'EventHandler') { } else if (type !== 'EventHandler' && type !== 'Action') {
check_unique(name); check_unique(name);
} }
@ -387,6 +387,13 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
}, start); }, start);
} }
if (type === 'Class' && directive_name === '') {
parser.error({
code: 'invalid-class-directive',
message: 'Class binding name cannot be empty'
}, start + colon_index + 1);
}
if (value[0]) { if (value[0]) {
if ((value as any[]).length > 1 || value[0].type === 'Text') { if ((value as any[]).length > 1 || value[0].type === 'Text') {
parser.error({ parser.error({

@ -0,0 +1,88 @@
import { decode as decode_mappings } from 'sourcemap-codec';
import { Processed } from './types';
/**
* Import decoded sourcemap from mozilla/source-map/SourceMapGenerator
* Forked from source-map/lib/source-map-generator.js
* from methods _serializeMappings and toJSON.
* We cannot use source-map.d.ts types, because we access hidden properties.
*/
function decoded_sourcemap_from_generator(generator: any) {
let previous_generated_line = 1;
const converted_mappings = [[]];
let result_line;
let result_segment;
let mapping;
const source_idx = generator._sources.toArray()
.reduce((acc, val, idx) => (acc[val] = idx, acc), {});
const name_idx = generator._names.toArray()
.reduce((acc, val, idx) => (acc[val] = idx, acc), {});
const mappings = generator._mappings.toArray();
result_line = converted_mappings[0];
for (let i = 0, len = mappings.length; i < len; i++) {
mapping = mappings[i];
if (mapping.generatedLine > previous_generated_line) {
while (mapping.generatedLine > previous_generated_line) {
converted_mappings.push([]);
previous_generated_line++;
}
result_line = converted_mappings[mapping.generatedLine - 1]; // line is one-based
} else if (i > 0) {
const previous_mapping = mappings[i - 1];
if (
// sorted by selectivity
mapping.generatedColumn === previous_mapping.generatedColumn &&
mapping.originalColumn === previous_mapping.originalColumn &&
mapping.name === previous_mapping.name &&
mapping.generatedLine === previous_mapping.generatedLine &&
mapping.originalLine === previous_mapping.originalLine &&
mapping.source === previous_mapping.source
) {
continue;
}
}
result_line.push([mapping.generatedColumn]);
result_segment = result_line[result_line.length - 1];
if (mapping.source != null) {
result_segment.push(...[
source_idx[mapping.source],
mapping.originalLine - 1, // line is one-based
mapping.originalColumn
]);
if (mapping.name != null) {
result_segment.push(name_idx[mapping.name]);
}
}
}
const map = {
version: generator._version,
sources: generator._sources.toArray(),
names: generator._names.toArray(),
mappings: converted_mappings
};
if (generator._file != null) {
(map as any).file = generator._file;
}
// not needed: map.sourcesContent and map.sourceRoot
return map;
}
export function decode_map(processed: Processed) {
let decoded_map = typeof processed.map === 'string' ? JSON.parse(processed.map) : processed.map;
if (typeof(decoded_map.mappings) === 'string') {
decoded_map.mappings = decode_mappings(decoded_map.mappings);
}
if ((decoded_map as any)._mappings && decoded_map.constructor.name === 'SourceMapGenerator') {
// import decoded sourcemap from mozilla/source-map/SourceMapGenerator
decoded_map = decoded_sourcemap_from_generator(decoded_map);
}
return decoded_map;
}

@ -1,319 +1,230 @@
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types'; import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
import { decode as decode_mappings } from 'sourcemap-codec';
import { getLocator } from 'locate-character'; import { getLocator } from 'locate-character';
import { StringWithSourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/string_with_sourcemap'; import { MappedCode, SourceLocation, parse_attached_sourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/mapped_code';
import { decode_map } from './decode_sourcemap';
export interface Processed { import { replace_in_code, slice_source } from './replace_in_code';
code: string; import { MarkupPreprocessor, Source, Preprocessor, PreprocessorGroup, Processed } from './types';
map?: string | object; // we are opaque with the type here to avoid dependency on the remapping module for our public types.
interface SourceUpdate {
string?: string;
map?: DecodedSourceMap;
dependencies?: string[]; dependencies?: string[];
} }
export interface PreprocessorGroup {
markup?: (options: {
content: string;
filename: string;
}) => Processed | Promise<Processed>;
style?: Preprocessor;
script?: Preprocessor;
}
export type Preprocessor = (options: {
content: string;
attributes: Record<string, string | boolean>;
filename?: string;
}) => Processed | Promise<Processed>;
function parse_attributes(str: string) {
const attrs = {};
str.split(/\s+/).filter(Boolean).forEach(attr => {
const p = attr.indexOf('=');
if (p === -1) {
attrs[attr] = true;
} else {
attrs[attr.slice(0, p)] = '\'"'.includes(attr[p + 1]) ?
attr.slice(p + 2, -1) :
attr.slice(p + 1);
}
});
return attrs;
}
function get_file_basename(filename: string) { function get_file_basename(filename: string) {
return filename.split(/[/\\]/).pop(); return filename.split(/[/\\]/).pop();
} }
interface Replacement { /**
offset: number; * Represents intermediate states of the preprocessing.
length: number; */
replacement: StringWithSourcemap; class PreprocessResult implements Source {
} // sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1)
// so we use sourcemap_list.unshift() to add new maps
// https://github.com/ampproject/remapping#multiple-transformations-of-a-file
sourcemap_list: Array<DecodedSourceMap | RawSourceMap> = [];
dependencies: string[] = [];
file_basename: string;
async function replace_async( get_location: ReturnType<typeof getLocator>;
file_basename: string,
source: string, constructor(public source: string, public filename: string) {
get_location: ReturnType<typeof getLocator>, this.update_source({ string: source });
re: RegExp,
func: (...any) => Promise<StringWithSourcemap> // preprocess source must be relative to itself or equal null
): Promise<StringWithSourcemap> { this.file_basename = filename == null ? null : get_file_basename(filename);
const replacements: Array<Promise<Replacement>> = [];
source.replace(re, (...args) => {
replacements.push(
func(...args).then(
res =>
({
offset: args[args.length - 2],
length: args[0].length,
replacement: res
}) as Replacement
)
);
return '';
});
const out = new StringWithSourcemap();
let last_end = 0;
for (const { offset, length, replacement } of await Promise.all(
replacements
)) {
// content = unchanged source characters before the replaced segment
const content = StringWithSourcemap.from_source(
file_basename, source.slice(last_end, offset), get_location(last_end));
out.concat(content).concat(replacement);
last_end = offset + length;
} }
// final_content = unchanged source characters after last replaced segment
const final_content = StringWithSourcemap.from_source(
file_basename, source.slice(last_end), get_location(last_end));
return out.concat(final_content);
}
/** update_source({ string: source, map, dependencies }: SourceUpdate) {
* Import decoded sourcemap from mozilla/source-map/SourceMapGenerator if (source != null) {
* Forked from source-map/lib/source-map-generator.js this.source = source;
* from methods _serializeMappings and toJSON. this.get_location = getLocator(source);
* We cannot use source-map.d.ts types, because we access hidden properties.
*/
function decoded_sourcemap_from_generator(generator: any) {
let previous_generated_line = 1;
const converted_mappings = [[]];
let result_line;
let result_segment;
let mapping;
const source_idx = generator._sources.toArray()
.reduce((acc, val, idx) => (acc[val] = idx, acc), {});
const name_idx = generator._names.toArray()
.reduce((acc, val, idx) => (acc[val] = idx, acc), {});
const mappings = generator._mappings.toArray();
result_line = converted_mappings[0];
for (let i = 0, len = mappings.length; i < len; i++) {
mapping = mappings[i];
if (mapping.generatedLine > previous_generated_line) {
while (mapping.generatedLine > previous_generated_line) {
converted_mappings.push([]);
previous_generated_line++;
}
result_line = converted_mappings[mapping.generatedLine - 1]; // line is one-based
} else if (i > 0) {
const previous_mapping = mappings[i - 1];
if (
// sorted by selectivity
mapping.generatedColumn === previous_mapping.generatedColumn &&
mapping.originalColumn === previous_mapping.originalColumn &&
mapping.name === previous_mapping.name &&
mapping.generatedLine === previous_mapping.generatedLine &&
mapping.originalLine === previous_mapping.originalLine &&
mapping.source === previous_mapping.source
) {
continue;
}
} }
result_line.push([mapping.generatedColumn]);
result_segment = result_line[result_line.length - 1]; if (map) {
this.sourcemap_list.unshift(map);
if (mapping.source != null) { }
result_segment.push(...[
source_idx[mapping.source], if (dependencies) {
mapping.originalLine - 1, // line is one-based this.dependencies.push(...dependencies);
mapping.originalColumn
]);
if (mapping.name != null) {
result_segment.push(name_idx[mapping.name]);
}
} }
} }
const map = { to_processed(): Processed {
version: generator._version, // Combine all the source maps for each preprocessor function into one
sources: generator._sources.toArray(), const map: RawSourceMap = combine_sourcemaps(this.file_basename, this.sourcemap_list);
names: generator._names.toArray(),
mappings: converted_mappings return {
}; // TODO return separated output, in future version where svelte.compile supports it:
if (generator._file != null) { // style: { code: styleCode, map: styleMap },
(map as any).file = generator._file; // script { code: scriptCode, map: scriptMap },
// markup { code: markupCode, map: markupMap },
code: this.source,
dependencies: [...new Set(this.dependencies)],
map: map as object,
toString: () => this.source
};
} }
// not needed: map.sourcesContent and map.sourceRoot
return map;
} }
/** /**
* Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap * Convert preprocessor output for the tag content into MappedCode
*/ */
function get_replacement( function processed_content_to_code(processed: Processed, location: SourceLocation, file_basename: string): MappedCode {
file_basename: string, // Convert the preprocessed code and its sourcemap to a MappedCode
offset: number,
get_location: ReturnType<typeof getLocator>,
original: string,
processed: Processed,
prefix: string,
suffix: string
): StringWithSourcemap {
// Convert the unchanged prefix and suffix to StringWithSourcemap
const prefix_with_map = StringWithSourcemap.from_source(
file_basename, prefix, get_location(offset));
const suffix_with_map = StringWithSourcemap.from_source(
file_basename, suffix, get_location(offset + prefix.length + original.length));
// Convert the preprocessed code and its sourcemap to a StringWithSourcemap
let decoded_map: DecodedSourceMap; let decoded_map: DecodedSourceMap;
if (processed.map) { if (processed.map) {
decoded_map = typeof processed.map === 'string' ? JSON.parse(processed.map) : processed.map; decoded_map = decode_map(processed);
if (typeof(decoded_map.mappings) === 'string') {
decoded_map.mappings = decode_mappings(decoded_map.mappings);
}
if ((decoded_map as any)._mappings && decoded_map.constructor.name === 'SourceMapGenerator') {
// import decoded sourcemap from mozilla/source-map/SourceMapGenerator
decoded_map = decoded_sourcemap_from_generator(decoded_map);
}
// offset only segments pointing at original component source // offset only segments pointing at original component source
const source_index = decoded_map.sources.indexOf(file_basename); const source_index = decoded_map.sources.indexOf(file_basename);
if (source_index !== -1) { if (source_index !== -1) {
sourcemap_add_offset(decoded_map, get_location(offset + prefix.length), source_index); sourcemap_add_offset(decoded_map, location, source_index);
} }
} }
const processed_with_map = StringWithSourcemap.from_processed(processed.code, decoded_map);
// Surround the processed code with the prefix and suffix, retaining valid sourcemappings return MappedCode.from_processed(processed.code, decoded_map);
return prefix_with_map.concat(processed_with_map).concat(suffix_with_map);
} }
export default async function preprocess( /**
source: string, * Given the whole tag including content, return a `MappedCode`
preprocessor: PreprocessorGroup | PreprocessorGroup[], * representing the tag content replaced with `processed`.
options?: { filename?: string } */
) { function processed_tag_to_code(
// @ts-ignore todo: doublecheck processed: Processed,
const filename = (options && options.filename) || preprocessor.filename; // legacy tag_name: 'style' | 'script',
const dependencies = []; attributes: string,
source: Source
): MappedCode {
const { file_basename, get_location } = source;
// preprocess source must be relative to itself or equal null const build_mapped_code = (code: string, offset: number) =>
const file_basename = filename == null ? null : get_file_basename(filename); MappedCode.from_source(slice_source(code, offset, source));
const preprocessors = preprocessor const tag_open = `<${tag_name}${attributes || ''}>`;
? Array.isArray(preprocessor) ? preprocessor : [preprocessor] const tag_close = `</${tag_name}>`;
: [];
const markup = preprocessors.map(p => p.markup).filter(Boolean); const tag_open_code = build_mapped_code(tag_open, 0);
const script = preprocessors.map(p => p.script).filter(Boolean); const tag_close_code = build_mapped_code(tag_close, tag_open.length + source.source.length);
const style = preprocessors.map(p => p.style).filter(Boolean);
// sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1) parse_attached_sourcemap(processed, tag_name);
// so we use sourcemap_list.unshift() to add new maps
// https://github.com/ampproject/remapping#multiple-transformations-of-a-file
const sourcemap_list: Array<DecodedSourceMap | RawSourceMap> = [];
// TODO keep track: what preprocessor generated what sourcemap? to make debugging easier = detect low-resolution sourcemaps in fn combine_mappings const content_code = processed_content_to_code(processed, get_location(tag_open.length), file_basename);
for (const fn of markup) { return tag_open_code.concat(content_code).concat(tag_close_code);
}
function parse_tag_attributes(str: string) {
// note: won't work with attribute values containing spaces.
return str
.split(/\s+/)
.filter(Boolean)
.reduce((attrs, attr) => {
const i = attr.indexOf('=');
const [key, value] = i > 0 ? [attr.slice(0, i), attr.slice(i+1)] : [attr];
const [, unquoted] = (value && value.match(/^['"](.*)['"]$/)) || [];
return { ...attrs, [key]: unquoted ?? value ?? true };
}, {});
}
/**
* Calculate the updates required to process all instances of the specified tag.
*/
async function process_tag(
tag_name: 'style' | 'script',
preprocessor: Preprocessor,
source: Source
): Promise<SourceUpdate> {
const { filename } = source;
const tag_regex =
tag_name === 'style'
? /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi
: /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
// run markup preprocessor const dependencies: string[] = [];
const processed = await fn({
content: source, async function process_single_tag(
tag_with_content: string,
attributes = '',
content = '',
tag_offset: number
): Promise<MappedCode> {
const no_change = () => MappedCode.from_source(slice_source(tag_with_content, tag_offset, source));
if (!attributes && !content) return no_change();
const processed = await preprocessor({
content: content || '',
attributes: parse_tag_attributes(attributes || ''),
filename filename
}); });
if (!processed) continue; if (!processed) return no_change();
if (processed.dependencies) dependencies.push(...processed.dependencies); if (processed.dependencies) dependencies.push(...processed.dependencies);
source = processed.code; if (!processed.map && processed.code === content) return no_change();
if (processed.map) {
sourcemap_list.unshift( return processed_tag_to_code(processed, tag_name, attributes, slice_source(content, tag_offset, source));
typeof(processed.map) === 'string' }
const { string, map } = await replace_in_code(tag_regex, process_single_tag, source);
return { string, map, dependencies };
}
async function process_markup(filename: string, process: MarkupPreprocessor, source: Source) {
const processed = await process({
content: source.source,
filename
});
if (processed) {
return {
string: processed.code,
map: processed.map
? // TODO: can we use decode_sourcemap?
typeof processed.map === 'string'
? JSON.parse(processed.map) ? JSON.parse(processed.map)
: processed.map : processed.map
); : undefined,
} dependencies: processed.dependencies
};
} else {
return {};
} }
}
async function preprocess_tag_content(tag_name: 'style' | 'script', preprocessor: Preprocessor) { export default async function preprocess(
const get_location = getLocator(source); source: string,
const tag_regex = tag_name === 'style' preprocessor: PreprocessorGroup | PreprocessorGroup[],
? /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi options?: { filename?: string }
: /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi; ): Promise<Processed> {
// @ts-ignore todo: doublecheck
const filename = (options && options.filename) || preprocessor.filename; // legacy
const preprocessors = preprocessor ? (Array.isArray(preprocessor) ? preprocessor : [preprocessor]) : [];
const markup = preprocessors.map(p => p.markup).filter(Boolean);
const script = preprocessors.map(p => p.script).filter(Boolean);
const style = preprocessors.map(p => p.style).filter(Boolean);
const result = new PreprocessResult(source, filename);
const res = await replace_async( // TODO keep track: what preprocessor generated what sourcemap?
file_basename, // to make debugging easier = detect low-resolution sourcemaps in fn combine_mappings
source,
get_location, for (const process of markup) {
tag_regex, result.update_source(await process_markup(filename, process, result));
async (match, attributes = '', content = '', offset) => {
const no_change = () => StringWithSourcemap.from_source(
file_basename, match, get_location(offset));
if (!attributes && !content) {
return no_change();
}
attributes = attributes || '';
content = content || '';
// run script preprocessor
const processed = await preprocessor({
content,
attributes: parse_attributes(attributes),
filename
});
if (processed && processed.dependencies) {
dependencies.push(...processed.dependencies);
}
if (!processed || !processed.map && processed.code === content) {
return no_change();
}
return get_replacement(file_basename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, `</${tag_name}>`);
}
);
source = res.string;
sourcemap_list.unshift(res.map);
} }
for (const fn of script) { for (const process of script) {
await preprocess_tag_content('script', fn); result.update_source(await process_tag('script', process, result));
} }
for (const fn of style) { for (const preprocess of style) {
await preprocess_tag_content('style', fn); result.update_source(await process_tag('style', preprocess, result));
} }
// Combine all the source maps for each preprocessor function into one return result.to_processed();
const map: RawSourceMap = combine_sourcemaps(
file_basename,
sourcemap_list
);
return {
// TODO return separated output, in future version where svelte.compile supports it:
// style: { code: styleCode, map: styleMap },
// script { code: scriptCode, map: scriptMap },
// markup { code: markupCode, map: markupMap },
code: source,
dependencies: [...new Set(dependencies)],
map: (map as object),
toString() {
return source;
}
};
} }

@ -0,0 +1,75 @@
import { MappedCode } from '../utils/mapped_code';
import { Source } from './types';
interface Replacement {
offset: number;
length: number;
replacement: MappedCode;
}
export function slice_source(
code_slice: string,
offset: number,
{ file_basename, filename, get_location }: Source
): Source {
return {
source: code_slice,
get_location: (index: number) => get_location(index + offset),
file_basename,
filename
};
}
function calculate_replacements(
re: RegExp,
get_replacement: (...match: any[]) => Promise<MappedCode>,
source: string
) {
const replacements: Array<Promise<Replacement>> = [];
source.replace(re, (...match) => {
replacements.push(
get_replacement(...match).then(
replacement => {
const matched_string = match[0];
const offset = match[match.length-2];
return ({ offset, length: matched_string.length, replacement });
}
)
);
return '';
});
return Promise.all(replacements);
}
function perform_replacements(
replacements: Replacement[],
source: Source
): MappedCode {
const out = new MappedCode();
let last_end = 0;
for (const { offset, length, replacement } of replacements) {
const unchanged_prefix = MappedCode.from_source(
slice_source(source.source.slice(last_end, offset), last_end, source)
);
out.concat(unchanged_prefix).concat(replacement);
last_end = offset + length;
}
const unchanged_suffix = MappedCode.from_source(slice_source(source.source.slice(last_end), last_end, source));
return out.concat(unchanged_suffix);
}
export async function replace_in_code(
regex: RegExp,
get_replacement: (...match: any[]) => Promise<MappedCode>,
location: Source
): Promise<MappedCode> {
const replacements = await calculate_replacements(regex, get_replacement, location.source);
return perform_replacements(replacements, location);
}

@ -0,0 +1,32 @@
import { Location } from 'locate-character';
export interface Source {
source: string;
get_location: (search: number) => Location;
file_basename: string;
filename: string;
}
export interface Processed {
code: string;
map?: string | object; // we are opaque with the type here to avoid dependency on the remapping module for our public types.
dependencies?: string[];
toString?: () => string;
}
export type MarkupPreprocessor = (options: {
content: string;
filename: string;
}) => Processed | Promise<Processed>;
export type Preprocessor = (options: {
content: string;
attributes: Record<string, string | boolean>;
filename?: string;
}) => Processed | Promise<Processed>;
export interface PreprocessorGroup {
markup?: MarkupPreprocessor;
style?: Preprocessor;
script?: Preprocessor;
}

@ -1,8 +1,9 @@
import { DecodedSourceMap, RawSourceMap, SourceMapLoader } from '@ampproject/remapping/dist/types/types'; import { DecodedSourceMap, RawSourceMap, SourceMapLoader } from '@ampproject/remapping/dist/types/types';
import remapping from '@ampproject/remapping'; import remapping from '@ampproject/remapping';
import { SourceMap } from 'magic-string'; import { SourceMap } from 'magic-string';
import { Source, Processed } from '../preprocess/types';
type SourceLocation = { export type SourceLocation = {
line: number; line: number;
column: number; column: number;
}; };
@ -67,7 +68,7 @@ function pushArray<T>(_this: T[], other: T[]) {
} }
} }
export class StringWithSourcemap { export class MappedCode {
string: string; string: string;
map: DecodedSourceMap; map: DecodedSourceMap;
@ -89,7 +90,7 @@ export class StringWithSourcemap {
* concat in-place (mutable), return this (chainable) * concat in-place (mutable), return this (chainable)
* will also mutate the `other` object * will also mutate the `other` object
*/ */
concat(other: StringWithSourcemap): StringWithSourcemap { concat(other: MappedCode): MappedCode {
// noop: if one is empty, return the other // noop: if one is empty, return the other
if (other.string == '') return this; if (other.string == '') return this;
if (this.string == '') { if (this.string == '') {
@ -166,34 +167,34 @@ export class StringWithSourcemap {
return this; return this;
} }
static from_processed(string: string, map?: DecodedSourceMap): StringWithSourcemap { static from_processed(string: string, map?: DecodedSourceMap): MappedCode {
const line_count = string.split('\n').length; const line_count = string.split('\n').length;
if (map) { if (map) {
// ensure that count of source map mappings lines // ensure that count of source map mappings lines
// is equal to count of generated code lines // is equal to count of generated code lines
// (some tools may produce less) // (some tools may produce less)
const missing_lines = line_count - map.mappings.length; const missing_lines = line_count - map.mappings.length;
for (let i = 0; i < missing_lines; i++) { for (let i = 0; i < missing_lines; i++) {
map.mappings.push([]); map.mappings.push([]);
} }
return new StringWithSourcemap(string, map); return new MappedCode(string, map);
} }
if (string == '') return new StringWithSourcemap(); if (string == '') return new MappedCode();
map = { version: 3, names: [], sources: [], mappings: [] }; map = { version: 3, names: [], sources: [], mappings: [] };
// add empty SourceMapSegment[] for every line // add empty SourceMapSegment[] for every line
for (let i = 0; i < line_count; i++) map.mappings.push([]); for (let i = 0; i < line_count; i++) map.mappings.push([]);
return new StringWithSourcemap(string, map); return new MappedCode(string, map);
} }
static from_source( static from_source({ source, file_basename, get_location }: Source): MappedCode {
source_file: string, source: string, offset?: SourceLocation let offset: SourceLocation = get_location(0);
): StringWithSourcemap {
if (!offset) offset = { line: 0, column: 0 }; if (!offset) offset = { line: 0, column: 0 };
const map: DecodedSourceMap = { version: 3, names: [], sources: [source_file], mappings: [] }; const map: DecodedSourceMap = { version: 3, names: [], sources: [file_basename], mappings: [] };
if (source == '') return new StringWithSourcemap(source, map); if (source == '') return new MappedCode(source, map);
// we create a high resolution identity map here, // we create a high resolution identity map here,
// we know that it will eventually be merged with svelte's map, // we know that it will eventually be merged with svelte's map,
@ -213,7 +214,7 @@ export class StringWithSourcemap {
for (let segment = 0; segment < segment_list.length; segment++) { for (let segment = 0; segment < segment_list.length; segment++) {
segment_list[segment][3] += offset.column; segment_list[segment][3] += offset.column;
} }
return new StringWithSourcemap(source, map); return new MappedCode(source, map);
} }
} }
@ -255,6 +256,7 @@ export function combine_sourcemaps(
// browser vs node.js // browser vs node.js
const b64enc = typeof btoa == 'function' ? btoa : b => Buffer.from(b).toString('base64'); const b64enc = typeof btoa == 'function' ? btoa : b => Buffer.from(b).toString('base64');
const b64dec = typeof atob == 'function' ? atob : a => Buffer.from(a, 'base64').toString();
export function apply_preprocessor_sourcemap(filename: string, svelte_map: SourceMap, preprocessor_map_input: string | DecodedSourceMap | RawSourceMap): SourceMap { export function apply_preprocessor_sourcemap(filename: string, svelte_map: SourceMap, preprocessor_map_input: string | DecodedSourceMap | RawSourceMap): SourceMap {
if (!svelte_map || !preprocessor_map_input) return svelte_map; if (!svelte_map || !preprocessor_map_input) return svelte_map;
@ -288,3 +290,39 @@ export function apply_preprocessor_sourcemap(filename: string, svelte_map: Sourc
return result_map as SourceMap; return result_map as SourceMap;
} }
// parse attached sourcemap in processed.code
export function parse_attached_sourcemap(processed: Processed, tag_name: 'script' | 'style'): void {
const r_in = '[#@]\\s*sourceMappingURL\\s*=\\s*(\\S*)';
const regex = (tag_name == 'script')
? new RegExp('(?://'+r_in+')|(?:/\\*'+r_in+'\\s*\\*/)$')
: new RegExp('/\\*'+r_in+'\\s*\\*/$');
function log_warning(message) {
// code_start: help to find preprocessor
const code_start = processed.code.length < 100 ? processed.code : (processed.code.slice(0, 100) + ' [...]');
console.warn(`warning: ${message}. processed.code = ${JSON.stringify(code_start)}`);
}
processed.code = processed.code.replace(regex, (_, match1, match2) => {
const map_url = (tag_name == 'script') ? (match1 || match2) : match1;
const map_data = (map_url.match(/data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/) || [])[1];
if (map_data) {
// sourceMappingURL is data URL
if (processed.map) {
log_warning('Not implemented. ' +
'Found sourcemap in both processed.code and processed.map. ' +
'Please update your preprocessor to return only one sourcemap.');
// ignore attached sourcemap
return '';
}
processed.map = b64dec(map_data); // use attached sourcemap
return ''; // remove from processed.code
}
// sourceMappingURL is path or URL
if (!processed.map) {
log_warning(`Found sourcemap path ${JSON.stringify(map_url)} in processed.code, but no sourcemap data. ` +
'Please update your preprocessor to return sourcemap data directly.');
}
// ignore sourcemap path
return ''; // remove from processed.code
});
}

@ -1,3 +1,6 @@
// The `foreign` namespace covers all DOM implementations that aren't HTML5.
// It opts out of HTML5-specific a11y checks and case-insensitive attribute names.
export const foreign = 'https://svelte.dev/docs#svelte_options';
export const html = 'http://www.w3.org/1999/xhtml'; export const html = 'http://www.w3.org/1999/xhtml';
export const mathml = 'http://www.w3.org/1998/Math/MathML'; export const mathml = 'http://www.w3.org/1998/Math/MathML';
export const svg = 'http://www.w3.org/2000/svg'; export const svg = 'http://www.w3.org/2000/svg';
@ -6,12 +9,14 @@ export const xml = 'http://www.w3.org/XML/1998/namespace';
export const xmlns = 'http://www.w3.org/2000/xmlns'; export const xmlns = 'http://www.w3.org/2000/xmlns';
export const valid_namespaces = [ export const valid_namespaces = [
'foreign',
'html', 'html',
'mathml', 'mathml',
'svg', 'svg',
'xlink', 'xlink',
'xml', 'xml',
'xmlns', 'xmlns',
foreign,
html, html,
mathml, mathml,
svg, svg,
@ -20,4 +25,4 @@ export const valid_namespaces = [
xmlns xmlns
]; ];
export const namespaces: Record<string, string> = { html, mathml, svg, xlink, xml, xmlns }; export const namespaces: Record<string, string> = { foreign, html, mathml, svg, xlink, xml, xmlns };

@ -16,7 +16,7 @@ interface FlipParams {
easing?: (t: number) => number; easing?: (t: number) => number;
} }
export function flip(node: Element, animation: { from: DOMRect; to: DOMRect }, params: FlipParams): AnimationConfig { export function flip(node: Element, animation: { from: DOMRect; to: DOMRect }, params: FlipParams = {}): AnimationConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
const scaleX = animation.from.width / node.clientWidth; const scaleX = animation.from.width / node.clientWidth;

@ -100,8 +100,6 @@ export function init(component, options, instance, create_fragment, not_equal, p
const parent_component = current_component; const parent_component = current_component;
set_current_component(component); set_current_component(component);
const prop_values = options.props || {};
const $$: T$$ = component.$$ = { const $$: T$$ = component.$$ = {
fragment: null, fragment: null,
ctx: null, ctx: null,
@ -128,7 +126,7 @@ export function init(component, options, instance, create_fragment, not_equal, p
let ready = false; let ready = false;
$$.ctx = instance $$.ctx = instance
? instance(component, prop_values, (i, ret, ...rest) => { ? instance(component, options.props || {}, (i, ret, ...rest) => {
const value = rest.length ? rest[0] : ret; const value = rest.length ? rest[0] : ret;
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) { if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value); if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);

@ -28,7 +28,9 @@ export function handle_promise(promise, info) {
if (i !== index && block) { if (i !== index && block) {
group_outros(); group_outros();
transition_out(block, 1, 1, () => { transition_out(block, 1, 1, () => {
info.blocks[i] = null; if (info.blocks[i] === block) {
info.blocks[i] = null;
}
}); });
check_outros(); check_outros();
} }

@ -115,6 +115,20 @@ export class SvelteComponentDev extends SvelteComponent {
* ### DO NOT USE! * ### DO NOT USE!
*/ */
$$prop_def: Props; $$prop_def: Props;
/**
* @private
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
*/
$$events_def: any;
/**
* @private
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
*/
$$slot_def: any;
constructor(options: { constructor(options: {
target: Element; target: Element;

@ -14,7 +14,7 @@ function tick_spring<T>(ctx: TickContext<T>, last_value: T, current_value: T, ta
// @ts-ignore // @ts-ignore
const delta = target_value - current_value; const delta = target_value - current_value;
// @ts-ignore // @ts-ignore
const velocity = (current_value - last_value) / (ctx.dt||1/60); // guard div by 0 const velocity = (current_value - last_value) / (ctx.dt || 1 / 60); // guard div by 0
const spring = ctx.opts.stiffness * delta; const spring = ctx.opts.stiffness * delta;
const damper = ctx.opts.damping * velocity; const damper = ctx.opts.damping * velocity;
const acceleration = (spring - damper) * ctx.inv_mass; const acceleration = (spring - damper) * ctx.inv_mass;
@ -80,7 +80,7 @@ export function spring<T=any>(value?: T, opts: SpringOpts = {}): Spring<T> {
let inv_mass_recovery_rate = 0; let inv_mass_recovery_rate = 0;
let cancel_task = false; let cancel_task = false;
function set(new_value: T, opts: SpringUpdateOpts={}): Promise<void> { function set(new_value: T, opts: SpringUpdateOpts = {}): Promise<void> {
target_value = new_value; target_value = new_value;
const token = current_token = {}; const token = current_token = {};

@ -125,10 +125,12 @@ type StoresValues<T> = T extends Readable<infer U> ? U :
* *
* @param stores - input stores * @param stores - input stores
* @param fn - function callback that aggregates the values * @param fn - function callback that aggregates the values
* @param initial_value - when used asynchronously
*/ */
export function derived<S extends Stores, T>( export function derived<S extends Stores, T>(
stores: S, stores: S,
fn: (values: StoresValues<S>) => T fn: (values: StoresValues<S>, set: (value: T) => void) => Unsubscriber | void,
initial_value?: T
): Readable<T>; ): Readable<T>;
/** /**
@ -137,12 +139,10 @@ export function derived<S extends Stores, T>(
* *
* @param stores - input stores * @param stores - input stores
* @param fn - function callback that aggregates the values * @param fn - function callback that aggregates the values
* @param initial_value - when used asynchronously
*/ */
export function derived<S extends Stores, T>( export function derived<S extends Stores, T>(
stores: S, stores: S,
fn: (values: StoresValues<S>, set: (value: T) => void) => Unsubscriber | void, fn: (values: StoresValues<S>) => T
initial_value?: T
): Readable<T>; ): Readable<T>;
export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Readable<T> { export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Readable<T> {

@ -25,7 +25,7 @@ export function blur(node: Element, {
easing = cubicInOut, easing = cubicInOut,
amount = 5, amount = 5,
opacity = 0 opacity = 0
}: BlurParams): TransitionConfig { }: BlurParams = {}): TransitionConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const target_opacity = +style.opacity; const target_opacity = +style.opacity;
const f = style.filter === 'none' ? '' : style.filter; const f = style.filter === 'none' ? '' : style.filter;
@ -50,7 +50,7 @@ export function fade(node: Element, {
delay = 0, delay = 0,
duration = 400, duration = 400,
easing = linear easing = linear
}: FadeParams): TransitionConfig { }: FadeParams = {}): TransitionConfig {
const o = +getComputedStyle(node).opacity; const o = +getComputedStyle(node).opacity;
return { return {
@ -77,7 +77,7 @@ export function fly(node: Element, {
x = 0, x = 0,
y = 0, y = 0,
opacity = 0 opacity = 0
}: FlyParams): TransitionConfig { }: FlyParams = {}): TransitionConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const target_opacity = +style.opacity; const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
@ -104,7 +104,7 @@ export function slide(node: Element, {
delay = 0, delay = 0,
duration = 400, duration = 400,
easing = cubicOut easing = cubicOut
}: SlideParams): TransitionConfig { }: SlideParams = {}): TransitionConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const opacity = +style.opacity; const opacity = +style.opacity;
const height = parseFloat(style.height); const height = parseFloat(style.height);
@ -146,7 +146,7 @@ export function scale(node: Element, {
easing = cubicOut, easing = cubicOut,
start = 0, start = 0,
opacity = 0 opacity = 0
}: ScaleParams): TransitionConfig { }: ScaleParams = {}): TransitionConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const target_opacity = +style.opacity; const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
@ -177,7 +177,7 @@ export function draw(node: SVGElement & { getTotalLength(): number }, {
speed, speed,
duration, duration,
easing = cubicInOut easing = cubicInOut
}: DrawParams): TransitionConfig { }: DrawParams = {}): TransitionConfig {
const len = node.getTotalLength(); const len = node.getTotalLength();
if (duration === undefined) { if (duration === undefined) {
@ -237,7 +237,7 @@ export function crossfade({ fallback, ...defaults }: CrossfadeParams & {
css: (t, u) => ` css: (t, u) => `
opacity: ${t * opacity}; opacity: ${t * opacity};
transform-origin: top left; transform-origin: top left;
transform: ${transform} translate(${u * dx}px,${u * dy}px) scale(${t + (1-t) * dw}, ${t + (1-t) * dh}); transform: ${transform} translate(${u * dx}px,${u * dy}px) scale(${t + (1 - t) * dw}, ${t + (1 - t) * dh});
` `
}; };
} }

@ -0,0 +1,167 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
component_subscribe,
detach,
element,
init,
insert,
noop,
safe_not_equal,
space,
subscribe,
toggle_class
} from "svelte/internal";
import { reactiveStoreVal, unreactiveExport } from "./store";
function create_fragment(ctx) {
let div0;
let t0;
let div1;
let t1;
let div2;
let t2;
let div3;
let t3;
let div4;
let t4;
let div5;
let t5;
let div6;
let t6;
let div7;
let t7;
let div8;
return {
c() {
div0 = element("div");
t0 = space();
div1 = element("div");
t1 = space();
div2 = element("div");
t2 = space();
div3 = element("div");
t3 = space();
div4 = element("div");
t4 = space();
div5 = element("div");
t5 = space();
div6 = element("div");
t6 = space();
div7 = element("div");
t7 = space();
div8 = element("div");
toggle_class(div0, "update1", reactiveModuleVar);
toggle_class(div1, "update2", /*reactiveConst*/ ctx[0].x);
toggle_class(div2, "update3", nonReactiveGlobal && /*reactiveConst*/ ctx[0].x);
toggle_class(div3, "update4", /*$reactiveStoreVal*/ ctx[2]);
toggle_class(div4, "update5", /*$reactiveDeclaration*/ ctx[3]);
toggle_class(div5, "static1", nonReactiveModuleVar);
toggle_class(div6, "static2", nonReactiveGlobal);
toggle_class(div7, "static3", nonReactiveModuleVar && nonReactiveGlobal);
toggle_class(div8, "static4", unreactiveExport);
},
m(target, anchor) {
insert(target, div0, anchor);
insert(target, t0, anchor);
insert(target, div1, anchor);
insert(target, t1, anchor);
insert(target, div2, anchor);
insert(target, t2, anchor);
insert(target, div3, anchor);
insert(target, t3, anchor);
insert(target, div4, anchor);
insert(target, t4, anchor);
insert(target, div5, anchor);
insert(target, t5, anchor);
insert(target, div6, anchor);
insert(target, t6, anchor);
insert(target, div7, anchor);
insert(target, t7, anchor);
insert(target, div8, anchor);
},
p(ctx, [dirty]) {
if (dirty & /*reactiveModuleVar*/ 0) {
toggle_class(div0, "update1", reactiveModuleVar);
}
if (dirty & /*reactiveConst*/ 1) {
toggle_class(div1, "update2", /*reactiveConst*/ ctx[0].x);
}
if (dirty & /*nonReactiveGlobal, reactiveConst*/ 1) {
toggle_class(div2, "update3", nonReactiveGlobal && /*reactiveConst*/ ctx[0].x);
}
if (dirty & /*$reactiveStoreVal*/ 4) {
toggle_class(div3, "update4", /*$reactiveStoreVal*/ ctx[2]);
}
if (dirty & /*$reactiveDeclaration*/ 8) {
toggle_class(div4, "update5", /*$reactiveDeclaration*/ ctx[3]);
}
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(div0);
if (detaching) detach(t0);
if (detaching) detach(div1);
if (detaching) detach(t1);
if (detaching) detach(div2);
if (detaching) detach(t2);
if (detaching) detach(div3);
if (detaching) detach(t3);
if (detaching) detach(div4);
if (detaching) detach(t4);
if (detaching) detach(div5);
if (detaching) detach(t5);
if (detaching) detach(div6);
if (detaching) detach(t6);
if (detaching) detach(div7);
if (detaching) detach(t7);
if (detaching) detach(div8);
}
};
}
let nonReactiveModuleVar = Math.random();
let reactiveModuleVar = Math.random();
function instance($$self, $$props, $$invalidate) {
let reactiveDeclaration;
let $reactiveStoreVal;
let $reactiveDeclaration,
$$unsubscribe_reactiveDeclaration = noop,
$$subscribe_reactiveDeclaration = () => ($$unsubscribe_reactiveDeclaration(), $$unsubscribe_reactiveDeclaration = subscribe(reactiveDeclaration, $$value => $$invalidate(3, $reactiveDeclaration = $$value)), reactiveDeclaration);
component_subscribe($$self, reactiveStoreVal, $$value => $$invalidate(2, $reactiveStoreVal = $$value));
$$self.$$.on_destroy.push(() => $$unsubscribe_reactiveDeclaration());
nonReactiveGlobal = Math.random();
const reactiveConst = { x: Math.random() };
reactiveModuleVar += 1;
if (Math.random()) {
reactiveConst.x += 1;
}
$$self.$$.update = () => {
if ($$self.$$.dirty & /*reactiveModuleVar*/ 0) {
$: $$subscribe_reactiveDeclaration($$invalidate(1, reactiveDeclaration = reactiveModuleVar * 2));
}
};
return [reactiveConst, reactiveDeclaration, $reactiveStoreVal, $reactiveDeclaration];
}
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
export default Component;

@ -0,0 +1,31 @@
<script context="module">
let nonReactiveModuleVar = Math.random();
let reactiveModuleVar = Math.random();
</script>
<script>
import { reactiveStoreVal, unreactiveExport } from './store';
nonReactiveGlobal = Math.random();
const reactiveConst = {x: Math.random()};
$: reactiveDeclaration = reactiveModuleVar * 2;
reactiveModuleVar += 1;
if (Math.random()) {
reactiveConst.x += 1;
}
</script>
<!--These should all get updaters because they have at least one reactive dependency-->
<div class:update1={reactiveModuleVar}></div>
<div class:update2={reactiveConst.x}></div>
<div class:update3={nonReactiveGlobal && reactiveConst.x}></div>
<div class:update4={$reactiveStoreVal}></div>
<div class:update5={$reactiveDeclaration}></div>
<!--These shouldn't get updates because they're purely non-reactive-->
<div class:static1={nonReactiveModuleVar}></div>
<div class:static2={nonReactiveGlobal}></div>
<div class:static3={nonReactiveModuleVar && nonReactiveGlobal}></div>
<div class:static4={unreactiveExport}></div>

@ -0,0 +1,4 @@
import { writable } from '../../../../store';
export const reactiveStoreVal = writable(0);
export const unreactiveExport = true;

@ -0,0 +1 @@
<input use:autofocus use:autofocus>

@ -0,0 +1,34 @@
{
"html": {
"start": 0,
"end": 35,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 35,
"type": "Element",
"name": "input",
"attributes": [
{
"start": 7,
"end": 20,
"type": "Action",
"name": "autofocus",
"modifiers": [],
"expression": null
},
{
"start": 21,
"end": 34,
"type": "Action",
"name": "autofocus",
"modifiers": [],
"expression": null
}
],
"children": []
}
]
}
}

@ -0,0 +1,10 @@
{
"code": "invalid-class-directive",
"message": "Class binding name cannot be empty",
"start": {
"line": 1,
"column": 10,
"character": 10
},
"pos": 10
}

@ -0,0 +1,20 @@
// Test support for the `foreign` namespace preserving attribute case.
export default {
html: `
<page horizontalAlignment="center">
<button textWrap="true" text="button">
</page>
`,
options: {
hydrate: false // Hydration test will fail as case sensitivity is only handled for svg elements.
},
compileOptions: {
namespace: 'foreign'
},
test({ assert, target }) {
const attr = sel => target.querySelector(sel).attributes[0].name;
assert.equal(attr('page'), 'horizontalAlignment');
assert.equal(attr('button'), 'textWrap');
}
};

@ -0,0 +1,3 @@
<page horizontalAlignment="center">
<button textWrap="true" text="button">
</page>

@ -0,0 +1,18 @@
// Test support for the `foreign` namespace preserving attribute case.
export default {
html: `
<page horizontalAlignment="center">
<button textWrap="true" text="button">
</page>
`,
options: {
hydrate: false // Hydration test will fail as case sensitivity is only handled for svg elements.
},
test({ assert, target }) {
const attr = sel => target.querySelector(sel).attributes[0].name;
assert.equal(attr('page'), 'horizontalAlignment');
assert.equal(attr('button'), 'textWrap');
}
};

@ -0,0 +1,4 @@
<svelte:options namespace="foreign" />
<page horizontalAlignment="center">
<button textWrap="true" text="button">
</page>

@ -0,0 +1,13 @@
let result;
export default {
before_test() {
result = [];
},
props: {
collect: (str) => result.push(str)
},
test({ assert }) {
assert.deepEqual(result, ['each_action', 'import_action']);
}
};

@ -0,0 +1,17 @@
<script>
import action from './util.js';
export let collect;
function each_action(_, fn) {
fn('each_action');
}
const array = [each_action];
</script>
<div use:action={collect} />
<ul>
{#each array as action}
<div use:action={collect} />
{/each}
</ul>

@ -0,0 +1,3 @@
export default function (_, fn) {
fn('import_action');
}

@ -1,6 +1,5 @@
// destructure to store value // destructure to store value
export default { export default {
skip_if_ssr: true, // pending https://github.com/sveltejs/svelte/issues/3582
html: '<h1>2 2 xxx 5 6 9 10 2</h1>', html: '<h1>2 2 xxx 5 6 9 10 2</h1>',
async test({ assert, target, component }) { async test({ assert, target, component }) {
await component.update(); await component.update();

@ -0,0 +1,20 @@
import { store } from './store.js';
export default {
html: '<h1>0</h1>',
before_test() {
store.reset();
},
async test({ assert, target, component }) {
store.set(42);
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<h1>42</h1>');
assert.equal(store.numberOfTimesSubscribeCalled(), 1);
},
test_ssr({ assert }) {
assert.equal(store.numberOfTimesSubscribeCalled(), 1);
}
};

@ -0,0 +1,5 @@
<script>
import { store } from './store';
</script>
<h1>{$store}</h1>

@ -0,0 +1,18 @@
import { writable } from '../../../../store';
const _store = writable(0);
let count = 0;
export const store = {
..._store,
subscribe(fn) {
count++;
return _store.subscribe(fn);
},
reset() {
count = 0;
_store.set(0);
},
numberOfTimesSubscribeCalled() {
return count;
}
};

@ -1,7 +1,6 @@
// destructure to store // destructure to store
export default { export default {
html: '<h1>2 2 xxx 5 6 9 10 2</h1>', html: '<h1>2 2 xxx 5 6 9 10 2</h1>',
skip_if_ssr: true,
async test({ assert, target, component }) { async test({ assert, target, component }) {
await component.update(); await component.update();
assert.htmlEqual(target.innerHTML, '<h1>11 11 yyy 12 13 14 15 11</h1>'); assert.htmlEqual(target.innerHTML, '<h1>11 11 yyy 12 13 14 15 11</h1>');

@ -5,7 +5,6 @@ export default {
<button></button> <button></button>
<button></button> <button></button>
`, `,
skip_if_ssr: true,
async test({ assert, component, target, window }) { async test({ assert, component, target, window }) {
const [btn1, btn2] = target.querySelectorAll('button'); const [btn1, btn2] = target.querySelectorAll('button');

@ -0,0 +1,3 @@
export default {
html: '{"answer":4}'
};

@ -0,0 +1,31 @@
<script>
import { writable } from '../../../../store';
let value = writable({ foo: 1, bar: 2 });
$value.foo = $value.foo + $value.bar; // 3
$value.bar = $value.foo * $value.bar; // 6
// should resubscribe immediately
value = writable({ foo: $value.foo + 2, bar: $value.bar - 2 }); // { foo: 5, bar: 4 }
// should mutate the store value
$value.baz = $value.foo + $value.bar; // { foo: 5, bar: 4, baz: 9 }
// should resubscribe immediately
value = writable({ qux: $value.baz - $value.foo }); // { qux: 4 }
// making sure instrumentation returns the expression value
$value = {
one: writable(
$value = {
two: ({ $value } = { $value: { fred: $value.qux } }) // { fred: 4 }
} // { two: { $value: { fred: 4 } } }
) // { one: { two: { $value: { fred: 4 } } } }
};
const one = $value.one;
value.update(val => ({ answer: $one.two.$value.fred })); // { answer: 4 }
</script>
{JSON.stringify($value)}

@ -0,0 +1,174 @@
let fulfil;
export default {
props: {
promise: new Promise((f) => {
fulfil = f;
})
},
intro: true,
async test({ assert, target, component, raf }) {
assert.htmlEqual(target.innerHTML, '<p class="pending" foo="0.0">loading...</p>');
let time = 0;
raf.tick(time += 50);
assert.htmlEqual(target.innerHTML, '<p class="pending" foo="0.5">loading...</p>');
await fulfil(42);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.0">42</p>
<p class="pending" foo="0.5">loading...</p>
`);
// see the transition 30% complete
raf.tick(time += 30);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.3">42</p>
<p class="pending" foo="0.2">loading...</p>
`);
// completely transition in the {:then} block
raf.tick(time += 70);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="1.0">42</p>
`);
// update promise #1
component.promise = new Promise((f) => {
fulfil = f;
});
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="1.0">42</p>
<p class="pending" foo="0.0">loading...</p>
`);
raf.tick(time += 100);
assert.htmlEqual(target.innerHTML, `
<p class="pending" foo="1.0">loading...</p>
`);
await fulfil(43);
assert.htmlEqual(target.innerHTML, `
<p class="pending" foo="1.0">loading...</p>
<p class="then" foo="0.0">43</p>
`);
raf.tick(time += 100);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="1.0">43</p>
`);
// update promise #2
component.promise = new Promise((f) => {
fulfil = f;
});
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="1.0">43</p>
<p class="pending" foo="0.0">loading...</p>
`);
raf.tick(time += 50);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.5">43</p>
<p class="pending" foo="0.5">loading...</p>
`);
await fulfil(44);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.5">43</p>
<p class="pending" foo="0.5">loading...</p>
<p class="then" foo="0.0">44</p>
`);
raf.tick(time += 100);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="1.0">44</p>
`);
// update promise #3 - quick succession
component.promise = new Promise((f) => {
fulfil = f;
});
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="1.0">44</p>
<p class="pending" foo="0.0">loading...</p>
`);
raf.tick(time += 40);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.6">44</p>
<p class="pending" foo="0.4">loading...</p>
`);
await fulfil(45);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.6">44</p>
<p class="pending" foo="0.4">loading...</p>
<p class="then" foo="0.0">45</p>
`);
raf.tick(time += 20);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.4">44</p>
<p class="pending" foo="0.2">loading...</p>
<p class="then" foo="0.2">45</p>
`);
component.promise = new Promise((f) => {
fulfil = f;
});
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.4">44</p>
<p class="pending" foo="0.2">loading...</p>
<p class="then" foo="0.2">45</p>
<p class="pending" foo="0.0">loading...</p>
`);
raf.tick(time += 10);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.3">44</p>
<p class="pending" foo="0.1">loading...</p>
<p class="then" foo="0.1">45</p>
<p class="pending" foo="0.1">loading...</p>
`);
await fulfil(46);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.3">44</p>
<p class="pending" foo="0.1">loading...</p>
<p class="then" foo="0.1">45</p>
<p class="pending" foo="0.1">loading...</p>
<p class="then" foo="0.0">46</p>
`);
raf.tick(time += 10);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.2">44</p>
<p class="then" foo="0.1">46</p>
`);
raf.tick(time += 20);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.3">46</p>
`);
raf.tick(time += 70);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="1.0">46</p>
`);
}
};

@ -0,0 +1,20 @@
<script>
export let promise;
function foo(node) {
return {
duration: 100,
tick: t => {
node.setAttribute('foo', t.toFixed(1));
}
};
}
</script>
{#await promise}
<p class='pending' transition:foo>loading...</p>
{:then value}
<p class='then' transition:foo>{value}</p>
{:catch error}
<p class='catch' transition:foo>{error.message}</p>
{/await}

@ -0,0 +1,38 @@
export default {
async test({ assert, component, target, window, raf }) {
const t = target.querySelector('#t');
await (component.condition = false);
let time = 0;
raf.tick(time += 25);
assert.htmlEqual(target.innerHTML, `
<div id="t" foo="0.75">TRUE</div>
<div id="f">FALSE</div>
`);
// toggling back in the middle of the out transition
// will reuse the previous element
await (component.condition = true);
assert.htmlEqual(target.innerHTML, `
<div id="f">FALSE</div>
<div id="t" foo="1">TRUE</div>
`);
assert.equal(target.querySelector('#t'), t);
raf.tick(time += 25);
assert.htmlEqual(target.innerHTML, `
<div id="f" foo="0.75">FALSE</div>
<div id="t" foo="1">TRUE</div>
`);
raf.tick(time += 75);
assert.htmlEqual(target.innerHTML, `
<div id="t" foo="1">TRUE</div>
`);
}
};

@ -0,0 +1,18 @@
<script>
export let condition = true;
function foo(node) {
return {
duration: 100,
tick: t => {
node.setAttribute('foo', t);
}
};
}
</script>
{#if condition}
<div id="t" out:foo>TRUE</div>
{:else}
<div id="f" out:foo>FALSE</div>
{/if}

@ -33,6 +33,10 @@ describe('ssr', () => {
return setupHtmlEqual(); return setupHtmlEqual();
}); });
let saved_window;
before(() => saved_window = global.window);
after(() => global.window = saved_window);
fs.readdirSync(`${__dirname}/samples`).forEach(dir => { fs.readdirSync(`${__dirname}/samples`).forEach(dir => {
if (dir[0] === '.') return; if (dir[0] === '.') return;
@ -197,6 +201,10 @@ describe('ssr', () => {
assert.htmlEqual(html, config.html); assert.htmlEqual(html, config.html);
} }
if (config.test_ssr) {
config.test_ssr({ assert });
}
if (config.after_test) config.after_test(); if (config.after_test) config.after_test();
if (config.show) { if (config.show) {

@ -31,7 +31,8 @@ describe('sourcemaps', () => {
const inputCode = fs.readFileSync(inputFile, 'utf-8'); const inputCode = fs.readFileSync(inputFile, 'utf-8');
const input = { const input = {
code: inputCode, code: inputCode,
locate: getLocator(inputCode) locate: getLocator(inputCode),
locate_1: getLocator(inputCode, { offsetLine: 1 })
}; };
const preprocessed = await svelte.preprocess( const preprocessed = await svelte.preprocess(
@ -41,7 +42,7 @@ describe('sourcemaps', () => {
filename: 'input.svelte' filename: 'input.svelte'
} }
); );
const { js, css } = svelte.compile( const { js, css } = svelte.compile(
preprocessed.code, { preprocessed.code, {
filename: 'input.svelte', filename: 'input.svelte',
@ -86,12 +87,14 @@ describe('sourcemaps', () => {
assert.deepEqual( assert.deepEqual(
js.map.sources.slice().sort(), js.map.sources.slice().sort(),
(config.js_map_sources || ['input.svelte']).sort() (config.js_map_sources || ['input.svelte']).sort(),
'js.map.sources is wrong'
); );
if (css.map) { if (css.map) {
assert.deepEqual( assert.deepEqual(
css.map.sources.slice().sort(), css.map.sources.slice().sort(),
(config.css_map_sources || ['input.svelte']).sort() (config.css_map_sources || ['input.svelte']).sort(),
'css.map.sources is wrong'
); );
} }

@ -0,0 +1,44 @@
import MagicString from 'magic-string';
let indent_size = 4;
let comment_multi = true;
function get_processor(tag_name, search, replace) {
return {
[tag_name]: ({ content, filename }) => {
let code = content.slice();
const ms = new MagicString(code);
const idx = ms.original.indexOf(search);
if (idx == -1) throw new Error('search not found in src');
ms.overwrite(idx, idx + search.length, replace, { storeName: true });
// change line + column
const indent = Array.from({ length: indent_size }).join(' ');
ms.prependLeft(idx, '\n'+indent);
const map_opts = { source: filename, hires: true, includeContent: false };
const map = ms.generateMap(map_opts);
const attach_line = (tag_name == 'style' || comment_multi)
? `\n/*# sourceMappingURL=${map.toUrl()} */`
: `\n//# sourceMappingURL=${map.toUrl()}` // only in script
;
code = ms.toString() + attach_line;
indent_size += 2;
if (tag_name == 'script') comment_multi = !comment_multi;
return { code };
}
};
}
export default {
preprocess: [
get_processor('script', 'replace_me_script', 'done_replace_script_1'),
get_processor('script', 'done_replace_script_1', 'done_replace_script_2'),
get_processor('style', '.replace_me_style', '.done_replace_style_1'),
get_processor('style', '.done_replace_style_1', '.done_replace_style_2')
]
};

@ -0,0 +1,11 @@
<style>
.replace_me_style {
color: red;
}
</style>
<script>
let
replace_me_script = 'hello'
;
</script>
<h1 class="done_replace_style_2">{done_replace_script_2}</h1>

@ -0,0 +1,45 @@
import * as assert from 'assert';
const get_line_column = obj => ({ line: obj.line, column: obj.column });
export function test({ input, css, js }) {
let out_obj, loc_output, actual, loc_input, expected;
out_obj = js;
// we need the second occurence of 'done_replace_script_2' in output.js
// the first occurence is mapped back to markup '{done_replace_script_2}'
loc_output = out_obj.locate_1('done_replace_script_2');
loc_output = out_obj.locate_1('done_replace_script_2', loc_output.character + 1);
actual = out_obj.mapConsumer.originalPositionFor(loc_output);
loc_input = input.locate_1('replace_me_script');
expected = {
source: 'input.svelte',
name: 'replace_me_script',
...get_line_column(loc_input)
};
assert.deepEqual(actual, expected);
out_obj = css;
loc_output = out_obj.locate_1('.done_replace_style_2');
actual = out_obj.mapConsumer.originalPositionFor(loc_output);
loc_input = input.locate_1('.replace_me_style');
expected = {
source: 'input.svelte',
name: '.replace_me_style',
...get_line_column(loc_input)
};
assert.deepEqual(actual, expected);
assert.equal(
js.code.indexOf('\n/*# sourceMappingURL=data:application/json;base64,'),
-1,
'magic-comment attachments were NOT removed'
);
assert.equal(
css.code.indexOf('\n/*# sourceMappingURL=data:application/json;base64,'),
-1,
'magic-comment attachments were NOT removed'
);
}

@ -6,7 +6,7 @@ export default {
style: async ({ content, filename }) => { style: async ({ content, filename }) => {
const src = new MagicString(content); const src = new MagicString(content);
const idx = content.indexOf('baritone'); const idx = content.indexOf('baritone');
src.overwrite(idx, idx+'baritone'.length, 'bar'); src.overwrite(idx, idx + 'baritone'.length, 'bar');
const map = SourceMapGenerator.fromSourceMap( const map = SourceMapGenerator.fromSourceMap(
await new SourceMapConsumer( await new SourceMapConsumer(

@ -19,7 +19,7 @@ export default {
preprocess: [ preprocess: [
{ {
style: ({ content, filename }) => { style: ({ content, filename }) => {
const external =`/* Filename from preprocess: ${filename} */` + external_code; const external = `/* Filename from preprocess: ${filename} */` + external_code;
return magic_string_bundle([ return magic_string_bundle([
{ code: external, filename: external_relative_filename }, { code: external, filename: external_relative_filename },
{ code: content, filename } { code: content, filename }

@ -101,4 +101,35 @@ describe('validate', () => {
assert.deepEqual(warnings, []); assert.deepEqual(warnings, []);
}); });
it('errors if namespace is provided but unrecognised', () => {
assert.throws(() => {
svelte.compile('<div></div>', {
name: 'test',
namespace: 'svefefe'
});
}, /Invalid namespace 'svefefe'/);
});
it('errors with a hint if namespace is provided but unrecognised but close', () => {
assert.throws(() => {
svelte.compile('<div></div>', {
name: 'test',
namespace: 'foriegn'
});
}, /Invalid namespace 'foriegn' \(did you mean 'foreign'\?\)/);
});
it('does not throw error if \'this\' is bound for foreign element', () => {
assert.doesNotThrow(() => {
svelte.compile(`
<script>
let whatever;
</script>
<div bind:this={whatever} />`, {
name: 'test',
namespace: 'foreign'
});
});
});
}); });

@ -0,0 +1,7 @@
<svelte:options namespace="foreign" />
<page>
<a>not actually a link</a>
<label>This isn't a html label</label>
<figure>This is maybe a QT figure</figure>
</page>

@ -0,0 +1,15 @@
[{
"code": "invalid-binding",
"message": "'value' is not a valid binding. Foreign elements only support bind:this",
"pos": 81,
"start": {
"line": 6,
"column": 7,
"character": 81
},
"end": {
"line": 6,
"column": 28,
"character": 102
}
}]

@ -0,0 +1,6 @@
<svelte:options namespace="foreign" />
<script>
let whatever;
</script>
<input bind:value={whatever} />
Loading…
Cancel
Save