diff --git a/CHANGELOG.md b/CHANGELOG.md index 39dc47b387..4df49ecc77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/package-lock.json b/package-lock.json index c0f623415d..8bddba41a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 7d36c10971..dfba5a0a3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "3.45.0", + "version": "3.46.2", "description": "Cybernetically enhanced web apps", "module": "index.mjs", "main": "index", diff --git a/site/content/blog/2022-01-13-accelerating-sveltes-development.md b/site/content/blog/2022-01-13-accelerating-sveltes-development.md new file mode 100644 index 0000000000..2eeebde17b --- /dev/null +++ b/site/content/blog/2022-01-13-accelerating-sveltes-development.md @@ -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 you’re 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, we’ve been developing the [SvelteKit](https://kit.svelte.dev/) application framework. We’re 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, Svelte’s creator, has [joined Vercel to work on Svelte full-time](https://vercel.com/blog/vercel-welcomes-rich-harris-creator-of-svelte). We’re incredibly excited to have Rich’s 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 Rich’s 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 SvelteKit’s ability to run wherever JavaScript does. + +We’ve 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 we’ve 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 community’s 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 Svelte’s reputation as a friendly, welcoming community, and we’re 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) + +We’re 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 `` 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 SvelteKit’s 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. diff --git a/site/content/docs/02-template-syntax.md b/site/content/docs/02-template-syntax.md index e6db1854d2..e433750977 100644 --- a/site/content/docs/02-template-syntax.md +++ b/site/content/docs/02-template-syntax.md @@ -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 + + +{#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}`, `` or ``. + ### Element directives @@ -1648,6 +1671,7 @@ All except `scrollX` and `scrollY` are readonly. ``` +> 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()`. ### `` diff --git a/site/content/docs/04-compile-time.md b/site/content/docs/04-compile-time.md index 6ba5dabacc..1ee41eeb93 100644 --- a/site/content/docs/04-compile-time.md +++ b/site/content/docs/04-compile-time.md @@ -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 diff --git a/site/content/tutorial/15-context/01-context-api/app-a/mapbox.js b/site/content/tutorial/15-context/01-context-api/app-a/mapbox.js index 55b4200038..9c5cec430c 100644 --- a/site/content/tutorial/15-context/01-context-api/app-a/mapbox.js +++ b/site/content/tutorial/15-context/01-context-api/app-a/mapbox.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 }; \ No newline at end of file diff --git a/site/content/tutorial/15-context/01-context-api/app-b/mapbox.js b/site/content/tutorial/15-context/01-context-api/app-b/mapbox.js index 55b4200038..9c5cec430c 100644 --- a/site/content/tutorial/15-context/01-context-api/app-b/mapbox.js +++ b/site/content/tutorial/15-context/01-context-api/app-b/mapbox.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 }; \ No newline at end of file diff --git a/site/content/tutorial/15-context/01-context-api/text.md b/site/content/tutorial/15-context/01-context-api/text.md index efb6157652..e1f095c494 100644 --- a/site/content/tutorial/15-context/01-context-api/text.md +++ b/site/content/tutorial/15-context/01-context-api/text.md @@ -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 diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index 54263c3eb9..f6e9d17ea3 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -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}, or ' + }, + 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(' → ')}` + }) }; diff --git a/src/compiler/compile/nodes/Binding.ts b/src/compiler/compile/nodes/Binding.ts index 0d9c069f0c..14c44a3985 100644 --- a/src/compiler/compile/nodes/Binding.ts +++ b/src/compiler/compile/nodes/Binding.ts @@ -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); diff --git a/src/compiler/compile/nodes/CatchBlock.ts b/src/compiler/compile/nodes/CatchBlock.ts index 1a92f617bb..ba6a4b77a6 100644 --- a/src/compiler/compile/nodes/CatchBlock.ts +++ b/src/compiler/compile/nodes/CatchBlock.ts @@ -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(); diff --git a/src/compiler/compile/nodes/ConstTag.ts b/src/compiler/compile/nodes/ConstTag.ts new file mode 100644 index 0000000000..157361fb67 --- /dev/null +++ b/src/compiler/compile/nodes/ConstTag.ts @@ -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 = new Set(); + dependencies: Set = 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); + }); + } +} diff --git a/src/compiler/compile/nodes/DefaultSlotTemplate.ts b/src/compiler/compile/nodes/DefaultSlotTemplate.ts deleted file mode 100644 index 7f55ee3a60..0000000000 --- a/src/compiler/compile/nodes/DefaultSlotTemplate.ts +++ /dev/null @@ -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; - } -} diff --git a/src/compiler/compile/nodes/EachBlock.ts b/src/compiler/compile/nodes/EachBlock.ts index a8312b2462..bb78845dd9 100644 --- a/src/compiler/compile/nodes/EachBlock.ts +++ b/src/compiler/compile/nodes/EachBlock.ts @@ -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() === ''; +} diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index d5beab0394..7f0a8efd21 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -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': diff --git a/src/compiler/compile/nodes/InlineComponent.ts b/src/compiler/compile/nodes/InlineComponent.ts index a7bc986e9b..9d88b136cd 100644 --- a/src/compiler/compile/nodes/InlineComponent.ts +++ b/src/compiler/compile/nodes/InlineComponent.ts @@ -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); } diff --git a/src/compiler/compile/nodes/SlotTemplate.ts b/src/compiler/compile/nodes/SlotTemplate.ts index fe94a24e4e..cd18859603 100644 --- a/src/compiler/compile/nodes/SlotTemplate.ts +++ b/src/compiler/compile/nodes/SlotTemplate.ts @@ -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() { diff --git a/src/compiler/compile/nodes/Style.ts b/src/compiler/compile/nodes/Style.ts deleted file mode 100644 index e922f6f578..0000000000 --- a/src/compiler/compile/nodes/Style.ts +++ /dev/null @@ -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; - } -} diff --git a/src/compiler/compile/nodes/StyleDirective.ts b/src/compiler/compile/nodes/StyleDirective.ts new file mode 100644 index 0000000000..ea340cf2dc --- /dev/null +++ b/src/compiler/compile/nodes/StyleDirective.ts @@ -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; + } + + } +} diff --git a/src/compiler/compile/nodes/ThenBlock.ts b/src/compiler/compile/nodes/ThenBlock.ts index 720f88ad78..6aee3f916f 100644 --- a/src/compiler/compile/nodes/ThenBlock.ts +++ b/src/compiler/compile/nodes/ThenBlock.ts @@ -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(); diff --git a/src/compiler/compile/nodes/interfaces.ts b/src/compiler/compile/nodes/interfaces.ts index 2e2fa8b5e0..2c636b36f3 100644 --- a/src/compiler/compile/nodes/interfaces.ts +++ b/src/compiler/compile/nodes/interfaces.ts @@ -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; diff --git a/src/compiler/compile/nodes/shared/Expression.ts b/src/compiler/compile/nodes/shared/Expression.ts index 7c6fc39185..c54540f249 100644 --- a/src/compiler/compile/nodes/shared/Expression.ts +++ b/src/compiler/compile/nodes/shared/Expression.ts @@ -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); } diff --git a/src/compiler/compile/nodes/shared/TemplateScope.ts b/src/compiler/compile/nodes/shared/TemplateScope.ts index df694ed45e..eae8640ca0 100644 --- a/src/compiler/compile/nodes/shared/TemplateScope.ts +++ b/src/compiler/compile/nodes/shared/TemplateScope.ts @@ -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; @@ -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'; + } } diff --git a/src/compiler/compile/nodes/shared/get_const_tags.ts b/src/compiler/compile/nodes/shared/get_const_tags.ts new file mode 100644 index 0000000000..77bc26fe9c --- /dev/null +++ b/src/compiler/compile/nodes/shared/get_const_tags.ts @@ -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>] { + const const_tags: ConstTagType[] = []; + const others: Array> = []; + + 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>]; +} + +function sort_consts_nodes(consts_nodes: ConstTag[], component: Component) { + type ConstNode = { + assignees: Set; + dependencies: Set; + 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); +} diff --git a/src/compiler/compile/nodes/shared/map_children.ts b/src/compiler/compile/nodes/shared/map_children.ts index b1d0816aac..8fe53088bd 100644 --- a/src/compiler/compile/nodes/shared/map_children.ts +++ b/src/compiler/compile/nodes/shared/map_children.ts @@ -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; diff --git a/src/compiler/compile/render_dom/Renderer.ts b/src/compiler/compile/render_dom/Renderer.ts index 94cd539082..c585cb3f72 100644 --- a/src/compiler/compile/render_dom/Renderer.ts +++ b/src/compiler/compile/render_dom/Renderer.ts @@ -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; diff --git a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts index 637d32676c..9c07267223 100644 --- a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts @@ -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)`); diff --git a/src/compiler/compile/render_dom/wrappers/EachBlock.ts b/src/compiler/compile/render_dom/wrappers/EachBlock.ts index 5a5a217bce..b77b2472df 100644 --- a/src/compiler/compile/render_dom/wrappers/EachBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/EachBlock.ts @@ -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; } `); diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index b9a8ce39df..de758c5513 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -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); } diff --git a/src/compiler/compile/render_dom/wrappers/SlotTemplate.ts b/src/compiler/compile/render_dom/wrappers/SlotTemplate.ts index daf41e10c3..a50f74fc04 100644 --- a/src/compiler/compile/render_dom/wrappers/SlotTemplate.ts +++ b/src/compiler/compile/render_dom/wrappers/SlotTemplate.ts @@ -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)`); + } } } diff --git a/src/compiler/compile/render_dom/wrappers/shared/add_const_tags.ts b/src/compiler/compile/render_dom/wrappers/shared/add_const_tags.ts new file mode 100644 index 0000000000..cd4dc7f033 --- /dev/null +++ b/src/compiler/compile/render_dom/wrappers/shared/add_const_tags.ts @@ -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); + }); + }); +} diff --git a/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts b/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts index e09abc817c..eecc8ddf5b 100644 --- a/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts +++ b/src/compiler/compile/render_ssr/handlers/AwaitBlock.ts @@ -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}) `); } diff --git a/src/compiler/compile/render_ssr/handlers/EachBlock.ts b/src/compiler/compile/render_ssr/handlers/EachBlock.ts index 0d05836e82..008c023e11 100644 --- a/src/compiler/compile/render_ssr/handlers/EachBlock.ts +++ b/src/compiler/compile/render_ssr/handlers/EachBlock.ts @@ -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(); diff --git a/src/compiler/compile/render_ssr/handlers/InlineComponent.ts b/src/compiler/compile/render_ssr/handlers/InlineComponent.ts index 2b3ca5fbcb..626365e3e2 100644 --- a/src/compiler/compile/render_ssr/handlers/InlineComponent.ts +++ b/src/compiler/compile/render_ssr/handlers/InlineComponent.ts @@ -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}; }` ); }); } diff --git a/src/compiler/compile/render_ssr/handlers/SlotTemplate.ts b/src/compiler/compile/render_ssr/handlers/SlotTemplate.ts index 568942c3f5..09f3293301 100644 --- a/src/compiler/compile/render_ssr/handlers/SlotTemplate.ts +++ b/src/compiler/compile/render_ssr/handlers/SlotTemplate.ts @@ -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; }) { 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) }); } } diff --git a/src/compiler/compile/render_ssr/handlers/shared/get_const_tags.ts b/src/compiler/compile/render_ssr/handlers/shared/get_const_tags.ts new file mode 100644 index 0000000000..2c156609cd --- /dev/null +++ b/src/compiler/compile/render_ssr/handlers/shared/get_const_tags.ts @@ -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 + }; + }) + }; +} diff --git a/src/compiler/compile/utils/nodes_to_template_literal.ts b/src/compiler/compile/utils/nodes_to_template_literal.ts new file mode 100644 index 0000000000..46da953261 --- /dev/null +++ b/src/compiler/compile/utils/nodes_to_template_literal.ts @@ -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): 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; +} diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts index 3e54ea55a3..a9920750f9 100644 --- a/src/compiler/interfaces.ts +++ b/src/compiler/interfaces.ts @@ -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 diff --git a/src/compiler/parse/state/mustache.ts b/src/compiler/parse/state/mustache.ts index 584f9d6e9a..da9016a9df 100644 --- a/src/compiler/parse/state/mustache.ts +++ b/src/compiler/parse/state/mustache.ts @@ -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); diff --git a/src/compiler/parse/state/tag.ts b/src/compiler/parse/state/tag.ts index 81d8fff1c4..7de0f88413 100644 --- a/src/compiler/parse/state/tag.ts +++ b/src/compiler/parse/state/tag.ts @@ -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): 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) { const start = parser.index; @@ -396,14 +365,22 @@ function read_attribute(parser: Parser, unique_names: Set) { 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) : 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) { } // Directive name is expression, e.g.

- 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'; diff --git a/src/compiler/parse/utils/node.ts b/src/compiler/parse/utils/node.ts index 0d39529b59..944bdb4c58 100644 --- a/src/compiler/parse/utils/node.ts +++ b/src/compiler/parse/utils/node.ts @@ -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': diff --git a/src/runtime/animate/index.ts b/src/runtime/animate/index.ts index a68e6b0dc3..ff73bbc29e 100644 --- a/src/runtime/animate/index.ts +++ b/src/runtime/animate/index.ts @@ -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; diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index d425a14af0..389d8bfdf4 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -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); diff --git a/src/runtime/internal/style_manager.ts b/src/runtime/internal/style_manager.ts index 0993b3bf18..6907e5af02 100644 --- a/src/runtime/internal/style_manager.ts +++ b/src/runtime/internal/style_manager.ts @@ -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; +interface StyleInformation { + stylesheet: CSSStyleSheet; + rules: Record; } -const active_docs = new Set(); +// 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(); 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(); }); } diff --git a/test/hydration/samples/raw-with-empty-line-at-top/_after.html b/test/hydration/samples/raw-with-empty-line-at-top/_after.html new file mode 100644 index 0000000000..28ee6452d3 --- /dev/null +++ b/test/hydration/samples/raw-with-empty-line-at-top/_after.html @@ -0,0 +1,6 @@ + +

