Merge branch 'master' into pr/jacwright/5963

pull/5963/head
bluwy 5 years ago
commit 6d8515c175

@ -1,5 +1,26 @@
# Svelte changelog
## Unreleased
* Ignore whitespace in `{#each}` blocks when containing elements with `animate:` ([#5477](https://github.com/sveltejs/svelte/pull/5477))
## 3.46.2
* Export `FlipParams` interface from `svelte/animate` ([#7103](https://github.com/sveltejs/svelte/issues/7103))
* Fix `style:` directive reactivity inside `{#each}` block ([#7136](https://github.com/sveltejs/svelte/issues/7136))
## 3.46.1
* Handle `style:kebab-case` directives ([#7122](https://github.com/sveltejs/svelte/issues/7122))
* Improve AST produced for `style:` directives ([#7127](https://github.com/sveltejs/svelte/pull/7127))
## 3.46.0
* Implement `{@const}` tag ([RFC #33](https://github.com/sveltejs/rfcs/pull/33), [#6413](https://github.com/sveltejs/svelte/pull/6413))
* Implement `style:` directive ([RFC #42](https://github.com/sveltejs/rfcs/pull/42), [#5923](https://github.com/sveltejs/svelte/pull/5923))
* Fix style manager conflicts when using multiple Svelte instances ([#7026](https://github.com/sveltejs/svelte/issues/7026))
* Fix hydration when using `{@html}` ([#7115](https://github.com/sveltejs/svelte/issues/7115))
## 3.45.0
* Fix non-boolean attribute rendering in SSR to render truthy values as-is ([#6121](https://github.com/sveltejs/svelte/issues/6121))

4
package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "svelte",
"version": "3.45.0",
"version": "3.46.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "svelte",
"version": "3.45.0",
"version": "3.46.2",
"license": "MIT",
"devDependencies": {
"@ampproject/remapping": "^0.3.0",

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

@ -0,0 +1,67 @@
---
title: "Accelerating Svelte's Development"
description: "Scaling the team, building partnerships, and growing the community"
author: Ben McCann
authorURL: https://www.benmccann.com/
---
[Svelte](/) is a frontend framework for building fast reactive web apps with less code. If youre new here, [check out the tutorial](/tutorial) or [examples](/examples) to get a feel for it.
Svelte was [launched 5 years ago](https://news.ycombinator.com/item?id=13069841) and has [come a long way in the time since](https://www.youtube.com/watch?v=YeY5M29-WcY). In 2021, as usage more than doubled, it was voted the [most loved](https://insights.stackoverflow.com/survey/2021#section-most-loved-dreaded-and-wanted-web-frameworks) framework with the [most satisfied](https://2020.stateofjs.com/en-US/technologies/front-end-frameworks/) developers in a pair of surveys. Alongside high-profile companies like The New York Times, Apple, Spotify, Square, Rakuten, Bloomberg, Reuters, Ikea, Brave, and countless others, Svelte is used to power everything from hobby projects to embedded systems interfaces.
To help developers build fully-featured applications with Svelte without worrying about the hard parts, weve been developing the [SvelteKit](https://kit.svelte.dev/) application framework. Were moving quickly towards a [stable 1.0 release](https://github.com/sveltejs/kit/issues?q=is%3Aopen+is%3Aissue+milestone%3A1.0) with the help of early adopters who have already downloaded SvelteKit almost one million times.
## Scaling the team
Rich Harris, Sveltes creator, has [joined Vercel to work on Svelte full-time](https://vercel.com/blog/vercel-welcomes-rich-harris-creator-of-svelte). Were incredibly excited to have Richs level of involvement in Svelte increase even more and have him steward Svelte into the future.
Svelte has been made possible by the work of a large, dedicated community. Svelte has added numerous core maintainers over the course of the pandemic, including three this past week. In alphabetical order:
- [benmccann](https://github.com/benmccann) - primary maintainer of SvelteKit for much of 2021
- [bluwy](https://github.com/bluwy) - major contributor across SvelteKit, vite-plugin-svelte, and Vite
- [dominikg](https://github.com/dominikg) - creator of vite-plugin-svelte
- [dummdidumm](https://github.com/dummdidumm) - maintainer of language-tools, which includes the VS Code extension and `svelte-check`
- [ehrencrona](https://github.com/ehrencrona) - contributor to SvelteKit and uses Svelte at work
- [geoffrich](https://github.com/geoffrich) - has driven efforts to improve the accessibility of the Svelte site and documentation
- [GrygrFlzr](https://github.com/GrygrFlzr) - holds a unique status as a maintainer of both SvelteKit and Vite
- [Halfnelson](https://github.com/Halfnelson) - creator of svelte-native
- [ignatiusmb](https://github.com/ignatiusmb) - regular SvelteKit contributor especially to TypeScript support
- [jasonlyu123](https://github.com/jasonlyu123) - maintainer of language-tools, which includes the VS Code extension and `svelte-check`
- [kaisermann](https://github.com/kaisermann) - creator of svelte-preprocess
- [RedHatter](https://github.com/RedHatter) - creator of Svelte Devtools
- [rixo](https://github.com/rixo) - creator of svelte-hmr
Svelte began accepting donations via [OpenCollective](https://opencollective.com/svelte) last year and has now had over $60,000 donated to-date with [Cohere](https://cohere.ai/) giving $10,000 just today. We hope that these funds will allow existing maintainers to spend more time on Svelte or that the funds could otherwise support Svelte on a part-time or contract basis, which we will continue to investigate.
## Partnerships
Multiple major cloud vendors are stepping up to make deploying SvelteKit applications anywhere a seamless experience. As a result of Richs new job, SvelteKit will soon run on [Vercel Edge Functions](https://vercel.com/features/edge-functions). Netlify has made [big contributions](https://github.com/sveltejs/kit/pull/2113) to the SvelteKit Netlify adapter and also [updated](https://github.com/dependents/node-precinct/pull/88) their zip-it-and-ship-it tool to better support SvelteKit. The recent [Cloudflare Pages launch](https://blog.cloudflare.com/cloudflare-pages-goes-full-stack/) featured SvelteKit as a day one partner via a [new adapter](https://github.com/sveltejs/kit/tree/master/packages/adapter-cloudflare) written by Svelte maintainers [pngwn](https://twitter.com/evilpingwin) and [lukeed](https://twitter.com/lukeed05), the latter of whom joined Cloudflare in 2021. [Begin](https://begin.com) created a [SvelteKit adapter](https://github.com/architect/sveltekit-adapter) for [Architect](https://arc.codes) apps. And community members have [contributed adapters](https://sveltesociety.dev/components#adapters) for environments such as Firebase and Deno, showcasing SvelteKits ability to run wherever JavaScript does.
Weve also been working closely with the [Vite](https://vitejs.dev) team to iron out SSR issues uncovered by SvelteKit users. Vite is the build tool that makes the SvelteKit developer experience possible, and thanks to hard work from a contributor base that includes representatives of multiple frameworks, recent releases have solved almost all the issues weve been tracking as SvelteKit 1.0 release blockers.
## A growing community
[SvelteSociety](https://sveltesociety.dev/) just hosted the [4th Svelte Summit](https://sveltesummit.com/) — [read a summary here](https://svelte.dev/blog/whats-new-in-svelte-december-2021#what-happened-at-svelte-summit) — and Kevin Åberg Kultalahti is [going full-time to lead SvelteSociety](https://twitter.com/kevmodrome/status/1463151477174714373). In addition to hosting Svelte Summit, Kevin and SvelteSociety host and manage the [Svelte Radio podcast](https://www.svelteradio.com/), the [SvelteSociety YouTube channel](https://www.youtube.com/SvelteSociety), and the [Svelte subreddit](https://www.reddit.com/r/sveltejs). SvelteSociety has become the home of all things related to the Svelte community, with the sveltejs/community and sveltejs/integrations repos being retired in favor of [sveltesociety.dev](https://sveltesociety.dev/), which has been redesigned and rebuilt in SvelteKit. In October [Brittney Postma](https://github.com/brittneypostma), [Willow aka GHOST](https://ghostdev.xyz), [Steph Dietz](https://github.com/StephDietz), and [Gen Ashley](https://twitter.com/coderinheels) founded [Svelte Sirens](https://sveltesirens.dev/), a group for women & non-binary community members and their allies.
Hundreds of developers join the Svelte Discord every week to chat about Svelte. You may have noticed that, as of recently, some members of the server have purple names. These are people with the ambassadors role, which was created to recognise some of the communitys most valued members and help manage the demands of a rapidly growing community. Svelte ambassadors are people who are well known for their helpfulness and contributions and for upholding Sveltes reputation as a friendly, welcoming community, and were deeply grateful for their involvement. The initial ambassadors in alphabetical order are:
- [babichjacob](https://github.com/babichjacob)
- [brady fractal](https://github.com/FractalHQ)
- [brittney postma](https://github.com/brittneypostma)
- [d3sandoval](https://github.com/d3sandoval)
- [geoffrich](https://github.com/geoffrich)
- [kev](https://github.com/kevmodrome)
- [puru](https://github.com/PuruVJ)
- [rainlife](https://github.com/stephane-vanraes)
- [rmunn](https://github.com/rmunn)
- [stolinski](https://github.com/stolinski)
- [swyx](https://github.com/sw-yx)
- [theo](https://github.com/theo-steiner)
Were also testing out [GitHub discussions on SvelteKit](https://github.com/sveltejs/kit/discussions) and may bring this to other repos in the Svelte organization if feedback is positive.
## Things to watch
SvelteKit is continuing to progress towards 1.0 and, in just the past week, we have added major features like [improved client-only rendering](https://github.com/sveltejs/kit/pull/2804), [routing hooks](https://github.com/sveltejs/kit/pull/3293), and [the ability to pass data from child components to layouts](https://github.com/sveltejs/kit/pull/3252) (e.g. to support easier management of `<meta>` tags). We're currently working on a number of other high priority items such as discussing API designs for features like streaming and file uploads and contributing to the upcoming Vite 2.8 release.
While a lot of effort has been going into SvelteKit recently, we continue to progress the entire ecosystem. [Svelte 3.46.0](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md#3460) was one of our biggest releases in awhile with two major new features added: [constants in markup](https://github.com/sveltejs/rfcs/blob/master/text/0007-markup-constants.md) and [style directives](https://github.com/sveltejs/rfcs/blob/master/text/0008-style-directives.md).
Svelte and SvelteKits trajectories have been accelerated by the numerous investments above and there will be many more updates to come — subscribe to the [blog](/blog) via [RSS](/blog/rss.xml) or check monthly to be the first to get them.

@ -453,6 +453,29 @@ The `{@debug ...}` tag offers an alternative to `console.log(...)`. It logs the
The `{@debug}` tag without any arguments will insert a `debugger` statement that gets triggered when *any* state changes, as opposed to the specified variables.
### {@const ...}
```sv
{@const assignment}
```
---
The `{@const ...}` tag defines a local constant.
```sv
<script>
export let boxes;
</script>
{#each boxes as box}
{@const area = box.width * box.height}
{box.width} * {box.height} = {area}
{/each}
```
`{@const}` is only allowed as direct child of `{#each}`, `{:then}`, `{:catch}`, `<Component />` or `<svelte:fragment />`.
### Element directives
@ -1648,6 +1671,7 @@ All except `scrollX` and `scrollY` are readonly.
<svelte:window bind:scrollY={y}/>
```
> Note that the page will not be scrolled to the initial value to avoid accessibility issues. Only subsequent changes to the bound variable of `scrollX` and `scrollY` will cause scrolling. However, if the scrolling behaviour is desired, call `scrollTo()` in `onMount()`.
### `<svelte:body>`

@ -178,7 +178,7 @@ ast: object = svelte.parse(
---
The `parse` function parses a component, returning only its abstract syntax tree. Unlike compiling with the `generate: false` option, this will not perform any validation or other analysis of the component beyond parsing it.
The `parse` function parses a component, returning only its abstract syntax tree. Unlike compiling with the `generate: false` option, this will not perform any validation or other analysis of the component beyond parsing it. Note that the returned AST is not considered public API, so breaking changes could occur at any point in time.
```js

@ -3,6 +3,6 @@ import mapbox from 'mapbox-gl';
// https://docs.mapbox.com/help/glossary/access-token/
mapbox.accessToken = MAPBOX_ACCESS_TOKEN;
const key = {};
const key = Symbol();
export { mapbox, key };

@ -3,6 +3,6 @@ import mapbox from 'mapbox-gl';
// https://docs.mapbox.com/help/glossary/access-token/
mapbox.accessToken = MAPBOX_ACCESS_TOKEN;
const key = {};
const key = Symbol();
export { mapbox, key };

@ -40,10 +40,10 @@ The markers can now add themselves to the map.
In `mapbox.js` you'll see this line:
```js
const key = {};
const key = Symbol();
```
We can use anything as a key — we could do `setContext('mapbox', ...)` for example. The downside of using a string is that different component libraries might accidentally use the same one; using an object literal means the keys are guaranteed not to conflict in any circumstance (since an object only has referential equality to itself, i.e. `{} !== {}` whereas `"x" === "x"`), even when you have multiple different contexts operating across many component layers.
Technically, we can use any value as a key — we could do `setContext('mapbox', ...)` for example. The downside of using a string is that different component libraries might accidentally use the same one; using [symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol), on the other hand, means that the keys are guaranteed not to conflict in any circumstance, even when you have multiple different contexts operating across many component layers, since a symbol is essentially a unique identifier.
## Contexts vs. stores

@ -40,6 +40,10 @@ export default {
code: 'invalid-binding',
message: 'Cannot bind to a variable declared with {#await ... then} or {:catch} blocks'
},
invalid_binding_const: {
code: 'invalid-binding',
message: 'Cannot bind to a variable declared with {@const ...}'
},
invalid_binding_writibale: {
code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable'
@ -208,7 +212,7 @@ export default {
},
invalid_attribute_value: (name: string) => ({
code: `invalid-${name}-value`,
message: `${name} attribute must be true or false`
message: `${name} attribute must be true or false`
}),
invalid_options_attribute_unknown: {
code: 'invalid-options-attribute',
@ -241,5 +245,21 @@ export default {
invalid_directive_value: {
code: 'invalid-directive-value',
message: 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
}
},
invalid_const_placement: {
code: 'invalid-const-placement',
message: '{@const} must be the immediate child of {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>'
},
invalid_const_declaration: (name: string) => ({
code: 'invalid-const-declaration',
message: `'${name}' has already been declared`
}),
invalid_const_update: (name: string) => ({
code: 'invalid-const-update',
message: `'${name}' is declared using {@const ...} and is read-only`
}),
cyclical_const_tags: (cycle: string[]) => ({
code: 'cyclical-const-tags',
message: `Cyclical dependency detected: ${cycle.join(' → ')}`
})
};

@ -58,6 +58,9 @@ export default class Binding extends Node {
component.error(this, compiler_errors.invalid_binding_await);
return;
}
if (scope.is_const(name)) {
component.error(this, compiler_errors.invalid_binding_const);
}
scope.dependencies_for_name.get(name).forEach(name => {
const variable = component.var_lookup.get(name);

@ -1,13 +1,15 @@
import map_children from './shared/map_children';
import TemplateScope from './shared/TemplateScope';
import AbstractBlock from './shared/AbstractBlock';
import AwaitBlock from './AwaitBlock';
import Component from '../Component';
import { TemplateNode } from '../../interfaces';
import get_const_tags from './shared/get_const_tags';
import ConstTag from './ConstTag';
export default class CatchBlock extends AbstractBlock {
type: 'CatchBlock';
scope: TemplateScope;
const_tags: ConstTag[];
constructor(component: Component, parent: AwaitBlock, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
@ -18,7 +20,8 @@ export default class CatchBlock extends AbstractBlock {
this.scope.add(context.key.name, parent.expression.dependencies, this);
});
}
this.children = map_children(component, parent, this.scope, info.children);
([this.const_tags, this.children] = get_const_tags(info.children, component, this, parent));
if (!info.skip) {
this.warn_if_empty_block();

@ -0,0 +1,72 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { Context, unpack_destructuring } from './shared/Context';
import { ConstTag as ConstTagType } from '../../interfaces';
import { INodeAllowConstTag } from './interfaces';
import { walk } from 'estree-walker';
import { extract_identifiers } from 'periscopic';
import is_reference, { NodeWithPropertyDefinition } from 'is-reference';
import get_object from '../utils/get_object';
import compiler_errors from '../compiler_errors';
const allowed_parents = new Set(['EachBlock', 'CatchBlock', 'ThenBlock', 'InlineComponent', 'SlotTemplate']);
export default class ConstTag extends Node {
type: 'ConstTag';
expression: Expression;
contexts: Context[] = [];
node: ConstTagType;
scope: TemplateScope;
assignees: Set<string> = new Set();
dependencies: Set<string> = new Set();
constructor(component: Component, parent: INodeAllowConstTag, scope: TemplateScope, info: ConstTagType) {
super(component, parent, scope, info);
if (!allowed_parents.has(parent.type)) {
component.error(info, compiler_errors.invalid_const_placement);
}
this.node = info;
this.scope = scope;
const { assignees, dependencies } = this;
extract_identifiers(info.expression.left).forEach(({ name }) => {
assignees.add(name);
const owner = this.scope.get_owner(name);
if (owner === parent) {
component.error(info, compiler_errors.invalid_const_declaration(name));
}
});
walk(info.expression.right, {
enter(node, parent) {
if (is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition)) {
const identifier = get_object(node as any);
const { name } = identifier;
dependencies.add(name);
}
}
});
}
parse_expression() {
unpack_destructuring({
contexts: this.contexts,
node: this.node.expression.left,
scope: this.scope,
component: this.component
});
this.expression = new Expression(this.component, this, this.scope, this.node.expression.right);
this.contexts.forEach(context => {
const owner = this.scope.get_owner(context.key.name);
if (owner && owner.type === 'ConstTag' && owner.parent === this.parent) {
this.component.error(this.node, compiler_errors.invalid_const_declaration(context.key.name));
}
this.scope.add(context.key.name, this.expression.dependencies, this);
});
}
}

@ -1,28 +0,0 @@
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import Node from './shared/Node';
import Let from './Let';
import { INode } from './interfaces';
export default class DefaultSlotTemplate extends Node {
type: 'SlotTemplate';
scope: TemplateScope;
children: INode[];
lets: Let[] = [];
slot_template_name = 'default';
constructor(
component: Component,
parent: INode,
scope: TemplateScope,
info: any,
lets: Let[],
children: INode[]
) {
super(component, parent, scope, info);
this.type = 'SlotTemplate';
this.children = children;
this.scope = scope;
this.lets = lets;
}
}

@ -1,14 +1,16 @@
import ElseBlock from './ElseBlock';
import Expression from './shared/Expression';
import map_children from './shared/map_children';
import TemplateScope from './shared/TemplateScope';
import AbstractBlock from './shared/AbstractBlock';
import Element from './Element';
import ConstTag from './ConstTag';
import { Context, unpack_destructuring } from './shared/Context';
import { Node } from 'estree';
import Component from '../Component';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
import { INode } from './interfaces';
import get_const_tags from './shared/get_const_tags';
export default class EachBlock extends AbstractBlock {
type: 'EachBlock';
@ -22,6 +24,7 @@ export default class EachBlock extends AbstractBlock {
key: Expression;
scope: TemplateScope;
contexts: Context[];
const_tags: ConstTag[];
has_animation: boolean;
has_binding = false;
has_index_binding = false;
@ -57,9 +60,11 @@ export default class EachBlock extends AbstractBlock {
this.has_animation = false;
this.children = map_children(component, this, this.scope, info.children);
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));
if (this.has_animation) {
this.children = this.children.filter(child => !isEmptyNode(child));
if (this.children.length !== 1) {
const child = this.children.find(child => !!(child as Element).animation);
component.error((child as Element).animation, compiler_errors.invalid_animation_sole);
@ -74,3 +79,7 @@ export default class EachBlock extends AbstractBlock {
: null;
}
}
function isEmptyNode(node: INode) {
return node.type === 'Text' && node.data.trim() === '';
}

@ -7,7 +7,7 @@ import Transition from './Transition';
import Animation from './Animation';
import Action from './Action';
import Class from './Class';
import Style from './Style';
import StyleDirective from './StyleDirective';
import Text from './Text';
import { namespaces } from '../../utils/namespaces';
import map_children from './shared/map_children';
@ -181,7 +181,7 @@ export default class Element extends Node {
actions: Action[] = [];
bindings: Binding[] = [];
classes: Class[] = [];
styles: Style[] = [];
styles: StyleDirective[] = [];
handlers: EventHandler[] = [];
lets: Let[] = [];
intro?: Transition = null;
@ -265,8 +265,8 @@ export default class Element extends Node {
this.classes.push(new Class(component, this, scope, node));
break;
case 'Style':
this.styles.push(new Style(component, this, scope, node));
case 'StyleDirective':
this.styles.push(new StyleDirective(component, this, scope, node));
break;
case 'EventHandler':

@ -126,7 +126,15 @@ export default class InlineComponent extends Node {
slot_template.attributes.push(attribute);
}
}
// transfer const
for (let i = child.children.length - 1; i >= 0; i--) {
const child_child = child.children[i];
if (child_child.type === 'ConstTag') {
slot_template.children.push(child_child);
child.children.splice(i, 1);
}
}
children.push(slot_template);
info.children.splice(i, 1);
}

@ -1,4 +1,3 @@
import map_children from './shared/map_children';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import Node from './shared/Node';
@ -6,12 +5,15 @@ import Let from './Let';
import Attribute from './Attribute';
import { INode } from './interfaces';
import compiler_errors from '../compiler_errors';
import get_const_tags from './shared/get_const_tags';
import ConstTag from './ConstTag';
export default class SlotTemplate extends Node {
type: 'SlotTemplate';
scope: TemplateScope;
children: INode[];
lets: Let[] = [];
const_tags: ConstTag[];
slot_attribute: Attribute;
slot_template_name: string = 'default';
@ -63,7 +65,7 @@ export default class SlotTemplate extends Node {
});
this.scope = scope;
this.children = map_children(component, this, this.scope, info.children);
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));
}
validate_slot_template_placement() {

@ -1,22 +0,0 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import { TemplateNode } from '../../interfaces';
import TemplateScope from './shared/TemplateScope';
import Component from '../Component';
export default class Style extends Node {
type: 'Style';
name: string;
expression: Expression;
should_cache: boolean;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.name = info.name;
this.expression = new Expression(component, this, scope, info.expression);
this.should_cache = info.expression.type === 'TemplateLiteral' && info.expression.expressions.length > 0;
}
}

@ -0,0 +1,39 @@
import { TemplateNode } from '../../interfaces';
import Component from '../Component';
import { nodes_to_template_literal } from '../utils/nodes_to_template_literal';
import Expression from './shared/Expression';
import Node from './shared/Node';
import TemplateScope from './shared/TemplateScope';
export default class StyleDirective extends Node {
type: 'StyleDirective';
name: string;
expression: Expression;
should_cache: boolean;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.name = info.name;
// Convert the value array to an expression so it's easier to handle
// the StyleDirective going forward.
if (info.value === true || (info.value.length === 1 && info.value[0].type === 'MustacheTag')) {
const identifier = info.value === true
? {
type: 'Identifier',
start: info.end - info.name.length,
end: info.end,
name: info.name
} as any
: info.value[0].expression;
this.expression = new Expression(component, this, scope, identifier);
this.should_cache = false;
} else {
const raw_expression = nodes_to_template_literal(info.value);
this.expression = new Expression(component, this, scope, raw_expression);
this.should_cache = raw_expression.expressions.length > 0;
}
}
}

@ -1,13 +1,15 @@
import map_children from './shared/map_children';
import TemplateScope from './shared/TemplateScope';
import AbstractBlock from './shared/AbstractBlock';
import AwaitBlock from './AwaitBlock';
import Component from '../Component';
import { TemplateNode } from '../../interfaces';
import get_const_tags from './shared/get_const_tags';
import ConstTag from './ConstTag';
export default class ThenBlock extends AbstractBlock {
type: 'ThenBlock';
scope: TemplateScope;
const_tags: ConstTag[];
constructor(component: Component, parent: AwaitBlock, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
@ -18,7 +20,8 @@ export default class ThenBlock extends AbstractBlock {
this.scope.add(context.key.name, parent.expression.dependencies, this);
});
}
this.children = map_children(component, parent, this.scope, info.children);
([this.const_tags, this.children] = get_const_tags(info.children, component, this, parent));
if (!info.skip) {
this.warn_if_empty_block();

@ -8,8 +8,9 @@ import Binding from './Binding';
import Body from './Body';
import CatchBlock from './CatchBlock';
import Class from './Class';
import Style from './Style';
import StyleDirective from './StyleDirective';
import Comment from './Comment';
import ConstTag from './ConstTag';
import DebugTag from './DebugTag';
import EachBlock from './EachBlock';
import Element from './Element';
@ -27,7 +28,6 @@ import PendingBlock from './PendingBlock';
import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot';
import SlotTemplate from './SlotTemplate';
import DefaultSlotTemplate from './DefaultSlotTemplate';
import Text from './Text';
import ThenBlock from './ThenBlock';
import Title from './Title';
@ -45,6 +45,7 @@ export type INode = Action
| CatchBlock
| Class
| Comment
| ConstTag
| DebugTag
| EachBlock
| Element
@ -62,11 +63,17 @@ export type INode = Action
| RawMustacheTag
| Slot
| SlotTemplate
| DefaultSlotTemplate
| Style
| StyleDirective
| Tag
| Text
| ThenBlock
| Title
| Transition
| Window;
export type INodeAllowConstTag =
| EachBlock
| CatchBlock
| ThenBlock
| InlineComponent
| SlotTemplate;

@ -133,6 +133,10 @@ export default class Expression {
if (names) {
names.forEach(name => {
if (template_scope.names.has(name)) {
if (template_scope.is_const(name)) {
component.error(node, compiler_errors.invalid_const_update(name));
}
template_scope.dependencies_for_name.get(name).forEach(name => {
const variable = component.var_lookup.get(name);
if (variable) variable[deep ? 'mutated' : 'reassigned'] = true;
@ -172,7 +176,7 @@ export default class Expression {
}
// TODO move this into a render-dom wrapper?
manipulate(block?: Block) {
manipulate(block?: Block, ctx?: string | void) {
// TODO ideally we wouldn't end up calling this method
// multiple times
if (this.manipulated) return this.manipulated;
@ -219,7 +223,7 @@ export default class Expression {
component.add_reference(name); // TODO is this redundant/misplaced?
}
} else if (is_contextual(component, template_scope, name)) {
const reference = block.renderer.reference(node);
const reference = block.renderer.reference(node, ctx);
this.replace(reference);
}

@ -4,8 +4,9 @@ import CatchBlock from '../CatchBlock';
import InlineComponent from '../InlineComponent';
import Element from '../Element';
import SlotTemplate from '../SlotTemplate';
import ConstTag from '../ConstTag';
type NodeWithScope = EachBlock | ThenBlock | CatchBlock | InlineComponent | Element | SlotTemplate;
type NodeWithScope = EachBlock | ThenBlock | CatchBlock | InlineComponent | Element | SlotTemplate | ConstTag;
export default class TemplateScope {
names: Set<string>;
@ -48,4 +49,9 @@ export default class TemplateScope {
const owner = this.get_owner(name);
return owner && (owner.type === 'ThenBlock' || owner.type === 'CatchBlock');
}
is_const(name: string) {
const owner = this.get_owner(name);
return owner && owner.type === 'ConstTag';
}
}

@ -0,0 +1,91 @@
import { TemplateNode, ConstTag as ConstTagType } from '../../../interfaces';
import Component from '../../Component';
import ConstTag from '../ConstTag';
import map_children from './map_children';
import { INodeAllowConstTag, INode } from '../interfaces';
import check_graph_for_cycles from '../../utils/check_graph_for_cycles';
import compiler_errors from '../../compiler_errors';
export default function get_const_tags(children: TemplateNode[], component: Component, node: INodeAllowConstTag, parent: INode): [ConstTag[], Array<Exclude<INode, ConstTag>>] {
const const_tags: ConstTagType[] = [];
const others: Array<Exclude<TemplateNode, ConstTagType>> = [];
for (const child of children) {
if (child.type === 'ConstTag') {
const_tags.push(child as ConstTagType);
} else {
others.push(child);
}
}
const consts_nodes = const_tags.map(tag => new ConstTag(component, node, node.scope, tag));
const sorted_consts_nodes = sort_consts_nodes(consts_nodes, component);
sorted_consts_nodes.forEach(node => node.parse_expression());
const children_nodes = map_children(component, parent, node.scope, others);
return [sorted_consts_nodes, children_nodes as Array<Exclude<INode, ConstTag>>];
}
function sort_consts_nodes(consts_nodes: ConstTag[], component: Component) {
type ConstNode = {
assignees: Set<string>;
dependencies: Set<string>;
node: ConstTag;
};
const sorted_consts_nodes: ConstNode[] = [];
const unsorted_consts_nodes: ConstNode[] = consts_nodes.map(node => {
return {
assignees: node.assignees,
dependencies: node.dependencies,
node
};
});
const lookup = new Map();
unsorted_consts_nodes.forEach(node => {
node.assignees.forEach(name => {
if (!lookup.has(name)) {
lookup.set(name, []);
}
lookup.get(name).push(node);
});
});
const cycle = check_graph_for_cycles(unsorted_consts_nodes.reduce((acc, node) => {
node.assignees.forEach(v => {
node.dependencies.forEach(w => {
if (!node.assignees.has(w)) {
acc.push([v, w]);
}
});
});
return acc;
}, []));
if (cycle && cycle.length) {
const nodeList = lookup.get(cycle[0]);
const node = nodeList[0];
component.error(node.node, compiler_errors.cyclical_const_tags(cycle));
}
const add_node = (node: ConstNode) => {
if (sorted_consts_nodes.includes(node)) return;
node.dependencies.forEach(name => {
if (node.assignees.has(name)) return;
const earlier_nodes = lookup.get(name);
if (earlier_nodes) {
earlier_nodes.forEach(add_node);
}
});
sorted_consts_nodes.push(node);
};
unsorted_consts_nodes.forEach(add_node);
return sorted_consts_nodes.map(node => node.node);
}

@ -1,5 +1,6 @@
import AwaitBlock from '../AwaitBlock';
import Body from '../Body';
import ConstTag from '../ConstTag';
import Comment from '../Comment';
import EachBlock from '../EachBlock';
import Element from '../Element';
@ -25,6 +26,7 @@ function get_constructor(type) {
case 'AwaitBlock': return AwaitBlock;
case 'Body': return Body;
case 'Comment': return Comment;
case 'ConstTag': return ConstTag;
case 'EachBlock': return EachBlock;
case 'Element': return Element;
case 'Head': return Head;

@ -254,7 +254,7 @@ export default class Renderer {
};
}
reference(node: string | Identifier | MemberExpression) {
reference(node: string | Identifier | MemberExpression, ctx: string | void = '#ctx') {
if (typeof node === 'string') {
node = { type: 'Identifier', name: node };
}
@ -268,7 +268,7 @@ export default class Renderer {
}
if (member !== undefined) {
const replacement = x`/*${member.name}*/ #ctx[${member.index}]` as MemberExpression;
const replacement = x`/*${member.name}*/ ${ctx}[${member.index}]` as MemberExpression;
if (nodes[0].loc) replacement.object.loc = nodes[0].loc;
nodes[0] = replacement;

@ -10,6 +10,7 @@ import ThenBlock from '../../nodes/ThenBlock';
import CatchBlock from '../../nodes/CatchBlock';
import { Context } from '../../nodes/shared/Context';
import { Identifier, Literal, Node } from 'estree';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
type Status = 'pending' | 'then' | 'catch';
@ -76,22 +77,34 @@ class AwaitBlockBranch extends Wrapper {
this.is_destructured = true;
}
this.value_index = this.renderer.context_lookup.get(this.value).index;
if (this.has_consts(this.node)) {
add_const_tags_context(this.renderer, this.node.const_tags);
}
}
has_consts(node: PendingBlock | ThenBlock | CatchBlock): node is ThenBlock | CatchBlock {
return node instanceof ThenBlock || node instanceof CatchBlock;
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
this.fragment.render(block, parent_node, parent_nodes);
if (this.is_destructured) {
this.render_destructure();
if (this.is_destructured || (this.has_consts(this.node) && this.node.const_tags.length > 0)) {
this.render_get_context();
}
}
render_destructure() {
const props = this.value_contexts.map(prop => b`#ctx[${this.block.renderer.context_lookup.get(prop.key.name).index}] = ${prop.default_modifier(prop.modifier(x`#ctx[${this.value_index}]`), name => this.renderer.reference(name))};`);
render_get_context() {
const props = this.is_destructured ? this.value_contexts.map(prop => b`#ctx[${this.block.renderer.context_lookup.get(prop.key.name).index}] = ${prop.default_modifier(prop.modifier(x`#ctx[${this.value_index}]`), name => this.renderer.reference(name))};`) : null;
const const_tags_props = this.has_consts(this.node) ? add_const_tags(this.block, this.node.const_tags, '#ctx') : null;
const get_context = this.block.renderer.component.get_unique_name(`get_${this.status}_context`);
this.block.renderer.blocks.push(b`
function ${get_context}(#ctx) {
${props}
${const_tags_props}
}
`);
this.block.chunks.declarations.push(b`${get_context}(#ctx)`);

@ -8,6 +8,7 @@ import { b, x } from 'code-red';
import ElseBlock from '../../nodes/ElseBlock';
import { Identifier, Node } from 'estree';
import get_object from '../../utils/get_object';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
export class ElseBlockWrapper extends Wrapper {
node: ElseBlock;
@ -86,6 +87,7 @@ export default class EachBlockWrapper extends Wrapper {
this.node.contexts.forEach(context => {
renderer.add_to_context(context.key.name, true);
});
add_const_tags_context(renderer, this.node.const_tags);
this.block = block.child({
comment: create_debugging_comment(this.node, this.renderer.component),
@ -350,11 +352,13 @@ export default class EachBlockWrapper extends Wrapper {
if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`);
if (this.node.has_binding || this.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`);
// TODO which is better — Object.create(array) or array.slice()?
renderer.blocks.push(b`
function ${this.vars.get_each_context}(#ctx, list, i) {
const child_ctx = #ctx.slice();
${this.context_props}
${add_const_tags(this.block, this.node.const_tags, 'child_ctx')}
return child_ctx;
}
`);

@ -204,7 +204,7 @@ export default class ElementWrapper extends Wrapper {
}
// add directive and handler dependencies
[node.animation, node.outro, ...node.actions, ...node.classes].forEach(directive => {
[node.animation, node.outro, ...node.actions, ...node.classes, ...node.styles].forEach(directive => {
if (directive && directive.expression) {
block.add_dependencies(directive.expression.dependencies);
}
@ -952,7 +952,7 @@ export default class ElementWrapper extends Wrapper {
const snippet = expression.manipulate(block);
let cached_snippet;
if (should_cache) {
cached_snippet = block.get_unique_name(`style_${name}`);
cached_snippet = block.get_unique_name(`style_${name.replace(/-/g, '_')}`);
block.add_variable(cached_snippet, snippet);
}

@ -4,23 +4,16 @@ import Block from '../Block';
import FragmentWrapper from './Fragment';
import create_debugging_comment from './shared/create_debugging_comment';
import { get_slot_definition } from './shared/get_slot_definition';
import { x } from 'code-red';
import { b, x } from 'code-red';
import { sanitize } from '../../../utils/names';
import { Identifier } from 'estree';
import InlineComponentWrapper from './InlineComponent';
import { extract_names } from 'periscopic';
import { INode } from '../../nodes/interfaces';
import Let from '../../nodes/Let';
import TemplateScope from '../../nodes/shared/TemplateScope';
type NodeWithLets = INode & {
scope: TemplateScope;
lets: Let[];
slot_template_name: string;
};
import SlotTemplate from '../../nodes/SlotTemplate';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
export default class SlotTemplateWrapper extends Wrapper {
node: NodeWithLets;
node: SlotTemplate;
fragment: FragmentWrapper;
block: Block;
parent: InlineComponentWrapper;
@ -29,13 +22,13 @@ export default class SlotTemplateWrapper extends Wrapper {
renderer: Renderer,
block: Block,
parent: Wrapper,
node: NodeWithLets,
node: SlotTemplate,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
const { scope, lets, slot_template_name } = this.node;
const { scope, lets, const_tags, slot_template_name } = this.node;
lets.forEach(l => {
extract_names(l.value || l.name).forEach(name => {
@ -43,6 +36,8 @@ export default class SlotTemplateWrapper extends Wrapper {
});
});
add_const_tags_context(renderer, const_tags);
this.block = block.child({
comment: create_debugging_comment(this.node, this.renderer.component),
name: this.renderer.component.get_unique_name(
@ -76,5 +71,21 @@ export default class SlotTemplateWrapper extends Wrapper {
render() {
this.fragment.render(this.block, null, x`#nodes` as Identifier);
if (this.node.const_tags.length > 0) {
this.render_get_context();
}
}
render_get_context() {
const get_context = this.block.renderer.component.get_unique_name('get_context');
this.block.renderer.blocks.push(b`
function ${get_context}(#ctx) {
${add_const_tags(this.block, this.node.const_tags, '#ctx')}
}
`);
this.block.chunks.declarations.push(b`${get_context}(#ctx)`);
if (this.block.has_update_method) {
this.block.chunks.update.unshift(b`${get_context}(#ctx)`);
}
}
}

@ -0,0 +1,24 @@
import ConstTag from '../../../nodes/ConstTag';
import Block from '../../Block';
import { b, x } from 'code-red';
import Renderer from '../../Renderer';
export function add_const_tags(block: Block, const_tags: ConstTag[], ctx: string) {
const const_tags_props = [];
const_tags.forEach((const_tag, i) => {
const name = `#constants_${i}`;
const_tags_props.push(b`const ${name} = ${const_tag.expression.manipulate(block, ctx)}`);
const_tag.contexts.forEach(context => {
const_tags_props.push(b`${ctx}[${block.renderer.context_lookup.get(context.key.name).index}] = ${context.default_modifier(context.modifier({ type: 'Identifier', name }), name => block.renderer.context_lookup.has(name) ? x`${ctx}[${block.renderer.context_lookup.get(name).index}]` : { type: 'Identifier', name })};`);
});
});
return const_tags_props;
}
export function add_const_tags_context(renderer: Renderer, const_tags: ConstTag[]) {
const_tags.forEach(const_tag => {
const_tag.contexts.forEach(context => {
renderer.add_to_context(context.key.name, true);
});
});
}

@ -1,6 +1,7 @@
import Renderer, { RenderOptions } from '../Renderer';
import AwaitBlock from '../../nodes/AwaitBlock';
import { x } from 'code-red';
import { get_const_tags } from './shared/get_const_tags';
export default function(node: AwaitBlock, renderer: Renderer, options: RenderOptions) {
renderer.push();
@ -17,7 +18,7 @@ export default function(node: AwaitBlock, renderer: Renderer, options: RenderOpt
__value.then(null, @noop);
return ${pending};
}
return (function(${node.then_node ? node.then_node : ''}) { return ${then}; }(__value));
return (function(${node.then_node ? node.then_node : ''}) { ${get_const_tags(node.then.const_tags)}; return ${then}; }(__value));
}(${node.expression.node})
`);
}

@ -1,6 +1,7 @@
import Renderer, { RenderOptions } from '../Renderer';
import EachBlock from '../../nodes/EachBlock';
import { x } from 'code-red';
import { get_const_tags } from './shared/get_const_tags';
export default function(node: EachBlock, renderer: Renderer, options: RenderOptions) {
const args = [node.context_node];
@ -10,7 +11,7 @@ export default function(node: EachBlock, renderer: Renderer, options: RenderOpti
renderer.render(node.children, options);
const result = renderer.pop();
const consequent = x`@each(${node.expression.node}, (${args}) => ${result})`;
const consequent = x`@each(${node.expression.node}, (${args}) => { ${get_const_tags(node.const_tags)}; return ${result} })`;
if (node.else) {
renderer.push();

@ -76,9 +76,9 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend
slot_scopes
}));
slot_scopes.forEach(({ input, output }, name) => {
slot_scopes.forEach(({ input, output, statements }, name) => {
slot_fns.push(
p`${name}: (${input}) => ${output}`
p`${name}: (${input}) => { ${statements}; return ${output}; }`
);
});
}

@ -3,9 +3,9 @@ import SlotTemplate from '../../nodes/SlotTemplate';
import remove_whitespace_children from './utils/remove_whitespace_children';
import { get_slot_scope } from './shared/get_slot_scope';
import InlineComponent from '../../nodes/InlineComponent';
import Element from '../../nodes/Element';
import { get_const_tags } from './shared/get_const_tags';
export default function(node: SlotTemplate | Element | InlineComponent, renderer: Renderer, options: RenderOptions & {
export default function(node: SlotTemplate, renderer: Renderer, options: RenderOptions & {
slot_scopes: Map<any, any>;
}) {
const parent_inline_component = node.parent as InlineComponent;
@ -31,7 +31,8 @@ export default function(node: SlotTemplate | Element | InlineComponent, renderer
options.slot_scopes.set(node.slot_template_name, {
input: get_slot_scope(node.lets),
output: slot_fragment_content
output: slot_fragment_content,
statements: get_const_tags(node.const_tags)
});
}
}

@ -0,0 +1,17 @@
import ConstTag from '../../../nodes/ConstTag';
export function get_const_tags(const_tags: ConstTag[]) {
if (const_tags.length === 0) return null;
return {
type: 'VariableDeclaration',
kind: 'let',
declarations: const_tags.map(const_tag => {
const assignment = const_tag.node.expression;
return {
type: 'VariableDeclarator',
id: assignment.left,
init: assignment.right
};
})
};
}

@ -0,0 +1,37 @@
import { TemplateElement, TemplateLiteral } from 'estree';
import { MustacheTag, Text } from '../../interfaces';
/**
* Transforms a list of Text and MustacheTags into a TemplateLiteral expression.
* Start/End positions on the elements of the expression are not set.
*/
export function nodes_to_template_literal(value: Array<Text | MustacheTag>): TemplateLiteral {
const literal: TemplateLiteral = {
type: 'TemplateLiteral',
expressions: [],
quasis: []
};
let quasi: TemplateElement = {
type: 'TemplateElement',
value: { raw: '', cooked: null },
tail: false
};
value.forEach((node) => {
if (node.type === 'Text') {
quasi.value.raw += node.raw;
} else if (node.type === 'MustacheTag') {
literal.quasis.push(quasi);
literal.expressions.push(node.expression as any);
quasi = {
type: 'TemplateElement',
value: { raw: '', cooked: null },
tail: false
};
}
});
quasi.tail = true;
literal.quasis.push(quasi);
return literal;
}

@ -1,4 +1,4 @@
import { Node, Program } from 'estree';
import { AssignmentExpression, Node, Program } from 'estree';
import { SourceMap } from 'magic-string';
interface BaseNode {
@ -20,7 +20,7 @@ export interface Text extends BaseNode {
}
export interface MustacheTag extends BaseNode {
type: 'MustacheTag';
type: 'MustacheTag' | 'RawMustacheTag';
expression: Node;
}
@ -30,17 +30,32 @@ export interface Comment extends BaseNode {
ignores: string[];
}
export interface ConstTag extends BaseNode {
type: 'ConstTag';
expression: AssignmentExpression;
}
interface DebugTag extends BaseNode {
type: 'DebugTag';
identifiers: Node[]
}
export type DirectiveType = 'Action'
| 'Animation'
| 'Binding'
| 'Class'
| 'Style'
| 'StyleDirective'
| 'EventHandler'
| 'Let'
| 'Ref'
| 'Transition';
interface BaseDirective extends BaseNode {
type: DirectiveType;
name: string;
}
interface BaseExpressionDirective extends BaseDirective {
type: DirectiveType;
expression: null | Node;
name: string;
@ -64,15 +79,17 @@ export interface SpreadAttribute extends BaseNode {
expression: Node;
}
export interface Transition extends BaseDirective {
export interface Transition extends BaseExpressionDirective {
type: 'Transition';
intro: boolean;
outro: boolean;
}
export type Directive = BaseDirective | Transition;
export type Directive = BaseDirective | BaseExpressionDirective | Transition;
export type TemplateNode = Text
| ConstTag
| DebugTag
| MustacheTag
| BaseNode
| Element

@ -375,6 +375,28 @@ export default function mustache(parser: Parser) {
type: 'DebugTag',
identifiers
});
} else if (parser.eat('@const')) {
// {@const a = b}
parser.require_whitespace();
const expression = read_expression(parser);
if (!(expression.type === 'AssignmentExpression' && expression.operator === '=')) {
parser.error({
code: 'invalid-const-args',
message: '{@const ...} must be an assignment.'
}, start);
}
parser.allow_whitespace();
parser.eat('}', true);
parser.current().children.push({
start,
end: parser.index,
type: 'ConstTag',
expression
});
} else {
const expression = read_expression(parser);

@ -1,14 +1,13 @@
import { TemplateLiteral, TemplateElement, Expression } from 'estree';
import { Directive, DirectiveType, TemplateNode, Text } from '../../interfaces';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore';
import fuzzymatch from '../../utils/fuzzymatch';
import { is_void } from '../../utils/names';
import parser_errors from '../errors';
import { Parser } from '../index';
import read_expression from '../read/expression';
import read_script from '../read/script';
import read_style from '../read/style';
import { decode_character_references, closing_tag_omitted } from '../utils/html';
import { is_void } from '../../utils/names';
import { Parser } from '../index';
import { Directive, DirectiveType, TemplateNode, Text, MustacheTag } from '../../interfaces';
import fuzzymatch from '../../utils/fuzzymatch';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore';
import parser_errors from '../errors';
import { closing_tag_omitted, decode_character_references } from '../utils/html';
// eslint-disable-next-line no-useless-escape
const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
@ -271,36 +270,6 @@ function read_tag_name(parser: Parser) {
return name;
}
function node_to_template_literal(value: Array<Text | MustacheTag>): TemplateLiteral {
let quasi: TemplateElement = {
type: 'TemplateElement',
value: { raw: '', cooked: null },
tail: false
};
const literal: TemplateLiteral = {
type: 'TemplateLiteral',
expressions: [],
quasis: []
};
value.forEach((node) => {
if (node.type === 'Text') {
quasi.value.raw += node.raw;
} else if (node.type === 'MustacheTag') {
literal.quasis.push(quasi);
literal.expressions.push(node.expression as Expression);
quasi = {
type: 'TemplateElement',
value: { raw: '', cooked: null },
tail: false
};
}
});
quasi.tail = true;
literal.quasis.push(quasi);
return literal;
}
function read_attribute(parser: Parser, unique_names: Set<string>) {
const start = parser.index;
@ -396,14 +365,22 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
parser.error(parser_errors.invalid_ref_directive(directive_name), start);
}
if (type === 'StyleDirective') {
return {
start,
end,
type,
name: directive_name,
value
};
}
const first_value = value[0];
let expression = null;
if (first_value) {
const attribute_contains_text = (value as any[]).length > 1 || first_value.type === 'Text';
if (type === 'Style') {
expression = attribute_contains_text ? node_to_template_literal(value as Array<Text | MustacheTag>) : first_value.expression;
} else if (attribute_contains_text) {
if (attribute_contains_text) {
parser.error(parser_errors.invalid_directive_value, first_value.start);
} else {
expression = first_value.expression;
@ -426,7 +403,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
}
// Directive name is expression, e.g. <p class:isRed />
if (!directive.expression && (type === 'Binding' || type === 'Class' || type === 'Style')) {
if (!directive.expression && (type === 'Binding' || type === 'Class')) {
directive.expression = {
start: directive.start + colon_index + 1,
end: directive.end,
@ -454,7 +431,7 @@ function get_directive_type(name: string): DirectiveType {
if (name === 'animate') return 'Animation';
if (name === 'bind') return 'Binding';
if (name === 'class') return 'Class';
if (name === 'style') return 'Style';
if (name === 'style') return 'StyleDirective';
if (name === 'on') return 'EventHandler';
if (name === 'let') return 'Let';
if (name === 'ref') return 'Ref';

@ -19,6 +19,8 @@ export function to_string(node: TemplateNode) {
return '{@html} block';
case 'DebugTag':
return '{@debug} block';
case 'ConstTag':
return '{@const} tag';
case 'Element':
case 'InlineComponent':
case 'Slot':

@ -10,7 +10,7 @@ export interface AnimationConfig {
tick?: (t: number, u: number) => void;
}
interface FlipParams {
export interface FlipParams {
delay?: number;
duration?: number | ((len: number) => number);
easing?: (t: number) => number;

@ -157,7 +157,7 @@ export function get_root_for_style(node: Node): ShadowRoot | Document {
export function append_empty_stylesheet(node: Node) {
const style_element = element('style') as HTMLStyleElement;
append_stylesheet(get_root_for_style(node), style_element);
return style_element;
return style_element.sheet as CSSStyleSheet;
}
function append_stylesheet(node: ShadowRoot | Document, style: HTMLStyleElement) {
@ -501,7 +501,7 @@ export function claim_html_tag(nodes) {
}
init_claim_info(nodes);
const html_tag_nodes = nodes.splice(start_index, end_index + 1);
const html_tag_nodes = nodes.splice(start_index, end_index - start_index + 1);
detach(html_tag_nodes[0]);
detach(html_tag_nodes[html_tag_nodes.length - 1]);
const claimed_nodes = html_tag_nodes.slice(1, html_tag_nodes.length - 1);

@ -1,12 +1,14 @@
import { append_empty_stylesheet, get_root_for_style } from './dom';
import { raf } from './environment';
interface ExtendedDoc extends Document {
__svelte_stylesheet: CSSStyleSheet;
__svelte_rules: Record<string, true>;
interface StyleInformation {
stylesheet: CSSStyleSheet;
rules: Record<string, true>;
}
const active_docs = new Set<ExtendedDoc>();
// we need to store the information for multiple documents because a Svelte application could also contain iframes
// https://github.com/sveltejs/svelte/issues/3624
const managed_styles = new Map<Document | ShadowRoot, StyleInformation>();
let active = 0;
// https://github.com/darkskyapp/string-hash/blob/master/index.js
@ -18,6 +20,12 @@ function hash(str: string) {
return hash >>> 0;
}
function create_style_information(doc: Document | ShadowRoot, node: Element & ElementCSSInlineStyle) {
const info = { stylesheet: append_empty_stylesheet(node), rules: {} };
managed_styles.set(doc, info);
return info;
}
export function create_rule(node: Element & ElementCSSInlineStyle, a: number, b: number, duration: number, delay: number, ease: (t: number) => number, fn: (t: number, u: number) => string, uid: number = 0) {
const step = 16.666 / duration;
let keyframes = '{\n';
@ -29,13 +37,12 @@ export function create_rule(node: Element & ElementCSSInlineStyle, a: number, b:
const rule = keyframes + `100% {${fn(b, 1 - b)}}\n}`;
const name = `__svelte_${hash(rule)}_${uid}`;
const doc = get_root_for_style(node) as ExtendedDoc;
active_docs.add(doc);
const stylesheet = doc.__svelte_stylesheet || (doc.__svelte_stylesheet = append_empty_stylesheet(node).sheet as CSSStyleSheet);
const current_rules = doc.__svelte_rules || (doc.__svelte_rules = {});
const doc = get_root_for_style(node);
const { stylesheet, rules } = managed_styles.get(doc) || create_style_information(doc, node);
if (!current_rules[name]) {
current_rules[name] = true;
if (!rules[name]) {
rules[name] = true;
stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length);
}
@ -63,12 +70,12 @@ export function delete_rule(node: Element & ElementCSSInlineStyle, name?: string
export function clear_rules() {
raf(() => {
if (active) return;
active_docs.forEach(doc => {
const stylesheet = doc.__svelte_stylesheet;
managed_styles.forEach(info => {
const { stylesheet } = info;
let i = stylesheet.cssRules.length;
while (i--) stylesheet.deleteRule(i);
doc.__svelte_rules = {};
info.rules = {};
});
active_docs.clear();
managed_styles.clear();
});
}

@ -0,0 +1,6 @@
<div>before</div>
<br>
<!-- HTML_TAG_START -->a <!-- HTML_TAG_END --><!-- HTML_TAG_START -->b <!-- HTML_TAG_END --><!-- HTML_TAG_START -->c <!-- HTML_TAG_END -->
<div>after</div>

@ -0,0 +1,6 @@
<div>before</div>
<br>
<!-- HTML_TAG_START -->a <!-- HTML_TAG_END --><!-- HTML_TAG_START -->b <!-- HTML_TAG_END --><!-- HTML_TAG_START -->c <!-- HTML_TAG_END -->
<div>after</div>

@ -0,0 +1,11 @@
<script>
let content = ["a ", "b ", "c "];
</script>
<div>before</div>
<br>
{#each content as c}
{@html c}
{/each}
<div>after</div>

@ -7,8 +7,10 @@ const Component = create_ssr_component(($$result, $$props, $$bindings, slots) =>
if ($$props.things === void 0 && $$bindings.things && things !== void 0) $$bindings.things(things);
if ($$props.foo === void 0 && $$bindings.foo && foo !== void 0) $$bindings.foo(foo);
return `${each(things, thing => `<span>${escape(thing.name)}</span>
${debug(null, 7, 2, { foo })}`)}
return `${each(things, thing => {
return `<span>${escape(thing.name)}</span>
${debug(null, 7, 2, { foo })}`;
})}
<p>foo: ${escape(foo)}</p>`;
});

@ -13,19 +13,13 @@
{
"start": 5,
"end": 16,
"type": "Style",
"type": "StyleDirective",
"name": "color",
"modifiers": [],
"expression": {
"start": 11,
"end": 16,
"name": "color",
"type": "Identifier"
}
"value": true
}
],
"children": []
}
]
}
}
}

@ -1 +1,7 @@
<div style:color="red"></div>
<div style:color="red"></div>
<div style:color='red'></div>
<div style:color=red></div>
<div style:color="red{variable}"></div>
<div style:color='red{variable}'></div>
<div style:color=red{variable}></div>
<div style:color={`template${literal}`}></div>

@ -1,7 +1,7 @@
{
"html": {
"start": 0,
"end": 29,
"end": 252,
"type": "Fragment",
"children": [
{
@ -13,23 +13,339 @@
{
"start": 5,
"end": 22,
"type": "Style",
"type": "StyleDirective",
"name": "color",
"modifiers": [],
"expression": {
"type": "TemplateLiteral",
"expressions": [],
"quasis": [
{
"type": "TemplateElement",
"value": {
"raw": "red",
"cooked": null
"value": [
{
"start": 18,
"end": 21,
"type": "Text",
"raw": "red",
"data": "red"
}
]
}
],
"children": []
},
{
"start": 29,
"end": 30,
"type": "Text",
"raw": "\n",
"data": "\n"
},
{
"start": 30,
"end": 59,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 35,
"end": 52,
"type": "StyleDirective",
"name": "color",
"value": [
{
"start": 48,
"end": 51,
"type": "Text",
"raw": "red",
"data": "red"
}
]
}
],
"children": []
},
{
"start": 59,
"end": 60,
"type": "Text",
"raw": "\n",
"data": "\n"
},
{
"start": 60,
"end": 87,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 65,
"end": 80,
"type": "StyleDirective",
"name": "color",
"value": [
{
"start": 77,
"end": 80,
"type": "Text",
"raw": "red",
"data": "red"
}
]
}
],
"children": []
},
{
"start": 87,
"end": 88,
"type": "Text",
"raw": "\n",
"data": "\n"
},
{
"start": 88,
"end": 127,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 93,
"end": 120,
"type": "StyleDirective",
"name": "color",
"value": [
{
"start": 106,
"end": 109,
"type": "Text",
"raw": "red",
"data": "red"
},
{
"start": 109,
"end": 119,
"type": "MustacheTag",
"expression": {
"type": "Identifier",
"start": 110,
"end": 118,
"loc": {
"start": {
"line": 4,
"column": 22
},
"end": {
"line": 4,
"column": 30
}
},
"name": "variable"
}
}
]
}
],
"children": []
},
{
"start": 127,
"end": 128,
"type": "Text",
"raw": "\n",
"data": "\n"
},
{
"start": 128,
"end": 167,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 133,
"end": 160,
"type": "StyleDirective",
"name": "color",
"value": [
{
"start": 146,
"end": 149,
"type": "Text",
"raw": "red",
"data": "red"
},
{
"start": 149,
"end": 159,
"type": "MustacheTag",
"expression": {
"type": "Identifier",
"start": 150,
"end": 158,
"loc": {
"start": {
"line": 5,
"column": 22
},
"end": {
"line": 5,
"column": 30
}
},
"name": "variable"
}
}
]
}
],
"children": []
},
{
"start": 167,
"end": 168,
"type": "Text",
"raw": "\n",
"data": "\n"
},
{
"start": 168,
"end": 205,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 173,
"end": 198,
"type": "StyleDirective",
"name": "color",
"value": [
{
"start": 185,
"end": 188,
"type": "Text",
"raw": "red",
"data": "red"
},
{
"start": 188,
"end": 198,
"type": "MustacheTag",
"expression": {
"type": "Identifier",
"start": 189,
"end": 197,
"loc": {
"start": {
"line": 6,
"column": 21
},
"end": {
"line": 6,
"column": 29
}
},
"name": "variable"
}
}
]
}
],
"children": []
},
{
"start": 205,
"end": 206,
"type": "Text",
"raw": "\n",
"data": "\n"
},
{
"start": 206,
"end": 252,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 211,
"end": 245,
"type": "StyleDirective",
"name": "color",
"value": [
{
"start": 223,
"end": 245,
"type": "MustacheTag",
"expression": {
"type": "TemplateLiteral",
"start": 224,
"end": 244,
"loc": {
"start": {
"line": 7,
"column": 18
},
"end": {
"line": 7,
"column": 38
}
},
"tail": true
"expressions": [
{
"type": "Identifier",
"start": 235,
"end": 242,
"loc": {
"start": {
"line": 7,
"column": 29
},
"end": {
"line": 7,
"column": 36
}
},
"name": "literal"
}
],
"quasis": [
{
"type": "TemplateElement",
"start": 225,
"end": 233,
"loc": {
"start": {
"line": 7,
"column": 19
},
"end": {
"line": 7,
"column": 27
}
},
"value": {
"raw": "template",
"cooked": "template"
},
"tail": false
},
{
"type": "TemplateElement",
"start": 243,
"end": 243,
"loc": {
"start": {
"line": 7,
"column": 37
},
"end": {
"line": 7,
"column": 37
}
},
"value": {
"raw": "",
"cooked": ""
},
"tail": true
}
]
}
]
}
}
]
}
],
"children": []

@ -13,25 +13,31 @@
{
"start": 5,
"end": 26,
"type": "Style",
"type": "StyleDirective",
"name": "color",
"modifiers": [],
"expression": {
"type": "Identifier",
"start": 18,
"end": 25,
"loc": {
"start": {
"line": 1,
"column": 18
},
"end": {
"line": 1,
"column": 25
"value": [
{
"start": 17,
"end": 26,
"type": "MustacheTag",
"expression": {
"type": "Identifier",
"start": 18,
"end": 25,
"loc": {
"start": {
"line": 1,
"column": 18
},
"end": {
"line": 1,
"column": 25
}
},
"name": "myColor"
}
},
"name": "myColor"
}
}
]
}
],
"children": []

@ -245,7 +245,7 @@ describe('runtime', () => {
.catch(err => {
// print a clickable link to open the directory
err.stack += `\n\ncmd-click: ${path.relative(process.cwd(), cwd)}/main.svelte`;
done();
done(err);
throw err;
})
.then(() => {

@ -0,0 +1,19 @@
export default {
html: '<div>12 120 70, 30+4=34</div>',
async test({ component, target, assert }) {
component.promise1 = Promise.resolve({width: 5, height: 6});
component.promise2 = Promise.reject({width: 6, height: 7});
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `
<div>30 300 110, 50+6=56</div>
<div>42 420 130, 60+7=67</div>
`);
component.constant = 20;
assert.htmlEqual(target.innerHTML, `
<div>30 600 220, 100+6=106</div>
<div>42 840 260, 120+7=127</div>
`);
}
};

@ -0,0 +1,23 @@
<script>
export let promise1 = {width: 3, height: 4};
export let promise2 = {width: 5, height: 7};
export let constant = 10;
function calculate(width, height, constant) {
return { area: width * height, volume: width * height * constant };
}
</script>
{#await promise1 then { width, height }}
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
{/await}
{#await promise2 catch { width, height }}
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
{/await}

@ -0,0 +1,19 @@
export default {
html: '<div>12 120 70, 30+4=34</div>',
async test({ component, target, assert }) {
component.promise1 = Promise.resolve({width: 5, height: 6});
component.promise2 = Promise.reject({width: 6, height: 7});
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `
<div>30 300 110, 50+6=56</div>
<div>42 420 130, 60+7=67</div>
`);
component.constant = 20;
assert.htmlEqual(target.innerHTML, `
<div>30 600 220, 100+6=106</div>
<div>42 840 260, 120+7=127</div>
`);
}
};

@ -0,0 +1,23 @@
<script>
export let promise1 = {width: 3, height: 4};
export let promise2 = {width: 5, height: 7};
export let constant = 10;
function calculate(width, height, constant) {
return { area: width * height, volume: width * height * constant };
}
</script>
{#await promise1 then box}
{@const {area, volume} = calculate(box.width, box.height, constant)}
{@const perimeter = (box.width + box.height) * constant}
{@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
<div>{area} {volume} {perimeter}, {width}+{height}={sum}</div>
{/await}
{#await promise2 catch box}
{@const {area, volume} = calculate(box.width, box.height, constant)}
{@const perimeter = (box.width + box.height) * constant}
{@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
<div>{area} {volume} {perimeter}, {width}+{height}={sum}</div>
{/await}

@ -0,0 +1,7 @@
<script>
export let box;
</script>
<slot name="box1" {box} />
<slot name="box2" width={box.width} height={box.height} />
<slot {box} />

@ -0,0 +1,46 @@
export default {
html: `
<div>12 120 70, 30+4=34</div>
<div>12 120 70, 30+4=34</div>
<div>12 120 70, 30+4=34</div>
<div slot="box1">
<div>12 120 70, 30+4=34</div>
</div>
<div slot="box2">
<div>12 120 70, 30+4=34</div>
</div>
<div>12 120 70, 30+4=34</div>
<div>12 120 70, 30+4=34</div>
`,
async test({ component, target, assert }) {
component.constant = 20;
assert.htmlEqual(target.innerHTML, `
<div>12 240 140, 60+4=64</div>
<div>12 240 140, 60+4=64</div>
<div>12 240 140, 60+4=64</div>
<div slot="box1">
<div>12 240 140, 60+4=64</div>
</div>
<div slot="box2">
<div>12 240 140, 60+4=64</div>
</div>
<div>12 240 140, 60+4=64</div>
<div>12 240 140, 60+4=64</div>
`);
component.box = {width: 5, height: 6};
assert.htmlEqual(target.innerHTML, `
<div>30 600 220, 100+6=106</div>
<div>30 600 220, 100+6=106</div>
<div>30 600 220, 100+6=106</div>
<div slot="box1">
<div>30 600 220, 100+6=106</div>
</div>
<div slot="box2">
<div>30 600 220, 100+6=106</div>
</div>
<div>30 600 220, 100+6=106</div>
<div>30 600 220, 100+6=106</div>
`);
}
};

@ -0,0 +1,60 @@
<script>
import Component from './Component.svelte';
export let box = {width: 3, height: 4};
export let constant = 10;
function calculate(width, height, constant) {
return { area: width * height, volume: width * height * constant };
}
</script>
<Component {box}>
<svelte:fragment slot="box1" let:box>
{@const {area, volume} = calculate(box.width, box.height, constant)}
{@const perimeter = (box.width + box.height) * constant}
{@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
<div>{area} {volume} {perimeter}, {width}+{height}={sum}</div>
</svelte:fragment>
<svelte:fragment slot="box2" let:width let:height>
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
</svelte:fragment>
<svelte:fragment let:box={{width, height}}>
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
</svelte:fragment>
</Component>
<Component {box} let:box>
<div slot="box1" let:box>
{@const {area, volume} = calculate(box.width, box.height, constant)}
{@const perimeter = (box.width + box.height) * constant}
{@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
<div>{area} {volume} {perimeter}, {width}+{height}={sum}</div>
</div>
<div slot="box2" let:width let:height>
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
</div>
{@const {area, volume} = calculate(box.width, box.height, constant)}
{@const perimeter = (box.width + box.height) * constant}
{@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
<div>{area} {volume} {perimeter}, {width}+{height}={sum}</div>
</Component>
<Component {box} let:box={{width, height}}>
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
</Component>

@ -0,0 +1,12 @@
export default {
html: `
<div>7</div>
`,
async test({ component, target, assert }) {
component.a = 5;
assert.htmlEqual(target.innerHTML, `
<div>9</div>
`);
}
};

@ -0,0 +1,10 @@
<script>
export let value = 4;
export let a = 3;
export let b = 4;
</script>
{#each [value] as n}
{@const ab = a + b}
<div>{ab}</div>
{/each}

@ -0,0 +1,30 @@
export default {
html: `
<div>12 120 70, 30+4=34</div>
<div>35 350 120, 50+7=57</div>
<div>48 480 140, 60+8=68</div>
`,
async test({ component, target, assert }) {
component.constant = 20;
assert.htmlEqual(target.innerHTML, `
<div>12 240 140, 60+4=64</div>
<div>35 700 240, 100+7=107</div>
<div>48 960 280, 120+8=128</div>
`);
component.boxes = [
{width: 3, height: 4},
{width: 4, height: 5},
{width: 5, height: 6},
{width: 6, height: 7}
];
assert.htmlEqual(target.innerHTML, `
<div>12 240 140, 60+4=64</div>
<div>20 400 180, 80+5=85</div>
<div>30 600 220, 100+6=106</div>
<div>42 840 260, 120+7=127</div>
`);
}
};

@ -0,0 +1,19 @@
<script>
export let boxes = [
{width: 3, height: 4},
{width: 5, height: 7},
{width: 6, height: 8},
];
export let constant = 10;
function calculate(width, height, constant) {
return { area: width * height, volume: width * height * constant };
}
</script>
{#each boxes as { width, height }}
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
{/each}

@ -0,0 +1,30 @@
export default {
html: `
<div>12 120 70, 30+4=34</div>
<div>35 350 120, 50+7=57</div>
<div>48 480 140, 60+8=68</div>
`,
async test({ component, target, assert }) {
component.constant = 20;
assert.htmlEqual(target.innerHTML, `
<div>12 240 140, 60+4=64</div>
<div>35 700 240, 100+7=107</div>
<div>48 960 280, 120+8=128</div>
`);
component.boxes = [
{width: 3, height: 4},
{width: 4, height: 5},
{width: 5, height: 6},
{width: 6, height: 7}
];
assert.htmlEqual(target.innerHTML, `
<div>12 240 140, 60+4=64</div>
<div>20 400 180, 80+5=85</div>
<div>30 600 220, 100+6=106</div>
<div>42 840 260, 120+7=127</div>
`);
}
};

@ -0,0 +1,19 @@
<script>
export let boxes = [
{width: 3, height: 4},
{width: 5, height: 7},
{width: 6, height: 8},
];
export let constant = 10;
function calculate(width, height, constant) {
return { area: width * height, volume: width * height * constant };
}
</script>
{#each boxes as box}
{@const {area, volume} = calculate(box.width, box.height, constant)}
{@const perimeter = (box.width + box.height) * constant}
{@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
<div>{area} {volume} {perimeter}, {width}+{height}={sum}</div>
{/each}

@ -0,0 +1,12 @@
export default {
html: `
<div>4 ^ 4 = 256</div>
`,
async test({ component, target, assert }) {
component.value = 3;
assert.htmlEqual(target.innerHTML, `
<div>3 ^ 4 = 81</div>
`);
}
};

@ -0,0 +1,11 @@
<script>
export let value = 4;
</script>
{#each [value] as n}
<div>{n} ^ 4 = {hypercubed}</div>
{@const squared = n * n}
{@const cubed = squared * n}
{@const hypercubed = cubed * n}
{/each}

@ -0,0 +1,12 @@
export default {
html: `
<div>4 ^ 4 = 256</div>
`,
async test({ component, target, assert }) {
component.value = 3;
assert.htmlEqual(target.innerHTML, `
<div>3 ^ 4 = 81</div>
`);
}
};

@ -0,0 +1,11 @@
<script>
export let value = 4;
</script>
{#each [value] as n}
<div>{n} ^ 4 = {hypercubed}</div>
{@const squared = n * n}
{@const cubed = squared * n}
{@const hypercubed = cubed * n}
{/each}

@ -0,0 +1,43 @@
export default {
html: `
<b>7</b>
<u>11</u>
<u>15</u>
<i>7</i>
<b>19</b>
<u>23</u>
<u>27</u>
<i>19</i>
`,
async test({ component, target, assert }) {
component.numbers = [
{
a: 4,
b: 5,
children: [
{ a: 6, b: 7 },
{ a: 8, b: 9 }
]
},
{
a: 10,
b: 11,
children: [
{ a: 12, b: 13 },
{ a: 14, b: 15 }
]
}
];
assert.htmlEqual(target.innerHTML, `
<b>9</b>
<u>13</u>
<u>17</u>
<i>9</i>
<b>21</b>
<u>25</u>
<u>29</u>
<i>21</i>
`);
}
};

@ -0,0 +1,31 @@
<script>
export let numbers = [
{
a: 3,
b: 4,
children: [
{ a: 5, b: 6 },
{ a: 7, b: 8 }
]
},
{
a: 9,
b: 10,
children: [
{ a: 11, b: 12 },
{ a: 13, b: 14 }
]
}
];
</script>
{#each numbers as {a, b, children}}
{@const ab = a + b}
<b>{ab}</b>
{#each children as {a, b}}
{@const ab = a + b}
<u>{ab}</u>
{/each}
<i>{ab}</i>
{/each}

@ -1,18 +1,25 @@
export default {
html: `
<p style="color: red;"></p>
<p style="color: red;"></p>
`,
test({ assert, component, target, window }) {
const p = target.querySelector('p');
const [p1, p2] = target.querySelectorAll('p');
let styles = window.getComputedStyle(p);
assert.equal(styles.color, 'red');
assert.equal(window.getComputedStyle(p1).color, 'red');
assert.equal(window.getComputedStyle(p2).color, 'red');
component.color = 'blue';
assert.htmlEqual(target.innerHTML, '<p style="color: blue;"></p>');
assert.htmlEqual(
target.innerHTML,
`
<p style="color: blue;"></p>
<p style="color: blue;"></p>
`
);
styles = window.getComputedStyle(p);
assert.equal(styles.color, 'blue');
assert.equal(window.getComputedStyle(p1).color, 'blue');
assert.equal(window.getComputedStyle(p2).color, 'blue');
}
};

@ -3,3 +3,7 @@
</script>
<p style:color />
{#each [1] as _}
<p style:color />
{/each}

@ -0,0 +1,18 @@
export default {
html: `
<div style="background-image: url(https://raw.githubusercontent.com/sveltejs/branding/master/svelte-vertical.png); --css-variable: rgba(0, 0, 0, 1);"></div>
`,
test({ assert, target, window }) {
const div = target.querySelector('div');
const styles = window.getComputedStyle(div);
assert.equal(styles['background-image'], 'url(https://raw.githubusercontent.com/sveltejs/branding/master/svelte-vertical.png)');
assert.equal(styles.getPropertyValue('--css-variable'), 'rgba(0, 0, 0, 1)');
assert.htmlEqual(
target.innerHTML,
'<div style="background-image: url(https://raw.githubusercontent.com/sveltejs/branding/master/svelte-vertical.png); --css-variable: rgba(0, 0, 0, 1);"></div>'
);
}
};

@ -0,0 +1,11 @@
<script>
let url =
"https://raw.githubusercontent.com/sveltejs/branding/master/svelte-vertical.png";
let alpha = 1;
</script>
<div
style:background-image="url({url})"
style:--css-variable="rgba(0, 0, 0, {alpha})"
/>

@ -128,13 +128,13 @@ describe('ssr', () => {
}
if (show) showOutput(dir, { generate: 'ssr', format: 'cjs' });
done();
} catch (err) {
showOutput(dir, { generate: 'ssr', format: 'cjs' });
err.stack += `\n\ncmd-click: ${path.relative(process.cwd(), dir)}/main.svelte`;
throw err;
done(err);
} finally {
set_current_component(null);
done();
}
});
});

@ -0,0 +1,10 @@
<script>
function flip(){}
</script>
<div>
{#each [] as n (n)}
{@const a = n}
<div animate:flip={a} />
{/each}
</div>

@ -0,0 +1,7 @@
<script>
function flip() {}
</script>
<div>
{#each [] as n (n)} <div animate:flip /> {/each}
</div>

@ -0,0 +1,9 @@
[
{
"code": "invalid-const-declaration",
"message": "'a' has already been declared",
"start": { "line": 7, "column": 2, "character": 84 },
"end": { "line": 7, "column": 19, "character": 101 },
"pos": 84
}
]

@ -0,0 +1,8 @@
<script>
export let array;
</script>
{#each array as item}
{@const a = item}
{@const a = item}
{/each}

@ -0,0 +1,9 @@
[
{
"code": "invalid-const-declaration",
"message": "'item' has already been declared",
"start": { "line": 6, "column": 2, "character": 64 },
"end": { "line": 6, "column": 21, "character": 83 },
"pos": 64
}
]

@ -0,0 +1,7 @@
<script>
export let array;
</script>
{#each array as item}
{@const item = 123}
{/each}

@ -0,0 +1,9 @@
[
{
"code": "cyclical-const-tags",
"message": "Cyclical dependency detected: b → c → b",
"start": { "line": 6, "column": 2, "character": 61 },
"end": { "line": 6, "column": 20, "character": 79 },
"pos": 61
}
]

@ -0,0 +1,8 @@
<script>
export let array;
</script>
{#each array as a}
{@const b = a + c}
{@const c = b + a}
{/each}

@ -0,0 +1,10 @@
<script>
export let array;
</script>
{#each array as a}
{@const b = a + 1}
<div />
{/each}
{b}

@ -0,0 +1,17 @@
[
{
"code": "missing-declaration",
"message": "'b' is not defined",
"pos": 100,
"start": {
"character": 100,
"column": 1,
"line": 10
},
"end": {
"character": 101,
"column": 2,
"line": 10
}
}
]

@ -0,0 +1,9 @@
[
{
"code": "invalid-const-placement",
"message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>",
"start": { "line": 5, "column": 0, "character": 36 },
"end": { "line": 5, "column": 18, "character": 54 },
"pos": 36
}
]

@ -0,0 +1,5 @@
<script>
export let a;
</script>
{@const b = a + 1}

@ -0,0 +1,9 @@
[
{
"code": "invalid-const-placement",
"message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>",
"start": { "line": 6, "column": 2, "character": 46 },
"end": { "line": 6, "column": 20, "character": 64 },
"pos": 46
}
]

@ -0,0 +1,7 @@
<script>
export let a;
</script>
{#if a}
{@const b = a + 1}
{/if}

@ -0,0 +1,9 @@
[
{
"code": "invalid-const-placement",
"message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>",
"start": { "line": 7, "column": 4, "character": 63 },
"end": { "line": 7, "column": 18, "character": 77 },
"pos": 63
}
]

@ -0,0 +1,9 @@
<script>
export let a;
</script>
{#each a as i}
<div>
{@const b = i}
</div>
{/each}

@ -0,0 +1,9 @@
[
{
"code": "invalid-const-update",
"message": "'b' is declared using {@const ...} and is read-only",
"start": { "line": 7, "column": 26, "character": 106 },
"end": { "line": 7, "column": 30, "character": 110 },
"pos": 106
}
]

@ -0,0 +1,8 @@
<script>
export let array;
</script>
{#each array as a}
{@const b = a + 1}
<button on:click={() => b ++} />
{/each}

@ -0,0 +1,9 @@
[
{
"code": "invalid-binding",
"message": "Cannot bind to a variable declared with {@const ...}",
"start": { "line": 7, "column": 9, "character": 89 },
"end": { "line": 7, "column": 23, "character": 103 },
"pos": 89
}
]

@ -0,0 +1,8 @@
<script>
export let array;
</script>
{#each array as a}
{@const b = a + 1}
<input bind:value={b} />
{/each}
Loading…
Cancel
Save