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/community
/site/src/routes/_contributors.js
/site/src/routes/_components/WhosUsingSvelte.svelte
/site/src/routes/_components/WhosUsingSvelte.*

@ -1,5 +1,28 @@
# 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
* 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
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).
@ -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.
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
npm run build
@ -56,7 +56,7 @@ To watch for changes and continually rebuild the package (this is useful if you'
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

2
package-lock.json generated

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

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

@ -43,12 +43,17 @@ In the terminal, you can instantly create a new project like so:
```bash
npx degit sveltejs/template my-svelte-project
cd my-svelte-project
# to use TypeScript run:
# node scripts/setupTypeScript.js
npm install
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.
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:
```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.
```sv
<!-- App.svelte -->
<Widget></Widget>
<Widget>
<p>this is some child content that will overwrite the default slot content</p>
</Widget>
<!-- Widget.svelte -->
<div>
<slot>
this fallback content will be rendered when no content is provided, like in the first example
</slot>
</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)
@ -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.
```sv
<!-- App.svelte -->
<Widget>
<h1 slot="header">Hello</h1>
<p slot="footer">Copyright (c) 2019 Svelte Industries</p>
</Widget>
<!-- Widget.svelte -->
<div>
<slot name="header">No header was provided</slot>
<p>Some content between header and footer</p>
<slot name="footer"></slot>
</div>
<!-- App.svelte -->
<Widget>
<h1 slot="header">Hello</h1>
<p slot="footer">Copyright (c) 2019 Svelte Industries</p>
</Widget>
```
#### [`$$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.
```sv
<!-- App.svelte -->
<Card>
<h1 slot="title">Blog Post Title</h1>
</Card>
<!-- Card.svelte -->
<div>
<slot name="title"></slot>
{#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>
<slot name="description"></slot>
{/if}
</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)
@ -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}>`.
```sv
<!-- App.svelte -->
<FancyList {items} let:prop={thing}>
<div>{thing.text}</div>
</FancyList>
<!-- FancyList.svelte -->
<ul>
{#each items as item}
@ -1375,6 +1371,11 @@ The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}
</li>
{/each}
</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.
```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 -->
<ul>
{#each items as item}
@ -1398,6 +1393,12 @@ Named slots can also expose values. The `let:` directive goes on the element wit
</ul>
<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
* `accessors={true}` — adds getters and setters for the component's props
* `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
```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.
| `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.
| `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
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.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
<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
<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 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 { print, x, b } from 'code-red';
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 { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types';
@ -1366,7 +1366,8 @@ function process_component_options(component: Component, nodes) {
'accessors' in component.compile_options
? component.compile_options.accessors
: !!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');

@ -6,6 +6,7 @@ import { CompileOptions, Warning } from '../interfaces';
import Component from './Component';
import fuzzymatch from '../utils/fuzzymatch';
import get_name_from_filename from './utils/get_name_from_filename';
import { valid_namespaces } from '../utils/namespaces';
const valid_options = [
'format',
@ -22,6 +23,7 @@ const valid_options = [
'hydratable',
'legacy',
'customElement',
'namespace',
'tag',
'css',
'loopGuardTimeout',
@ -30,7 +32,7 @@ const valid_options = [
];
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 => {
if (!valid_options.includes(key)) {
@ -65,6 +67,15 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
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 = {}) {

@ -2,15 +2,16 @@ import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import { Directive } from '../../interfaces';
export default class Action extends Node {
type: 'Action';
name: string;
expression: Expression;
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);
const object = info.name.split('.')[0];
@ -23,6 +24,8 @@ export default class Action extends Node {
? new Expression(component, this, scope, info.expression)
: null;
this.template_scope = scope;
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);
if (this.name === 'textarea') {
if (info.children.length > 0) {
const value_attribute = info.attributes.find(node => node.name === 'value');
if (value_attribute) {
component.error(value_attribute, {
code: 'textarea-duplicate-value',
message: 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
});
}
if (this.namespace !== namespaces.foreign) {
if (this.name === 'textarea') {
if (info.children.length > 0) {
const value_attribute = info.attributes.find(node => node.name === 'value');
if (value_attribute) {
component.error(value_attribute, {
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>
// children treated the same way as a value attribute
info.attributes.push({
type: 'Attribute',
name: 'value',
value: info.children
});
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
info.attributes.push({
type: 'Attribute',
name: 'value',
value: info.children
});
info.children = [];
info.children = [];
}
}
}
if (this.name === 'option') {
// Special case — treat these the same way:
// <option>{foo}</option>
// <option value={foo}>{foo}</option>
const value_attribute = info.attributes.find(attribute => attribute.name === 'value');
if (this.name === 'option') {
// Special case — treat these the same way:
// <option>{foo}</option>
// <option value={foo}>{foo}</option>
const value_attribute = info.attributes.find(attribute => attribute.name === 'value');
if (!value_attribute) {
info.attributes.push({
type: 'Attribute',
name: 'value',
value: info.children,
synthetic: true
});
if (!value_attribute) {
info.attributes.push({
type: 'Attribute',
name: 'value',
value: info.children,
synthetic: true
});
}
}
}
const has_let = info.attributes.some(node => node.type === 'Let');
if (has_let) {
scope = scope.child();
@ -253,65 +254,83 @@ export default class Element extends Node {
});
}
if (a11y_distracting_elements.has(this.name)) {
// no-distracting-elements
this.component.warn(this, {
code: 'a11y-distracting-elements',
message: `A11y: Avoid <${this.name}> elements`
});
this.validate_attributes();
this.validate_event_handlers();
if (this.namespace === namespaces.foreign) {
this.validate_bindings_foreign();
} 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) {
if ((parent as Element).name === 'figure') {
is_figure_parent = true;
break;
}
if (parent.type === 'Element') {
break;
}
parent = parent.parent;
}
validate_attributes() {
const { component, parent } = this;
if (!is_figure_parent) {
this.component.warn(this, {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be an immediate child of <figure>'
this.attributes.forEach(attribute => {
if (attribute.is_spread) return;
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') {
const children = this.children.filter(node => {
if (node.type === 'Comment') return false;
if (node.type === 'Text') return /\S/.test(node.data);
return true;
});
if (name === 'slot') {
if (!attribute.is_static) {
component.error(attribute, {
code: 'invalid-slot-attribute',
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)) {
this.component.warn(children[index], {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be first or last child of <figure>'
});
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'
});
}
}
}
this.validate_attributes();
this.validate_special_cases();
this.validate_bindings();
this.validate_content();
this.validate_event_handlers();
}
// Warnings
validate_attributes() {
const { component, parent } = this;
if (this.namespace !== namespaces.foreign) {
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 => {
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() {
const { component, attributes, handlers } = this;
const attribute_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() {

@ -14,6 +14,7 @@ import { Node, FunctionExpression, Identifier } from 'estree';
import { INode } from '../interfaces';
import { is_reserved_keyword } from '../../utils/reserved_keywords';
import replace_object from '../../utils/replace_object';
import is_contextual from './is_contextual';
import EachBlock from '../EachBlock';
type Owner = INode;
@ -409,18 +410,3 @@ function get_function_name(_node, parent) {
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 flatten_reference from '../utils/flatten_reference';
import { reserved_keywords } from '../utils/reserved_keywords';
import { renderer_invalidate } from './invalidate';
interface ContextMember {
name: string;
@ -168,57 +169,7 @@ export default class Renderer {
}
invalidate(name: string, value?, main_execution_context: boolean = false) {
const variable = this.component.var_lookup.get(name);
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}`);
return renderer_invalidate(this, name, value, main_execution_context);
}
dirty(names: string[], is_reactive_declaration = false): Expression {

@ -7,7 +7,7 @@ import { extract_names, Scope } from '../utils/scope';
import { invalidate } from './invalidate';
import Block from './Block';
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';
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>\`;`}
@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}
@ -537,7 +537,7 @@ export default function dom(
constructor(options) {
super(${options.dev && 'options'});
${should_add_css && b`if (!@_document.getElementById("${component.stylesheet.id}-style")) ${add_css}();`}
@init(this, options, ${definition}, ${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 });`}
${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] !== '$') {
return node;
}
return renderer.invalidate(variable.name, undefined, main_execution_context);
return renderer_invalidate(renderer, variable.name, undefined, main_execution_context);
}
if (!head) {
@ -79,3 +79,66 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
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 handle_select_value_binding from './handle_select_value_binding';
import { Identifier, Node } from 'estree';
import { namespaces } from '../../../../utils/namespaces';
export class BaseAttributeWrapper {
node: Attribute;
@ -67,15 +68,26 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
}
}
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;
if (this.parent.node.namespace == namespaces.foreign) {
// leave attribute case alone for elements in the "foreign" namespace
this.name = this.node.name;
this.metadata = this.get_metadata();
this.is_indirectly_bound_value = false;
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_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);
}

@ -26,6 +26,7 @@ import Action from '../../../nodes/Action';
import MustacheTagWrapper from '../MustacheTag';
import RawMustacheTagWrapper from '../RawMustacheTag';
import create_slot_block from './create_slot_block';
import is_dynamic from '../shared/is_dynamic';
interface BindingGroup {
events: string[];
@ -321,7 +322,7 @@ export default class ElementWrapper extends Wrapper {
literal.quasis.push(state.quasi);
block.chunks.create.push(
b`${node}.${this.can_use_innerhtml ? 'innerHTML': 'textContent'} = ${literal};`
b`${node}.${this.can_use_innerhtml ? 'innerHTML' : 'textContent'} = ${literal};`
);
}
} else {
@ -898,10 +899,19 @@ export default class ElementWrapper extends Wrapper {
const all_dependencies = this.class_dependencies.concat(...dependencies);
const condition = block.renderer.dirty(all_dependencies);
block.chunks.update.push(b`
if (${condition}) {
${updater}
}`);
// If all of the dependencies are non-dynamic (don't get updated) then there is no reason
// to add an updater for this.
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}.c();
} else {
${name}.p(#ctx, #dirty);
${dynamic && b`${name}.p(#ctx, #dirty);`}
}
${has_transitions && b`@transition_in(${name}, 1);`}
${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) {
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(b`${if_blocks}[${current_block_type_index}].p(#ctx, #dirty);`)}
} else {
@ -484,8 +487,6 @@ export default class IfBlockWrapper extends Wrapper {
`);
} else {
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}) {
${change_block}
}

@ -173,7 +173,7 @@ export default class SlotWrapper extends Wrapper {
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});
}
`: b`
` : b`
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});
}

@ -1,6 +1,7 @@
import { b, x } from 'code-red';
import Block from '../../Block';
import Action from '../../../nodes/Action';
import is_contextual from '../../../nodes/shared/is_contextual';
export default function add_actions(
block: Block,
@ -11,7 +12,7 @@ export default function add_actions(
}
export function add_action(block: Block, target: string, action: Action) {
const { expression } = action;
const { expression, template_scope } = action;
let snippet;
let dependencies;
@ -28,7 +29,9 @@ export function add_action(block: Block, target: string, action: Action) {
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) {
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 Text from '../nodes/Text';
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(
component: Component,
@ -38,24 +41,93 @@ export default function ssr(
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_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 }) => {
const store_name = name.slice(1);
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
? b`@validate_store(${store_name}, '${store_name}'); ${assignment}`
: assignment;
})
.filter(Boolean);
return b`let ${name}, ${unsubscribe} = @noop, ${subscribe} = () => (${unsubscribe}(), ${unsubscribe} = @subscribe(${store_name}, $$value => ${name} = $$value), ${store_name})`;
}
return b`let ${name}, ${`$$unsubscribe_${store_name}`};`;
});
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}`;
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) {
insert = b`@validate_store(${name}, '${name}'); ${insert}`;
}
@ -99,35 +171,28 @@ export default function ssr(
do {
$$settled = true;
${reactive_store_values}
${reactive_declarations}
$$rendered = ${literal};
} while (!$$settled);
${reactive_store_unsubscriptions}
return $$rendered;
`
: b`
${reactive_store_values}
${reactive_declarations}
${reactive_store_unsubscriptions}
return ${literal};`;
const blocks = [
...injected.map(name => b`let ${name};`),
rest,
slots,
...reactive_stores.map(({ name }) => {
const store_name = name.slice(1);
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};`;
}),
...reactive_store_declarations,
...reactive_store_subscriptions,
instance_javascript,
...parent_bindings,
css.code && b`$$result.css.add(#css);`,

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

@ -196,7 +196,7 @@ export default function mustache(parser: Parser) {
if (!parser.eat('}')) {
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.eat('}', true);
}
@ -204,7 +204,7 @@ export default function mustache(parser: Parser) {
const new_block: TemplateNode = {
start,
end: null,
type: is_then ? 'ThenBlock': 'CatchBlock',
type: is_then ? 'ThenBlock' : 'CatchBlock',
children: [],
skip: false
};

@ -376,7 +376,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
if (type === 'Binding' && directive_name !== 'this') {
check_unique(directive_name);
} else if (type !== 'EventHandler') {
} else if (type !== 'EventHandler' && type !== 'Action') {
check_unique(name);
}
@ -387,6 +387,13 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
}, 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 as any[]).length > 1 || value[0].type === 'Text') {
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 { decode as decode_mappings } from 'sourcemap-codec';
import { getLocator } from 'locate-character';
import { StringWithSourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/string_with_sourcemap';
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.
import { MappedCode, SourceLocation, parse_attached_sourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/mapped_code';
import { decode_map } from './decode_sourcemap';
import { replace_in_code, slice_source } from './replace_in_code';
import { MarkupPreprocessor, Source, Preprocessor, PreprocessorGroup, Processed } from './types';
interface SourceUpdate {
string?: string;
map?: DecodedSourceMap;
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) {
return filename.split(/[/\\]/).pop();
}
interface Replacement {
offset: number;
length: number;
replacement: StringWithSourcemap;
}
/**
* Represents intermediate states of the preprocessing.
*/
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(
file_basename: string,
source: string,
get_location: ReturnType<typeof getLocator>,
re: RegExp,
func: (...any) => Promise<StringWithSourcemap>
): Promise<StringWithSourcemap> {
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;
get_location: ReturnType<typeof getLocator>;
constructor(public source: string, public filename: string) {
this.update_source({ string: source });
// preprocess source must be relative to itself or equal null
this.file_basename = filename == null ? null : get_file_basename(filename);
}
// 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);
}
/**
* 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;
}
update_source({ string: source, map, dependencies }: SourceUpdate) {
if (source != null) {
this.source = source;
this.get_location = getLocator(source);
}
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]);
}
if (map) {
this.sourcemap_list.unshift(map);
}
if (dependencies) {
this.dependencies.push(...dependencies);
}
}
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;
to_processed(): Processed {
// Combine all the source maps for each preprocessor function into one
const map: RawSourceMap = combine_sourcemaps(this.file_basename, this.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: 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(
file_basename: string,
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
function processed_content_to_code(processed: Processed, location: SourceLocation, file_basename: string): MappedCode {
// Convert the preprocessed code and its sourcemap to a MappedCode
let decoded_map: DecodedSourceMap;
if (processed.map) {
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);
}
decoded_map = decode_map(processed);
// offset only segments pointing at original component source
const source_index = decoded_map.sources.indexOf(file_basename);
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 prefix_with_map.concat(processed_with_map).concat(suffix_with_map);
return MappedCode.from_processed(processed.code, decoded_map);
}
export default async function preprocess(
source: string,
preprocessor: PreprocessorGroup | PreprocessorGroup[],
options?: { filename?: string }
) {
// @ts-ignore todo: doublecheck
const filename = (options && options.filename) || preprocessor.filename; // legacy
const dependencies = [];
/**
* Given the whole tag including content, return a `MappedCode`
* representing the tag content replaced with `processed`.
*/
function processed_tag_to_code(
processed: Processed,
tag_name: 'style' | 'script',
attributes: string,
source: Source
): MappedCode {
const { file_basename, get_location } = source;
// preprocess source must be relative to itself or equal null
const file_basename = filename == null ? null : get_file_basename(filename);
const build_mapped_code = (code: string, offset: number) =>
MappedCode.from_source(slice_source(code, offset, source));
const preprocessors = preprocessor
? Array.isArray(preprocessor) ? preprocessor : [preprocessor]
: [];
const tag_open = `<${tag_name}${attributes || ''}>`;
const tag_close = `</${tag_name}>`;
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 tag_open_code = build_mapped_code(tag_open, 0);
const tag_close_code = build_mapped_code(tag_close, tag_open.length + source.source.length);
// 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
const sourcemap_list: Array<DecodedSourceMap | RawSourceMap> = [];
parse_attached_sourcemap(processed, tag_name);
// 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 processed = await fn({
content: source,
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
});
if (!processed) continue;
if (!processed) return no_change();
if (processed.dependencies) dependencies.push(...processed.dependencies);
source = processed.code;
if (processed.map) {
sourcemap_list.unshift(
typeof(processed.map) === 'string'
if (!processed.map && processed.code === content) return no_change();
return processed_tag_to_code(processed, tag_name, attributes, slice_source(content, tag_offset, source));
}
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)
: processed.map
);
}
: undefined,
dependencies: processed.dependencies
};
} else {
return {};
}
}
async function preprocess_tag_content(tag_name: 'style' | 'script', preprocessor: Preprocessor) {
const get_location = getLocator(source);
const tag_regex = tag_name === 'style'
? /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi
: /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
export default async function preprocess(
source: string,
preprocessor: PreprocessorGroup | PreprocessorGroup[],
options?: { filename?: string }
): 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(
file_basename,
source,
get_location,
tag_regex,
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);
// TODO keep track: what preprocessor generated what sourcemap?
// to make debugging easier = detect low-resolution sourcemaps in fn combine_mappings
for (const process of markup) {
result.update_source(await process_markup(filename, process, result));
}
for (const fn of script) {
await preprocess_tag_content('script', fn);
for (const process of script) {
result.update_source(await process_tag('script', process, result));
}
for (const fn of style) {
await preprocess_tag_content('style', fn);
for (const preprocess of style) {
result.update_source(await process_tag('style', preprocess, result));
}
// Combine all the source maps for each preprocessor function into one
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;
}
};
return result.to_processed();
}

@ -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 remapping from '@ampproject/remapping';
import { SourceMap } from 'magic-string';
import { Source, Processed } from '../preprocess/types';
type SourceLocation = {
export type SourceLocation = {
line: number;
column: number;
};
@ -67,7 +68,7 @@ function pushArray<T>(_this: T[], other: T[]) {
}
}
export class StringWithSourcemap {
export class MappedCode {
string: string;
map: DecodedSourceMap;
@ -89,7 +90,7 @@ export class StringWithSourcemap {
* concat in-place (mutable), return this (chainable)
* will also mutate the `other` object
*/
concat(other: StringWithSourcemap): StringWithSourcemap {
concat(other: MappedCode): MappedCode {
// noop: if one is empty, return the other
if (other.string == '') return this;
if (this.string == '') {
@ -166,34 +167,34 @@ export class StringWithSourcemap {
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;
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
// (some tools may produce less)
const missing_lines = line_count - map.mappings.length;
for (let i = 0; i < missing_lines; i++) {
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: [] };
// add empty SourceMapSegment[] for every line
for (let i = 0; i < line_count; i++) map.mappings.push([]);
return new StringWithSourcemap(string, map);
return new MappedCode(string, map);
}
static from_source(
source_file: string, source: string, offset?: SourceLocation
): StringWithSourcemap {
static from_source({ source, file_basename, get_location }: Source): MappedCode {
let offset: SourceLocation = get_location(0);
if (!offset) offset = { line: 0, column: 0 };
const map: DecodedSourceMap = { version: 3, names: [], sources: [source_file], mappings: [] };
if (source == '') return new StringWithSourcemap(source, map);
const map: DecodedSourceMap = { version: 3, names: [], sources: [file_basename], mappings: [] };
if (source == '') return new MappedCode(source, map);
// we create a high resolution identity map here,
// 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++) {
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
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 {
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;
}
// 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 mathml = 'http://www.w3.org/1998/Math/MathML';
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 valid_namespaces = [
'foreign',
'html',
'mathml',
'svg',
'xlink',
'xml',
'xmlns',
foreign,
html,
mathml,
svg,
@ -20,4 +25,4 @@ export const valid_namespaces = [
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;
}
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 transform = style.transform === 'none' ? '' : style.transform;
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;
set_current_component(component);
const prop_values = options.props || {};
const $$: T$$ = component.$$ = {
fragment: null,
ctx: null,
@ -128,7 +126,7 @@ export function init(component, options, instance, create_fragment, not_equal, p
let ready = false;
$$.ctx = instance
? instance(component, prop_values, (i, ret, ...rest) => {
? instance(component, options.props || {}, (i, ret, ...rest) => {
const value = rest.length ? rest[0] : ret;
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);

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

@ -115,6 +115,20 @@ export class SvelteComponentDev extends SvelteComponent {
* ### DO NOT USE!
*/
$$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: {
target: Element;

@ -14,7 +14,7 @@ function tick_spring<T>(ctx: TickContext<T>, last_value: T, current_value: T, ta
// @ts-ignore
const delta = target_value - current_value;
// @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 damper = ctx.opts.damping * velocity;
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 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;
const token = current_token = {};

@ -125,10 +125,12 @@ type StoresValues<T> = T extends Readable<infer U> ? U :
*
* @param stores - input stores
* @param fn - function callback that aggregates the values
* @param initial_value - when used asynchronously
*/
export function derived<S extends Stores, T>(
stores: S,
fn: (values: StoresValues<S>) => T
fn: (values: StoresValues<S>, set: (value: T) => void) => Unsubscriber | void,
initial_value?: T
): Readable<T>;
/**
@ -137,12 +139,10 @@ export function derived<S extends Stores, T>(
*
* @param stores - input stores
* @param fn - function callback that aggregates the values
* @param initial_value - when used asynchronously
*/
export function derived<S extends Stores, T>(
stores: S,
fn: (values: StoresValues<S>, set: (value: T) => void) => Unsubscriber | void,
initial_value?: T
fn: (values: StoresValues<S>) => 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,
amount = 5,
opacity = 0
}: BlurParams): TransitionConfig {
}: BlurParams = {}): TransitionConfig {
const style = getComputedStyle(node);
const target_opacity = +style.opacity;
const f = style.filter === 'none' ? '' : style.filter;
@ -50,7 +50,7 @@ export function fade(node: Element, {
delay = 0,
duration = 400,
easing = linear
}: FadeParams): TransitionConfig {
}: FadeParams = {}): TransitionConfig {
const o = +getComputedStyle(node).opacity;
return {
@ -77,7 +77,7 @@ export function fly(node: Element, {
x = 0,
y = 0,
opacity = 0
}: FlyParams): TransitionConfig {
}: FlyParams = {}): TransitionConfig {
const style = getComputedStyle(node);
const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform;
@ -104,7 +104,7 @@ export function slide(node: Element, {
delay = 0,
duration = 400,
easing = cubicOut
}: SlideParams): TransitionConfig {
}: SlideParams = {}): TransitionConfig {
const style = getComputedStyle(node);
const opacity = +style.opacity;
const height = parseFloat(style.height);
@ -146,7 +146,7 @@ export function scale(node: Element, {
easing = cubicOut,
start = 0,
opacity = 0
}: ScaleParams): TransitionConfig {
}: ScaleParams = {}): TransitionConfig {
const style = getComputedStyle(node);
const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform;
@ -177,7 +177,7 @@ export function draw(node: SVGElement & { getTotalLength(): number }, {
speed,
duration,
easing = cubicInOut
}: DrawParams): TransitionConfig {
}: DrawParams = {}): TransitionConfig {
const len = node.getTotalLength();
if (duration === undefined) {
@ -237,7 +237,7 @@ export function crossfade({ fallback, ...defaults }: CrossfadeParams & {
css: (t, u) => `
opacity: ${t * opacity};
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
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>',
async test({ assert, target, component }) {
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
export default {
html: '<h1>2 2 xxx 5 6 9 10 2</h1>',
skip_if_ssr: true,
async test({ assert, target, component }) {
await component.update();
assert.htmlEqual(target.innerHTML, '<h1>11 11 yyy 12 13 14 15 11</h1>');

@ -5,7 +5,6 @@ export default {
<button></button>
<button></button>
`,
skip_if_ssr: true,
async test({ assert, component, target, window }) {
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();
});
let saved_window;
before(() => saved_window = global.window);
after(() => global.window = saved_window);
fs.readdirSync(`${__dirname}/samples`).forEach(dir => {
if (dir[0] === '.') return;
@ -197,6 +201,10 @@ describe('ssr', () => {
assert.htmlEqual(html, config.html);
}
if (config.test_ssr) {
config.test_ssr({ assert });
}
if (config.after_test) config.after_test();
if (config.show) {

@ -31,7 +31,8 @@ describe('sourcemaps', () => {
const inputCode = fs.readFileSync(inputFile, 'utf-8');
const input = {
code: inputCode,
locate: getLocator(inputCode)
locate: getLocator(inputCode),
locate_1: getLocator(inputCode, { offsetLine: 1 })
};
const preprocessed = await svelte.preprocess(
@ -41,7 +42,7 @@ describe('sourcemaps', () => {
filename: 'input.svelte'
}
);
const { js, css } = svelte.compile(
preprocessed.code, {
filename: 'input.svelte',
@ -86,12 +87,14 @@ describe('sourcemaps', () => {
assert.deepEqual(
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) {
assert.deepEqual(
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 }) => {
const src = new MagicString(content);
const idx = content.indexOf('baritone');
src.overwrite(idx, idx+'baritone'.length, 'bar');
src.overwrite(idx, idx + 'baritone'.length, 'bar');
const map = SourceMapGenerator.fromSourceMap(
await new SourceMapConsumer(

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

@ -101,4 +101,35 @@ describe('validate', () => {
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