before
+
+ +a b c +
after
diff --git a/test/hydration/samples/raw-with-empty-line-at-top/_before.html b/test/hydration/samples/raw-with-empty-line-at-top/_before.html new file mode 100644 index 0000000000..28ee6452d3 --- /dev/null +++ b/test/hydration/samples/raw-with-empty-line-at-top/_before.html @@ -0,0 +1,6 @@ + +
before
+
+ +a b c +
after
diff --git a/test/hydration/samples/raw-with-empty-line-at-top/main.svelte b/test/hydration/samples/raw-with-empty-line-at-top/main.svelte new file mode 100644 index 0000000000..2d08a3f418 --- /dev/null +++ b/test/hydration/samples/raw-with-empty-line-at-top/main.svelte @@ -0,0 +1,11 @@ + + +
before
+
+ +{#each content as c} + {@html c} +{/each} +
after
diff --git a/test/js/samples/debug-ssr-foo/expected.js b/test/js/samples/debug-ssr-foo/expected.js index 69da37b2d9..e0a29f18de 100644 --- a/test/js/samples/debug-ssr-foo/expected.js +++ b/test/js/samples/debug-ssr-foo/expected.js @@ -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 => `${escape(thing.name)} - ${debug(null, 7, 2, { foo })}`)} + return `${each(things, thing => { + return `${escape(thing.name)} + ${debug(null, 7, 2, { foo })}`; + })}

foo: ${escape(foo)}

`; }); diff --git a/test/parser/samples/attribute-style-directive-shorthand/output.json b/test/parser/samples/attribute-style-directive-shorthand/output.json index d4e81b6e4b..ac658408e6 100644 --- a/test/parser/samples/attribute-style-directive-shorthand/output.json +++ b/test/parser/samples/attribute-style-directive-shorthand/output.json @@ -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": [] } ] } -} +} \ No newline at end of file diff --git a/test/parser/samples/attribute-style-directive-string/input.svelte b/test/parser/samples/attribute-style-directive-string/input.svelte index b2eb6bfef8..dd8ec8dc94 100644 --- a/test/parser/samples/attribute-style-directive-string/input.svelte +++ b/test/parser/samples/attribute-style-directive-string/input.svelte @@ -1 +1,7 @@ -
\ No newline at end of file +
+
+
+
+
+
+
\ No newline at end of file diff --git a/test/parser/samples/attribute-style-directive-string/output.json b/test/parser/samples/attribute-style-directive-string/output.json index 4cf2bd30c2..e81e7be3c9 100644 --- a/test/parser/samples/attribute-style-directive-string/output.json +++ b/test/parser/samples/attribute-style-directive-string/output.json @@ -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": [] diff --git a/test/parser/samples/attribute-style-directive/output.json b/test/parser/samples/attribute-style-directive/output.json index 36906045ce..f36ce6b01e 100644 --- a/test/parser/samples/attribute-style-directive/output.json +++ b/test/parser/samples/attribute-style-directive/output.json @@ -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": [] diff --git a/test/runtime/index.ts b/test/runtime/index.ts index 4fc3880208..f3c962f1d9 100644 --- a/test/runtime/index.ts +++ b/test/runtime/index.ts @@ -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(() => { diff --git a/test/runtime/samples/const-tag-await-then-destructuring/_config.js b/test/runtime/samples/const-tag-await-then-destructuring/_config.js new file mode 100644 index 0000000000..cb40e7c456 --- /dev/null +++ b/test/runtime/samples/const-tag-await-then-destructuring/_config.js @@ -0,0 +1,19 @@ +export default { + html: '
12 120 70, 30+4=34
', + 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, ` +
30 300 110, 50+6=56
+
42 420 130, 60+7=67
+ `); + + component.constant = 20; + assert.htmlEqual(target.innerHTML, ` +
30 600 220, 100+6=106
+
42 840 260, 120+7=127
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-await-then-destructuring/main.svelte b/test/runtime/samples/const-tag-await-then-destructuring/main.svelte new file mode 100644 index 0000000000..ab450822c6 --- /dev/null +++ b/test/runtime/samples/const-tag-await-then-destructuring/main.svelte @@ -0,0 +1,23 @@ + + +{#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]} +
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+{/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]} +
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+{/await} \ No newline at end of file diff --git a/test/runtime/samples/const-tag-await-then/_config.js b/test/runtime/samples/const-tag-await-then/_config.js new file mode 100644 index 0000000000..cb40e7c456 --- /dev/null +++ b/test/runtime/samples/const-tag-await-then/_config.js @@ -0,0 +1,19 @@ +export default { + html: '
12 120 70, 30+4=34
', + 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, ` +
30 300 110, 50+6=56
+
42 420 130, 60+7=67
+ `); + + component.constant = 20; + assert.htmlEqual(target.innerHTML, ` +
30 600 220, 100+6=106
+
42 840 260, 120+7=127
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-await-then/main.svelte b/test/runtime/samples/const-tag-await-then/main.svelte new file mode 100644 index 0000000000..f8eed87115 --- /dev/null +++ b/test/runtime/samples/const-tag-await-then/main.svelte @@ -0,0 +1,23 @@ + + +{#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]} +
{area} {volume} {perimeter}, {width}+{height}={sum}
+{/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]} +
{area} {volume} {perimeter}, {width}+{height}={sum}
+{/await} \ No newline at end of file diff --git a/test/runtime/samples/const-tag-component/Component.svelte b/test/runtime/samples/const-tag-component/Component.svelte new file mode 100644 index 0000000000..68ba82f245 --- /dev/null +++ b/test/runtime/samples/const-tag-component/Component.svelte @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/test/runtime/samples/const-tag-component/_config.js b/test/runtime/samples/const-tag-component/_config.js new file mode 100644 index 0000000000..66399a06fa --- /dev/null +++ b/test/runtime/samples/const-tag-component/_config.js @@ -0,0 +1,46 @@ +export default { + html: ` +
12 120 70, 30+4=34
+
12 120 70, 30+4=34
+
12 120 70, 30+4=34
+
+
12 120 70, 30+4=34
+
+
+
12 120 70, 30+4=34
+
+
12 120 70, 30+4=34
+
12 120 70, 30+4=34
+ `, + async test({ component, target, assert }) { + component.constant = 20; + assert.htmlEqual(target.innerHTML, ` +
12 240 140, 60+4=64
+
12 240 140, 60+4=64
+
12 240 140, 60+4=64
+
+
12 240 140, 60+4=64
+
+
+
12 240 140, 60+4=64
+
+
12 240 140, 60+4=64
+
12 240 140, 60+4=64
+ `); + + component.box = {width: 5, height: 6}; + assert.htmlEqual(target.innerHTML, ` +
30 600 220, 100+6=106
+
30 600 220, 100+6=106
+
30 600 220, 100+6=106
+
+
30 600 220, 100+6=106
+
+
+
30 600 220, 100+6=106
+
+
30 600 220, 100+6=106
+
30 600 220, 100+6=106
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-component/main.svelte b/test/runtime/samples/const-tag-component/main.svelte new file mode 100644 index 0000000000..1854c3fba9 --- /dev/null +++ b/test/runtime/samples/const-tag-component/main.svelte @@ -0,0 +1,60 @@ + + + + + {@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]} +
{area} {volume} {perimeter}, {width}+{height}={sum}
+
+ + + {@const {area, volume} = calculate(width, height, constant)} + {@const perimeter = (width + height) * constant} + {@const [_width, _height, sum] = [width * constant, height, width * constant + height]} +
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+
+ + + {@const {area, volume} = calculate(width, height, constant)} + {@const perimeter = (width + height) * constant} + {@const [_width, _height, sum] = [width * constant, height, width * constant + height]} +
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+
+
+ + +
+ {@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]} +
{area} {volume} {perimeter}, {width}+{height}={sum}
+
+ +
+ {@const {area, volume} = calculate(width, height, constant)} + {@const perimeter = (width + height) * constant} + {@const [_width, _height, sum] = [width * constant, height, width * constant + height]} +
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+
+ + {@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]} +
{area} {volume} {perimeter}, {width}+{height}={sum}
+
+ + + {@const {area, volume} = calculate(width, height, constant)} + {@const perimeter = (width + height) * constant} + {@const [_width, _height, sum] = [width * constant, height, width * constant + height]} +
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+
diff --git a/test/runtime/samples/const-tag-dependencies/_config.js b/test/runtime/samples/const-tag-dependencies/_config.js new file mode 100644 index 0000000000..5cbb2da587 --- /dev/null +++ b/test/runtime/samples/const-tag-dependencies/_config.js @@ -0,0 +1,12 @@ +export default { + html: ` +
7
+ `, + async test({ component, target, assert }) { + component.a = 5; + + assert.htmlEqual(target.innerHTML, ` +
9
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-dependencies/main.svelte b/test/runtime/samples/const-tag-dependencies/main.svelte new file mode 100644 index 0000000000..cce3b0c1f7 --- /dev/null +++ b/test/runtime/samples/const-tag-dependencies/main.svelte @@ -0,0 +1,10 @@ + + +{#each [value] as n} + {@const ab = a + b} +
{ab}
+{/each} \ No newline at end of file diff --git a/test/runtime/samples/const-tag-each-destructure/_config.js b/test/runtime/samples/const-tag-each-destructure/_config.js new file mode 100644 index 0000000000..00f8b31540 --- /dev/null +++ b/test/runtime/samples/const-tag-each-destructure/_config.js @@ -0,0 +1,30 @@ +export default { + html: ` +
12 120 70, 30+4=34
+
35 350 120, 50+7=57
+
48 480 140, 60+8=68
+ `, + async test({ component, target, assert }) { + component.constant = 20; + + assert.htmlEqual(target.innerHTML, ` +
12 240 140, 60+4=64
+
35 700 240, 100+7=107
+
48 960 280, 120+8=128
+ `); + + component.boxes = [ + {width: 3, height: 4}, + {width: 4, height: 5}, + {width: 5, height: 6}, + {width: 6, height: 7} + ]; + + assert.htmlEqual(target.innerHTML, ` +
12 240 140, 60+4=64
+
20 400 180, 80+5=85
+
30 600 220, 100+6=106
+
42 840 260, 120+7=127
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-each-destructure/main.svelte b/test/runtime/samples/const-tag-each-destructure/main.svelte new file mode 100644 index 0000000000..7f0391d5e6 --- /dev/null +++ b/test/runtime/samples/const-tag-each-destructure/main.svelte @@ -0,0 +1,19 @@ + + +{#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]} +
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+{/each} \ No newline at end of file diff --git a/test/runtime/samples/const-tag-each/_config.js b/test/runtime/samples/const-tag-each/_config.js new file mode 100644 index 0000000000..00f8b31540 --- /dev/null +++ b/test/runtime/samples/const-tag-each/_config.js @@ -0,0 +1,30 @@ +export default { + html: ` +
12 120 70, 30+4=34
+
35 350 120, 50+7=57
+
48 480 140, 60+8=68
+ `, + async test({ component, target, assert }) { + component.constant = 20; + + assert.htmlEqual(target.innerHTML, ` +
12 240 140, 60+4=64
+
35 700 240, 100+7=107
+
48 960 280, 120+8=128
+ `); + + component.boxes = [ + {width: 3, height: 4}, + {width: 4, height: 5}, + {width: 5, height: 6}, + {width: 6, height: 7} + ]; + + assert.htmlEqual(target.innerHTML, ` +
12 240 140, 60+4=64
+
20 400 180, 80+5=85
+
30 600 220, 100+6=106
+
42 840 260, 120+7=127
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-each/main.svelte b/test/runtime/samples/const-tag-each/main.svelte new file mode 100644 index 0000000000..004f81cd83 --- /dev/null +++ b/test/runtime/samples/const-tag-each/main.svelte @@ -0,0 +1,19 @@ + + +{#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]} +
{area} {volume} {perimeter}, {width}+{height}={sum}
+{/each} \ No newline at end of file diff --git a/test/runtime/samples/const-tag-hoisting/_config.js b/test/runtime/samples/const-tag-hoisting/_config.js new file mode 100644 index 0000000000..f8f4a5c85e --- /dev/null +++ b/test/runtime/samples/const-tag-hoisting/_config.js @@ -0,0 +1,12 @@ +export default { + html: ` +
4 ^ 4 = 256
+ `, + async test({ component, target, assert }) { + component.value = 3; + + assert.htmlEqual(target.innerHTML, ` +
3 ^ 4 = 81
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-hoisting/main.svelte b/test/runtime/samples/const-tag-hoisting/main.svelte new file mode 100644 index 0000000000..49fce93274 --- /dev/null +++ b/test/runtime/samples/const-tag-hoisting/main.svelte @@ -0,0 +1,11 @@ + + +{#each [value] as n} +
{n} ^ 4 = {hypercubed}
+ + {@const squared = n * n} + {@const cubed = squared * n} + {@const hypercubed = cubed * n} +{/each} \ No newline at end of file diff --git a/test/runtime/samples/const-tag-ordering/_config.js b/test/runtime/samples/const-tag-ordering/_config.js new file mode 100644 index 0000000000..f8f4a5c85e --- /dev/null +++ b/test/runtime/samples/const-tag-ordering/_config.js @@ -0,0 +1,12 @@ +export default { + html: ` +
4 ^ 4 = 256
+ `, + async test({ component, target, assert }) { + component.value = 3; + + assert.htmlEqual(target.innerHTML, ` +
3 ^ 4 = 81
+ `); + } +}; diff --git a/test/runtime/samples/const-tag-ordering/main.svelte b/test/runtime/samples/const-tag-ordering/main.svelte new file mode 100644 index 0000000000..49fce93274 --- /dev/null +++ b/test/runtime/samples/const-tag-ordering/main.svelte @@ -0,0 +1,11 @@ + + +{#each [value] as n} +
{n} ^ 4 = {hypercubed}
+ + {@const squared = n * n} + {@const cubed = squared * n} + {@const hypercubed = cubed * n} +{/each} \ No newline at end of file diff --git a/test/runtime/samples/const-tag-shadow/_config.js b/test/runtime/samples/const-tag-shadow/_config.js new file mode 100644 index 0000000000..312235f113 --- /dev/null +++ b/test/runtime/samples/const-tag-shadow/_config.js @@ -0,0 +1,43 @@ +export default { + html: ` + 7 + 11 + 15 + 7 + 19 + 23 + 27 + 19 + `, + 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, ` + 9 + 13 + 17 + 9 + 21 + 25 + 29 + 21 + `); + } +}; diff --git a/test/runtime/samples/const-tag-shadow/main.svelte b/test/runtime/samples/const-tag-shadow/main.svelte new file mode 100644 index 0000000000..725121c733 --- /dev/null +++ b/test/runtime/samples/const-tag-shadow/main.svelte @@ -0,0 +1,31 @@ + + +{#each numbers as {a, b, children}} + {@const ab = a + b} + {ab} + {#each children as {a, b}} + {@const ab = a + b} + {ab} + {/each} + {ab} +{/each} \ No newline at end of file diff --git a/test/runtime/samples/inline-style-directive-shorthand/_config.js b/test/runtime/samples/inline-style-directive-shorthand/_config.js index 64043e5269..352d4a139c 100644 --- a/test/runtime/samples/inline-style-directive-shorthand/_config.js +++ b/test/runtime/samples/inline-style-directive-shorthand/_config.js @@ -1,18 +1,25 @@ export default { html: `

+

`, 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, '

'); + assert.htmlEqual( + target.innerHTML, + ` +

+

+ ` + ); - styles = window.getComputedStyle(p); - assert.equal(styles.color, 'blue'); + assert.equal(window.getComputedStyle(p1).color, 'blue'); + assert.equal(window.getComputedStyle(p2).color, 'blue'); } }; diff --git a/test/runtime/samples/inline-style-directive-shorthand/main.svelte b/test/runtime/samples/inline-style-directive-shorthand/main.svelte index 65bbff910b..2b5f8e3db7 100644 --- a/test/runtime/samples/inline-style-directive-shorthand/main.svelte +++ b/test/runtime/samples/inline-style-directive-shorthand/main.svelte @@ -3,3 +3,7 @@

+ +{#each [1] as _} +

+{/each} diff --git a/test/runtime/samples/inline-style-directive-string-variable-kebab-case/_config.js b/test/runtime/samples/inline-style-directive-string-variable-kebab-case/_config.js new file mode 100644 index 0000000000..3bd6d38ea5 --- /dev/null +++ b/test/runtime/samples/inline-style-directive-string-variable-kebab-case/_config.js @@ -0,0 +1,18 @@ +export default { + html: ` +

+ `, + + 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, + '
' + ); + } +}; diff --git a/test/runtime/samples/inline-style-directive-string-variable-kebab-case/main.svelte b/test/runtime/samples/inline-style-directive-string-variable-kebab-case/main.svelte new file mode 100644 index 0000000000..dbfba08d64 --- /dev/null +++ b/test/runtime/samples/inline-style-directive-string-variable-kebab-case/main.svelte @@ -0,0 +1,11 @@ + + +
diff --git a/test/server-side-rendering/index.ts b/test/server-side-rendering/index.ts index f4175c73a6..98d19a6006 100644 --- a/test/server-side-rendering/index.ts +++ b/test/server-side-rendering/index.ts @@ -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(); } }); }); diff --git a/test/validator/samples/animation-each-with-const/errors.json b/test/validator/samples/animation-each-with-const/errors.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/test/validator/samples/animation-each-with-const/errors.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/test/validator/samples/animation-each-with-const/input.svelte b/test/validator/samples/animation-each-with-const/input.svelte new file mode 100644 index 0000000000..1e236de76f --- /dev/null +++ b/test/validator/samples/animation-each-with-const/input.svelte @@ -0,0 +1,10 @@ + + +
+ {#each [] as n (n)} + {@const a = n} +
+ {/each} +
diff --git a/test/validator/samples/animation-each-with-whitespace/errors.json b/test/validator/samples/animation-each-with-whitespace/errors.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/test/validator/samples/animation-each-with-whitespace/errors.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/test/validator/samples/animation-each-with-whitespace/input.svelte b/test/validator/samples/animation-each-with-whitespace/input.svelte new file mode 100644 index 0000000000..3e063716ab --- /dev/null +++ b/test/validator/samples/animation-each-with-whitespace/input.svelte @@ -0,0 +1,7 @@ + + +
+ {#each [] as n (n)}
{/each} +
diff --git a/test/validator/samples/const-tag-conflict-1/errors.json b/test/validator/samples/const-tag-conflict-1/errors.json new file mode 100644 index 0000000000..59c72eccaf --- /dev/null +++ b/test/validator/samples/const-tag-conflict-1/errors.json @@ -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 + } +] diff --git a/test/validator/samples/const-tag-conflict-1/input.svelte b/test/validator/samples/const-tag-conflict-1/input.svelte new file mode 100644 index 0000000000..6633eeacdf --- /dev/null +++ b/test/validator/samples/const-tag-conflict-1/input.svelte @@ -0,0 +1,8 @@ + + +{#each array as item} + {@const a = item} + {@const a = item} +{/each} \ No newline at end of file diff --git a/test/validator/samples/const-tag-conflict-2/errors.json b/test/validator/samples/const-tag-conflict-2/errors.json new file mode 100644 index 0000000000..11e2535091 --- /dev/null +++ b/test/validator/samples/const-tag-conflict-2/errors.json @@ -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 + } +] diff --git a/test/validator/samples/const-tag-conflict-2/input.svelte b/test/validator/samples/const-tag-conflict-2/input.svelte new file mode 100644 index 0000000000..2be7207691 --- /dev/null +++ b/test/validator/samples/const-tag-conflict-2/input.svelte @@ -0,0 +1,7 @@ + + +{#each array as item} + {@const item = 123} +{/each} \ No newline at end of file diff --git a/test/validator/samples/const-tag-cyclical/errors.json b/test/validator/samples/const-tag-cyclical/errors.json new file mode 100644 index 0000000000..1436747798 --- /dev/null +++ b/test/validator/samples/const-tag-cyclical/errors.json @@ -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 + } +] diff --git a/test/validator/samples/const-tag-cyclical/input.svelte b/test/validator/samples/const-tag-cyclical/input.svelte new file mode 100644 index 0000000000..2f694be808 --- /dev/null +++ b/test/validator/samples/const-tag-cyclical/input.svelte @@ -0,0 +1,8 @@ + + +{#each array as a} + {@const b = a + c} + {@const c = b + a} +{/each} \ No newline at end of file diff --git a/test/validator/samples/const-tag-out-of-scope/input.svelte b/test/validator/samples/const-tag-out-of-scope/input.svelte new file mode 100644 index 0000000000..d1c2723e22 --- /dev/null +++ b/test/validator/samples/const-tag-out-of-scope/input.svelte @@ -0,0 +1,10 @@ + + +{#each array as a} + {@const b = a + 1} +
+{/each} + +{b} \ No newline at end of file diff --git a/test/validator/samples/const-tag-out-of-scope/warnings.json b/test/validator/samples/const-tag-out-of-scope/warnings.json new file mode 100644 index 0000000000..c85b417fe4 --- /dev/null +++ b/test/validator/samples/const-tag-out-of-scope/warnings.json @@ -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 + } + } +] diff --git a/test/validator/samples/const-tag-placement-1/errors.json b/test/validator/samples/const-tag-placement-1/errors.json new file mode 100644 index 0000000000..52de84e2be --- /dev/null +++ b/test/validator/samples/const-tag-placement-1/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "invalid-const-placement", + "message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, or ", + "start": { "line": 5, "column": 0, "character": 36 }, + "end": { "line": 5, "column": 18, "character": 54 }, + "pos": 36 + } +] diff --git a/test/validator/samples/const-tag-placement-1/input.svelte b/test/validator/samples/const-tag-placement-1/input.svelte new file mode 100644 index 0000000000..14ea3085e7 --- /dev/null +++ b/test/validator/samples/const-tag-placement-1/input.svelte @@ -0,0 +1,5 @@ + + +{@const b = a + 1} \ No newline at end of file diff --git a/test/validator/samples/const-tag-placement-2/errors.json b/test/validator/samples/const-tag-placement-2/errors.json new file mode 100644 index 0000000000..005b3ff8b7 --- /dev/null +++ b/test/validator/samples/const-tag-placement-2/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "invalid-const-placement", + "message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, or ", + "start": { "line": 6, "column": 2, "character": 46 }, + "end": { "line": 6, "column": 20, "character": 64 }, + "pos": 46 + } +] diff --git a/test/validator/samples/const-tag-placement-2/input.svelte b/test/validator/samples/const-tag-placement-2/input.svelte new file mode 100644 index 0000000000..a843f9d58f --- /dev/null +++ b/test/validator/samples/const-tag-placement-2/input.svelte @@ -0,0 +1,7 @@ + + +{#if a} + {@const b = a + 1} +{/if} \ No newline at end of file diff --git a/test/validator/samples/const-tag-placement-3/errors.json b/test/validator/samples/const-tag-placement-3/errors.json new file mode 100644 index 0000000000..cc0ba4d74d --- /dev/null +++ b/test/validator/samples/const-tag-placement-3/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "invalid-const-placement", + "message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, or ", + "start": { "line": 7, "column": 4, "character": 63 }, + "end": { "line": 7, "column": 18, "character": 77 }, + "pos": 63 + } +] diff --git a/test/validator/samples/const-tag-placement-3/input.svelte b/test/validator/samples/const-tag-placement-3/input.svelte new file mode 100644 index 0000000000..2519482a98 --- /dev/null +++ b/test/validator/samples/const-tag-placement-3/input.svelte @@ -0,0 +1,9 @@ + + +{#each a as i} +
+ {@const b = i} +
+{/each} \ No newline at end of file diff --git a/test/validator/samples/const-tag-readonly-1/errors.json b/test/validator/samples/const-tag-readonly-1/errors.json new file mode 100644 index 0000000000..ed60c529fd --- /dev/null +++ b/test/validator/samples/const-tag-readonly-1/errors.json @@ -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 + } +] diff --git a/test/validator/samples/const-tag-readonly-1/input.svelte b/test/validator/samples/const-tag-readonly-1/input.svelte new file mode 100644 index 0000000000..b5a37c8608 --- /dev/null +++ b/test/validator/samples/const-tag-readonly-1/input.svelte @@ -0,0 +1,8 @@ + + +{#each array as a} + {@const b = a + 1} +