Merge remote-tracking branch 'upstream/master' into feat/export-store-types

pull/5887/head
hantatsang 6 years ago
commit fd843a1d77

@ -1,9 +1,21 @@
# Svelte changelog # Svelte changelog
## Unreleased ## 3.32.1
* Warn when using `module` variables reactively, and close weird reactivity loophole ([#5847](https://github.com/sveltejs/svelte/pull/5847))
* Support attached sourcemaps as magic comment inside code from preprocessors ([#5854](https://github.com/sveltejs/svelte/pull/5854)) * 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 ## 3.31.2

2
package-lock.json generated

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

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

@ -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"`.
--- ---

@ -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';
@ -1175,15 +1175,20 @@ export default class Component {
extract_reactive_declarations() { extract_reactive_declarations() {
const component = this; const component = this;
const unsorted_reactive_declarations = []; const unsorted_reactive_declarations: Array<{
assignees: Set<string>;
dependencies: Set<string>;
node: Node;
declaration: Node;
}> = [];
this.ast.instance.content.body.forEach(node => { this.ast.instance.content.body.forEach(node => {
if (node.type === 'LabeledStatement' && node.label.name === '$') { if (node.type === 'LabeledStatement' && node.label.name === '$') {
this.reactive_declaration_nodes.add(node); this.reactive_declaration_nodes.add(node);
const assignees = new Set(); const assignees = new Set<string>();
const assignee_nodes = new Set(); const assignee_nodes = new Set();
const dependencies = new Set(); const dependencies = new Set<string>();
let scope = this.instance_scope; let scope = this.instance_scope;
const map = this.instance_scope_map; const map = this.instance_scope_map;
@ -1214,10 +1219,22 @@ export default class Component {
const { name } = identifier; const { name } = identifier;
const owner = scope.find_owner(name); const owner = scope.find_owner(name);
const variable = component.var_lookup.get(name); const variable = component.var_lookup.get(name);
if (variable) variable.is_reactive_dependency = true; let should_add_as_dependency = true;
if (variable) {
variable.is_reactive_dependency = true;
if (variable.module) {
should_add_as_dependency = false;
component.warn(node as any, {
code: 'module-script-reactive-declaration',
message: `"${name}" is declared in a module script and will not be reactive`
});
}
}
const is_writable_or_mutated = const is_writable_or_mutated =
variable && (variable.writable || variable.mutated); variable && (variable.writable || variable.mutated);
if ( if (
should_add_as_dependency &&
(!owner || owner === component.instance_scope) && (!owner || owner === component.instance_scope) &&
(name[0] === '$' || is_writable_or_mutated) (name[0] === '$' || is_writable_or_mutated)
) { ) {
@ -1349,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 = {}) {

@ -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() {

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

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

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

@ -51,7 +51,6 @@ export default function ssr(
return b` return b`
${component.compile_options.dev && b`@validate_store(${store_name}, '${store_name}');`} ${component.compile_options.dev && b`@validate_store(${store_name}, '${store_name}');`}
${`$$unsubscribe_${store_name}`} = @subscribe(${store_name}, #value => ${name} = #value) ${`$$unsubscribe_${store_name}`} = @subscribe(${store_name}, #value => ${name} = #value)
${store_name}.subscribe($$value => ${name} = $$value);
`; `;
}); });
const reactive_store_unsubscriptions = reactive_stores.map( const reactive_store_unsubscriptions = reactive_stores.map(

@ -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,327 +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 { import { MappedCode, SourceLocation, parse_attached_sourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/mapped_code';
StringWithSourcemap, import { decode_map } from './decode_sourcemap';
sourcemap_add_offset, import { replace_in_code, slice_source } from './replace_in_code';
combine_sourcemaps, import { MarkupPreprocessor, Source, Preprocessor, PreprocessorGroup, Processed } from './types';
parse_attached_sourcemap
} from '../utils/string_with_sourcemap'; interface SourceUpdate {
string?: string;
export interface Processed { map?: DecodedSourceMap;
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[]; 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) { if (map) {
let previous_generated_line = 1; this.sourcemap_list.unshift(map);
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 (dependencies) {
this.dependencies.push(...dependencies);
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 = { 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,
tag_name: 'script' | 'style'
): 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));
parse_attached_sourcemap(processed, tag_name);
// 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);
}
// run markup preprocessor function parse_tag_attributes(str: string) {
const processed = await fn({ // note: won't work with attribute values containing spaces.
content: source, 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;
const dependencies: string[] = [];
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}>`, 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,9 +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 { Processed } from '../preprocess'; import { Source, Processed } from '../preprocess/types';
type SourceLocation = { export type SourceLocation = {
line: number; line: number;
column: number; column: number;
}; };
@ -68,7 +68,7 @@ function pushArray<T>(_this: T[], other: T[]) {
} }
} }
export class StringWithSourcemap { export class MappedCode {
string: string; string: string;
map: DecodedSourceMap; map: DecodedSourceMap;
@ -90,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 == '') {
@ -167,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,
@ -214,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);
} }
} }

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

@ -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,162 @@
/* 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;
}
$: $$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,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;
}
};

@ -0,0 +1,18 @@
export default {
html: `
a: moduleA
b: moduleB
moduleA: moduleA
moduleB: moduleB
`,
async test({ assert, target, component }) {
await component.updateModuleA();
assert.htmlEqual(target.innerHTML, `
a: moduleA
b: moduleB
moduleA: moduleA
moduleB: moduleB
`);
}
};

@ -0,0 +1,15 @@
<script context="module">
let moduleA = 'moduleA';
let moduleB = 'moduleB';
</script>
<script>
export function updateModuleA() {
moduleA = 'something else';
}
$: a = moduleA;
$: b = moduleB;
</script>
a: {a}
b: {b}
moduleA: {moduleA}
moduleB: {moduleB}

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

@ -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} />

@ -0,0 +1,6 @@
<script context="module">
let foo;
</script>
<script>
$: bar = foo;
</script>

@ -0,0 +1,17 @@
[
{
"code": "module-script-reactive-declaration",
"message": "\"foo\" is declared in a module script and will not be reactive",
"pos": 65,
"start": {
"character": 65,
"column": 10,
"line": 5
},
"end": {
"character": 68,
"column": 13,
"line": 5
}
}
]
Loading…
Cancel
Save