diff --git a/.github/stale.yml b/.github/stale.yml
deleted file mode 100644
index c95bc4cf22..0000000000
--- a/.github/stale.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-# Configuration for probot-stale - https://github.com/probot/stale
-
-# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
-onlyLabels: []
-
-# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
-exemptLabels:
- - popular
- - pinned
- - security
- - bug
- - "[Status] Maybe Later"
-
-# Set to true to ignore issues in a project (defaults to false)
-exemptProjects: false
-
-# Set to true to ignore issues in a milestone (defaults to false)
-exemptMilestones: false
-
-# Set to true to ignore issues with an assignee (defaults to false)
-exemptAssignees: false
-
-# Label to use when marking as stale
-staleLabel: stale-bot
-
-# Comment to post when removing the stale label.
-# unmarkComment: >
-# Your comment here.
-
-# Limit the number of actions per hour, from 1-30. Default is 30
-limitPerRun: 1
-
-# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
-pulls:
- daysUntilStale: 210
- daysUntilClose: 25
- markComment: >
- This pull request has been automatically marked as stale because it has not had
- recent activity. It will be closed if no further activity occurs. Thank you
- for your contributions.
- closeComment: >
- This pull request has been closed as it was previously marked as stale and saw no subsequent activity.
-
-issues:
- daysUntilStale: 180
- daysUntilClose: 14
- markComment: >
- This issue has been automatically marked as stale because it has not had
- recent activity. It will be closed if no further activity occurs. Thank you
- for your contributions.
- closeComment: >
- This issue has been closed as it was previously marked as stale and saw no subsequent activity.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1ffb41e35b..4df49ecc77 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,35 @@
# 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))
+* Fix binding to a member expression also invalidating the member property ([#6921](https://github.com/sveltejs/svelte/issues/6921))
+* Fix default values in `{#each}`/etc. destructurings not being considered references for the purposes of compiler warnings ([#6964](https://github.com/sveltejs/svelte/issues/6964))
+* Fix `{:else if}` value incorrectly being cached ([#7043](https://github.com/sveltejs/svelte/pull/7043))
+* Add `a11y-no-redundant-roles` warning ([#7067](https://github.com/sveltejs/svelte/pull/7067))
+* Fix code generation error with arrow functions whose bodies are object destructuring assignments ([#7087](https://github.com/sveltejs/svelte/issues/7087))
+
## 3.44.3
* Fix `bind:this` binding inside `onMount` for manually instantiated component ([#6760](https://github.com/sveltejs/svelte/issues/6760))
diff --git a/LICENSE.md b/LICENSE.md
index cbf510847f..cd8f94f4ca 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,4 +1,4 @@
-Copyright (c) 2016-21 [these people](https://github.com/sveltejs/svelte/graphs/contributors)
+Copyright (c) 2016-22 [these people](https://github.com/sveltejs/svelte/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
diff --git a/package-lock.json b/package-lock.json
index 0aeed9166d..8bddba41a1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "svelte",
- "version": "3.44.3",
+ "version": "3.46.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "svelte",
- "version": "3.44.3",
+ "version": "3.46.2",
"license": "MIT",
"devDependencies": {
"@ampproject/remapping": "^0.3.0",
@@ -25,7 +25,7 @@
"acorn": "^8.4.1",
"agadoo": "^1.1.0",
"c8": "^5.0.1",
- "code-red": "^0.2.3",
+ "code-red": "^0.2.4",
"codecov": "^3.5.0",
"css-tree": "^1.1.2",
"eslint": "^7.32.0",
@@ -1082,9 +1082,9 @@
}
},
"node_modules/code-red": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/code-red/-/code-red-0.2.3.tgz",
- "integrity": "sha512-l9MRiYO9iNx3dCpoZBZkaHAVtbhig8TBddEHq7ssWcZRAjaYR8NoRFzZ56VJ20TIg7hEQegCVCH3fVus+2Ol4Q==",
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/code-red/-/code-red-0.2.4.tgz",
+ "integrity": "sha512-tAJQiZviSyB2KUhz+rocKFzCHPkVooX2aFrdpfWDRvxWJaBQTYFJ/Z2TcWqbjXj5oJJBlqd2GxBXdtAhOXySVQ==",
"dev": true,
"dependencies": {
"@types/estree": "^0.0.50",
@@ -6376,9 +6376,9 @@
}
},
"code-red": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/code-red/-/code-red-0.2.3.tgz",
- "integrity": "sha512-l9MRiYO9iNx3dCpoZBZkaHAVtbhig8TBddEHq7ssWcZRAjaYR8NoRFzZ56VJ20TIg7hEQegCVCH3fVus+2Ol4Q==",
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/code-red/-/code-red-0.2.4.tgz",
+ "integrity": "sha512-tAJQiZviSyB2KUhz+rocKFzCHPkVooX2aFrdpfWDRvxWJaBQTYFJ/Z2TcWqbjXj5oJJBlqd2GxBXdtAhOXySVQ==",
"dev": true,
"requires": {
"@types/estree": "^0.0.50",
diff --git a/package.json b/package.json
index ee6c781860..dfba5a0a3f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "svelte",
- "version": "3.44.3",
+ "version": "3.46.2",
"description": "Cybernetically enhanced web apps",
"module": "index.mjs",
"main": "index",
@@ -83,8 +83,8 @@
},
"types": "types/runtime/index.d.ts",
"scripts": {
- "test": "mocha",
- "test:unit": "mocha --require sucrase/register --recursive src/**/__test__.ts",
+ "test": "mocha --exit",
+ "test:unit": "mocha --require sucrase/register --recursive src/**/__test__.ts --exit",
"quicktest": "mocha",
"precoverage": "c8 mocha",
"coverage": "c8 report --reporter=text-lcov > coverage.lcov && c8 report --reporter=html",
@@ -132,7 +132,7 @@
"acorn": "^8.4.1",
"agadoo": "^1.1.0",
"c8": "^5.0.1",
- "code-red": "^0.2.3",
+ "code-red": "^0.2.4",
"codecov": "^3.5.0",
"css-tree": "^1.1.2",
"eslint": "^7.32.0",
diff --git a/site/content/blog/2019-04-22-svelte-3-rethinking-reactivity.md b/site/content/blog/2019-04-22-svelte-3-rethinking-reactivity.md
index c372d2a85b..672fe1b777 100644
--- a/site/content/blog/2019-04-22-svelte-3-rethinking-reactivity.md
+++ b/site/content/blog/2019-04-22-svelte-3-rethinking-reactivity.md
@@ -94,4 +94,4 @@ We don't take this lightly: hopefully once you've experienced Svelte 3 you'll un
As grueling as this release has been, we're nowhere near finished. We have a ton of ideas for generating smarter, more compact code, and a long feature wish-list. [Sapper](https://sapper.svelte.dev), our Next.js-style app framework, is still in the middle of being updated to use Svelte 3. The [Svelte Native](https://svelte-native.technology/) community project, which allows you to write Android and iOS apps in Svelte, is making solid progress but deserves more complete support from core. We don't yet have the bounty of editor extensions, syntax highlighters, component kits, devtools and so on that other frameworks have, and we should fix that. We *really* want to add first-class TypeScript support.
-But in the meantime we think Svelte 3 is the best way to build web apps yet. Take an hour to go through the [tutorial](/tutorial) and we hope to convince you of the same. Either way, we'd love to see you in our [Discord chatroom](/chat) and on [GitHub](https://github.com/sveltejs/svelte) — everyone is welcome, especially you.
+But in the meantime we think Svelte 3 is the best way to build web apps yet. Take an hour to go through the [tutorial](/tutorial) and we hope to convince you of the same. Either way, we'd love to see you in our [Discord chatroom](https://svelte.dev/chat) and on [GitHub](https://github.com/sveltejs/svelte) — everyone is welcome, especially you.
diff --git a/site/content/blog/2022-01-01-whats-new-in-svelte-january-2022.md b/site/content/blog/2022-01-01-whats-new-in-svelte-january-2022.md
new file mode 100644
index 0000000000..bf7bc3df57
--- /dev/null
+++ b/site/content/blog/2022-01-01-whats-new-in-svelte-january-2022.md
@@ -0,0 +1,77 @@
+---
+title: "What's new in Svelte: January 2022"
+description: "Faster builds with SvelteKit and a much anticipated REPL feature"
+author: Daniel Sandoval
+authorURL: https://desandoval.net
+---
+
+Happy new year, Svelte Community! Lots to share this month across Svelte, SvelteKit, Language Tools and the Showcase. Thanks to everyone who made 2021 a great year to use Svelte. Looking forward to the next one 🚀
+
+## What's new in SvelteKit
+- `@sveltejs/adapter-static` for SvelteKit now has a `precompress` option to make brotli compression of assets and pages easier to do out of the box ([#3079](https://github.com/sveltejs/kit/pull/3079))
+- Concurrency mode in SvelteKit will now prerender pages in parallel ([#3120](https://github.com/sveltejs/kit/pull/3120)). It is enabled by default in `1.0.0-next.205` and later
+- CSS is now automatically included before JS for improved page performance ([d13efe](https://github.com/sveltejs/kit/commit/d138efe21692f5925f1e89afc0a33f42d6a1a711))
+- A new config option adds the ability to disable service worker registration to do your own custom registration ([#2988](https://github.com/sveltejs/kit/pull/2988))
+- SSR route-splitting is here - breaking monolithic builds into smaller pieces for improved startup and routing performance ([#2931](https://github.com/sveltejs/kit/pull/2931))
+- `request.origin/path/query` is now `request.url` - simplifying the config and page `load` functions ([#3126](https://github.com/sveltejs/kit/pull/3126))
+- After the [update to Vite 2.7](https://github.com/sveltejs/kit/pull/3018), SvelteKit users are [reporting significant performance improvements](https://www.reddit.com/r/sveltejs/comments/rljhfc/sveltekit_massive_compiler_improvement_by/) and loading third-parties libraries in SSR has also been greatly improved
+- SvelteKit server will now automatically restart when the config files is changed ([vite-plugin-svelte#237](https://github.com/sveltejs/vite-plugin-svelte/pull/237))
+
+
+## Other new bits from `svelte/*`
+- [Svelte 3.44.3](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md#3443) is out with a few bug fixes in the binding and loop code
+- Svelte Language Tools has introduced support for the then/catch shorthands from Svelte 3.41 and TypeScript's "go to" functionality ([105.8.0 and later](https://github.com/sveltejs/language-tools/releases/tag/extensions-105.8.0))
+- The Svelte REPL got a nice upgrade as well - letting you delete saved REPLs. Try it out by logging in at [svelte.dev/apps](https://svelte.dev/apps)
+
+
+---
+
+## Community Showcase
+
+**Apps & Sites**
+- [Discover Twitter Spaces](https://github.com/navneetsharmaui/discover-twitter-spaces) is a tool that helps you find the Twitter Spaces
+- [Modern Fluid Typography Editor](https://github.com/codeAdrian/modern-fluid-typography-editor) helps create beautiful fluid typography using CSS clamp
+- [Unnwhiteboard](https://github.com/AviKKi/unnwhiteboard) is a job board for companies (or teams) that don't do "whiteboard" interviews
+- [Secret Santa](https://gitlab.com/arturoguzman/secret-santa-sveltekit) is a gift giving coordination app developed with easiness in mind
+- [LogSnag](https://logsnag.com/) notifies you of your projects' events and provides you with a timeline to keep track of anything important that happens
+- [Version 0.2 of Tangent](http://tangentnotes.com/Download), a Svelte-based note writing app, is now in beta
+- [Intl Explorer](https://github.com/jesperorb/intl-explorer) is a tool for viewing output for all possible formatters for Intl
+
+A lot of work this month has gone into migrating the Svelte main website and Svelte REPL to live in the https://github.com/sveltejs/sites repository - including a brand new homepage for [svelte.dev](https://svelte.dev/). Thanks to all the contributors who made this possible!
+
+If you're looking for a fun SvelteKit project to work on, [you can contribute to the Svelte Society site rewrite](https://github.com/svelte-society/sveltesociety-2021/issues) 💅
+
+
+**Learning and Listening**
+
+_To Read_
+- [Mutating Query Params in SvelteKit Without Page Reloads or Navigations](https://dev.to/mohamadharith/mutating-query-params-in-sveltekit-without-page-reloads-or-navigations-2i2b) by Mohamad Harith
+- [Svelte for Reactaholics : A guide for React developers](https://www.100ms.live/blog/svelte-guide-for-react-developers) by Puru Vijay
+- [Svelte's lifecycle methods can be used anywhere](https://geoffrich.net/posts/svelte-lifecycle-examples/) and [The many meanings of $ in Svelte](https://geoffrich.net/posts/svelte-$-meanings/) by Geoff Rich
+- [Vercel and Svelte: A Perfect Match for Web Developers](https://thenewstack.io/vercel-and-svelte-a-perfect-match-for-web-developers/) by Darryl K. Taft
+- [User-defined TailwindCSS Color Scheme with Svelte Stores](https://blog.dayslice.io/user-defined-tailwindcss-color-scheme-with-svelte-stores-ad80ca2cf038) by jeremy zaborowski
+- [Ionic 6 + Svelte 🚀](https://medium.com/@raymondboswel/ionic-6-svelte-ae904caa82df) by Raymond Boswel
+- [What happened in #Svelte language tools this year](https://twitter.com/dummdidumm_/status/1474158105395179525?t=ytj2K2Q52iD5-lNyLnQaAQ&s=19) by Simon H
+
+_To Watch_
+- [The Future of Svelte (Interview with Rich Harris)](https://www.youtube.com/watch?v=uQntFkK8Z54) by Lee Robinson, Director of Developer Relations at Vercel
+- [Svelte is becoming the go-to framework](https://www.youtube.com/watch?v=fo6BKY2xR2w&t=1834s) for Obsidian plugin developers
+- [Sveltekit WordPress Headless Blog](https://www.youtube.com/watch?v=c0UDVgjPxFw) by WebJeda
+- [Getting started with SvelteKit](https://www.youtube.com/watch?v=i2suPKMPUFA) by Lihau Tan
+- [Deploy a full-stack SvelteKit app on Cloudflare Pages](https://www.youtube.com/watch?v=Wc1_U6Dy5Tw) by 1nf
+
+_To Listen To_
+- [Syntax podcast: How To Do Things In Svelte](https://podcasts.apple.com/ca/podcast/how-to-do-things-in-svelte/id1253186678?i=1000544796072)
+- [JS Party #205: So much Sveltey goodness (w/ Rich Harris)](https://changelog.com/jsparty/205)
+
+**Libraries, Tools & Components**
+- [svelte-headlessui](https://github.com/rgossiaux/svelte-headlessui) is an unofficial, complete Svelte port of the Headless UI component library
+- [svelte-forms v2](https://chainlist.github.io/svelte-forms/) has been released - the author is [looking for feedback](https://www.reddit.com/r/sveltejs/comments/r6354j/svelteforms_v2_has_been_released/)
+- [Percival](https://github.com/ekzhang/percival) is a declarative data query and visualization language
+- [Svelte FlatList](https://github.com/snuffyDev/svelte-flatlist) is a mobile-friendly, simple, and customizable draggable menu
+- [svelte-keyed](https://github.com/bryanmylee/svelte-keyed) is a writable derived store for objects and arrays
+- [Svemix](https://github.com/svemix/svemix) is Remix for Svelte - providing server scripts inside your Svelte components/routes, which will be transformed into endpoints
+
+Want to add something to the showcase? Need help bringing your next idea to life in Svelte? Join us on [Reddit](https://www.reddit.com/r/sveltejs/) or [Discord](https://discord.com/invite/yy75DKs).
+
+See ya next month!
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..7860c2acbc
--- /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](https://svelte.dev/blog/rss.xml) or check monthly to be the first to get them.
diff --git a/site/content/docs/00-introduction.md b/site/content/docs/00-introduction.md
index 2fece5534f..11f82117e9 100644
--- a/site/content/docs/00-introduction.md
+++ b/site/content/docs/00-introduction.md
@@ -6,6 +6,6 @@ This page contains detailed API reference documentation. It's intended to be a r
If that's not you (yet), you may prefer to visit the [interactive tutorial](/tutorial) or the [examples](/examples) before consulting this reference.
-Don't be shy about asking for help in the [Discord chatroom](/chat).
+Don't be shy about asking for help in the [Discord chatroom](https://svelte.dev/chat).
Using an older version of Svelte? Have a look at the [v2 docs](https://v2.svelte.dev).
diff --git a/site/content/docs/02-template-syntax.md b/site/content/docs/02-template-syntax.md
index aeabeb7a51..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
@@ -821,6 +844,46 @@ A `class:` directive provides a shorter way of toggling a class on an element.
...
```
+#### style:*property*
+
+```sv
+style:property={value}
+```
+```sv
+style:property="value"
+```
+```sv
+style:property
+```
+
+---
+
+The `style:` directive provides a shorthand for setting multiple styles on an element.
+
+```sv
+
+...
+...
+
+
+...
+
+
+...
+
+
+...
+```
+
+---
+
+When `style:` directives are combined with `style` attributes, the directives will take precedence:
+
+```sv
+This will be red
+```
+
+
#### use:*action*
@@ -1608,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/examples/05-bindings/09-media-elements/App.svelte b/site/content/examples/05-bindings/09-media-elements/App.svelte
index b4c5907f6d..de89b1be8d 100644
--- a/site/content/examples/05-bindings/09-media-elements/App.svelte
+++ b/site/content/examples/05-bindings/09-media-elements/App.svelte
@@ -50,7 +50,7 @@
Caminandes: Llamigos
-From Blender Open Projects . CC-BY
+From Blender Studio . CC-BY
Caminandes: Llamigos
-From Blender Open Projects . CC-BY
+From Blender Studio . CC-BY
Caminandes: Llamigos
-From Blender Open Projects . CC-BY
+From Blender Studio . CC-BY
({
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/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts
index bfcd779fc7..86ec98cb8b 100644
--- a/src/compiler/compile/compiler_warnings.ts
+++ b/src/compiler/compile/compiler_warnings.ts
@@ -76,6 +76,10 @@ export default {
code: 'a11y-unknown-role',
message: `A11y: Unknown role '${role}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '')
}),
+ a11y_no_redundant_roles: (role: string | boolean) => ({
+ code: 'a11y-no-redundant-roles',
+ message: `A11y: Redundant role '${role}'`
+ }),
a11y_accesskey: {
code: 'a11y-accesskey',
message: 'A11y: Avoid using accesskey'
diff --git a/src/compiler/compile/nodes/AwaitBlock.ts b/src/compiler/compile/nodes/AwaitBlock.ts
index ef74d26848..735fdbfff3 100644
--- a/src/compiler/compile/nodes/AwaitBlock.ts
+++ b/src/compiler/compile/nodes/AwaitBlock.ts
@@ -33,12 +33,12 @@ export default class AwaitBlock extends Node {
if (this.then_node) {
this.then_contexts = [];
- unpack_destructuring(this.then_contexts, info.value);
+ unpack_destructuring({ contexts: this.then_contexts, node: info.value, scope, component });
}
if (this.catch_node) {
this.catch_contexts = [];
- unpack_destructuring(this.catch_contexts, info.error);
+ unpack_destructuring({ contexts: this.catch_contexts, node: info.error, scope, component });
}
this.pending = new PendingBlock(component, this, scope, info.pending);
diff --git a/src/compiler/compile/nodes/Binding.ts b/src/compiler/compile/nodes/Binding.ts
index 1efc1a3038..39f6fa374e 100644
--- a/src/compiler/compile/nodes/Binding.ts
+++ b/src/compiler/compile/nodes/Binding.ts
@@ -57,6 +57,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 c03128c388..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;
@@ -39,7 +42,7 @@ export default class EachBlock extends AbstractBlock {
this.scope = scope.child();
this.contexts = [];
- unpack_destructuring(this.contexts, info.context);
+ unpack_destructuring({ contexts: this.contexts, node: info.context, scope, component });
this.contexts.forEach(context => {
this.scope.add(context.key.name, this.expression.dependencies, this);
@@ -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 837a0296d0..92a87dd6f8 100644
--- a/src/compiler/compile/nodes/Element.ts
+++ b/src/compiler/compile/nodes/Element.ts
@@ -7,6 +7,7 @@ import Transition from './Transition';
import Animation from './Animation';
import Action from './Action';
import Class from './Class';
+import StyleDirective from './StyleDirective';
import Text from './Text';
import { namespaces } from '../../utils/namespaces';
import map_children from './shared/map_children';
@@ -70,6 +71,46 @@ const a11y_labelable = new Set([
'textarea'
]);
+const a11y_nested_implicit_semantics = new Map([
+ ['header', 'banner'],
+ ['footer', 'contentinfo']
+]);
+
+const a11y_implicit_semantics = new Map([
+ ['a', 'link'],
+ ['aside', 'complementary'],
+ ['body', 'document'],
+ ['datalist', 'listbox'],
+ ['dd', 'definition'],
+ ['dfn', 'term'],
+ ['details', 'group'],
+ ['dt', 'term'],
+ ['fieldset', 'group'],
+ ['form', 'form'],
+ ['h1', 'heading'],
+ ['h2', 'heading'],
+ ['h3', 'heading'],
+ ['h4', 'heading'],
+ ['h5', 'heading'],
+ ['h6', 'heading'],
+ ['hr', 'separator'],
+ ['li', 'listitem'],
+ ['menu', 'list'],
+ ['nav', 'navigation'],
+ ['ol', 'list'],
+ ['optgroup', 'group'],
+ ['output', 'status'],
+ ['progress', 'progressbar'],
+ ['section', 'region'],
+ ['summary', 'button'],
+ ['tbody', 'rowgroup'],
+ ['textarea', 'textbox'],
+ ['tfoot', 'rowgroup'],
+ ['thead', 'rowgroup'],
+ ['tr', 'row'],
+ ['ul', 'list']
+]);
+
const invisible_elements = new Set(['meta', 'html', 'script', 'style']);
const valid_modifiers = new Set([
@@ -98,6 +139,23 @@ const react_attributes = new Map([
const attributes_to_compact_whitespace = ['class', 'style'];
+function is_parent(parent: INode, elements: string[]) {
+ let check = false;
+
+ while (parent) {
+ const parent_name = (parent as Element).name;
+ if (elements.includes(parent_name)) {
+ check = true;
+ break;
+ }
+ if (parent.type === 'Element') {
+ break;
+ }
+ parent = parent.parent;
+ }
+ return check;
+}
+
function get_namespace(parent: Element, element: Element, explicit_namespace: string) {
const parent_element = parent.find_nearest(/^Element/);
@@ -123,6 +181,7 @@ export default class Element extends Node {
actions: Action[] = [];
bindings: Binding[] = [];
classes: Class[] = [];
+ styles: StyleDirective[] = [];
handlers: EventHandler[] = [];
lets: Let[] = [];
intro?: Transition = null;
@@ -206,6 +265,10 @@ export default class Element extends Node {
this.classes.push(new Class(component, this, scope, node));
break;
+ case 'StyleDirective':
+ this.styles.push(new StyleDirective(component, this, scope, node));
+ break;
+
case 'EventHandler':
this.handlers.push(new EventHandler(component, this, scope, node));
break;
@@ -351,6 +414,22 @@ export default class Element extends Node {
const match = fuzzymatch(value, aria_roles);
component.warn(attribute, compiler_warnings.a11y_unknown_role(value, match));
}
+
+ // no-redundant-roles
+ const has_redundant_role = value === a11y_implicit_semantics.get(this.name);
+
+ if (this.name === value || has_redundant_role) {
+ component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(value));
+ }
+
+ // Footers and headers are special cases, and should not have redundant roles unless they are the children of sections or articles.
+ const is_parent_section_or_article = is_parent(this.parent, ['section', 'article']);
+ if (!is_parent_section_or_article) {
+ const has_nested_redundant_role = value === a11y_nested_implicit_semantics.get(this.name);
+ if (has_nested_redundant_role) {
+ component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(value));
+ }
+ }
}
// no-access-key
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/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 a98c21511f..2c636b36f3 100644
--- a/src/compiler/compile/nodes/interfaces.ts
+++ b/src/compiler/compile/nodes/interfaces.ts
@@ -8,7 +8,9 @@ import Binding from './Binding';
import Body from './Body';
import CatchBlock from './CatchBlock';
import Class from './Class';
+import StyleDirective from './StyleDirective';
import Comment from './Comment';
+import ConstTag from './ConstTag';
import DebugTag from './DebugTag';
import EachBlock from './EachBlock';
import Element from './Element';
@@ -26,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';
@@ -44,6 +45,7 @@ export type INode = Action
| CatchBlock
| Class
| Comment
+| ConstTag
| DebugTag
| EachBlock
| Element
@@ -61,10 +63,17 @@ export type INode = Action
| RawMustacheTag
| Slot
| SlotTemplate
-| DefaultSlotTemplate
+| StyleDirective
| Tag
| Text
| ThenBlock
| Title
| Transition
| Window;
+
+export type INodeAllowConstTag =
+| EachBlock
+| CatchBlock
+| ThenBlock
+| InlineComponent
+| SlotTemplate;
diff --git a/src/compiler/compile/nodes/shared/Context.ts b/src/compiler/compile/nodes/shared/Context.ts
index 6cc5a4358e..7507d1e37e 100644
--- a/src/compiler/compile/nodes/shared/Context.ts
+++ b/src/compiler/compile/nodes/shared/Context.ts
@@ -3,6 +3,9 @@ import { Node, Identifier, Expression } from 'estree';
import { walk } from 'estree-walker';
import is_reference, { NodeWithPropertyDefinition } from 'is-reference';
import { clone } from '../../../utils/clone';
+import Component from '../../Component';
+import flatten_reference from '../../utils/flatten_reference';
+import TemplateScope from './TemplateScope';
export interface Context {
key: Identifier;
@@ -11,7 +14,21 @@ export interface Context {
default_modifier: (node: Node, to_ctx: (name: string) => Node) => Node;
}
-export function unpack_destructuring(contexts: Context[], node: Node, modifier: Context['modifier'] = node => node, default_modifier: Context['default_modifier'] = node => node) {
+export function unpack_destructuring({
+ contexts,
+ node,
+ modifier = (node) => node,
+ default_modifier = (node) => node,
+ scope,
+ component
+}: {
+ contexts: Context[];
+ node: Node;
+ modifier?: Context['modifier'];
+ default_modifier?: Context['default_modifier'];
+ scope: TemplateScope;
+ component: Component;
+}) {
if (!node) return;
if (node.type === 'Identifier') {
@@ -29,13 +46,41 @@ export function unpack_destructuring(contexts: Context[], node: Node, modifier:
} else if (node.type === 'ArrayPattern') {
node.elements.forEach((element, i) => {
if (element && element.type === 'RestElement') {
- unpack_destructuring(contexts, element, node => x`${modifier(node)}.slice(${i})` as Node, default_modifier);
+ unpack_destructuring({
+ contexts,
+ node: element,
+ modifier: (node) => x`${modifier(node)}.slice(${i})` as Node,
+ default_modifier,
+ scope,
+ component
+ });
} else if (element && element.type === 'AssignmentPattern') {
const n = contexts.length;
+ mark_referenced(element.right, scope, component);
- unpack_destructuring(contexts, element.left, node => x`${modifier(node)}[${i}]`, (node, to_ctx) => x`${node} !== undefined ? ${node} : ${update_reference(contexts, n, element.right, to_ctx)}` as Node);
+ unpack_destructuring({
+ contexts,
+ node: element.left,
+ modifier: (node) => x`${modifier(node)}[${i}]`,
+ default_modifier: (node, to_ctx) =>
+ x`${node} !== undefined ? ${node} : ${update_reference(
+ contexts,
+ n,
+ element.right,
+ to_ctx
+ )}` as Node,
+ scope,
+ component
+ });
} else {
- unpack_destructuring(contexts, element, node => x`${modifier(node)}[${i}]` as Node, default_modifier);
+ unpack_destructuring({
+ contexts,
+ node: element,
+ modifier: (node) => x`${modifier(node)}[${i}]` as Node,
+ default_modifier,
+ scope,
+ component
+ });
}
});
} else if (node.type === 'ObjectPattern') {
@@ -43,12 +88,17 @@ export function unpack_destructuring(contexts: Context[], node: Node, modifier:
node.properties.forEach((property) => {
if (property.type === 'RestElement') {
- unpack_destructuring(
+ unpack_destructuring({
contexts,
- property.argument,
- node => x`@object_without_properties(${modifier(node)}, [${used_properties}])` as Node,
- default_modifier
- );
+ node: property.argument,
+ modifier: (node) =>
+ x`@object_without_properties(${modifier(
+ node
+ )}, [${used_properties}])` as Node,
+ default_modifier,
+ scope,
+ component
+ });
} else {
const key = property.key as Identifier;
const value = property.value;
@@ -57,16 +107,43 @@ export function unpack_destructuring(contexts: Context[], node: Node, modifier:
if (value.type === 'AssignmentPattern') {
const n = contexts.length;
- unpack_destructuring(contexts, value.left, node => x`${modifier(node)}.${key.name}`, (node, to_ctx) => x`${node} !== undefined ? ${node} : ${update_reference(contexts, n, value.right, to_ctx)}` as Node);
+ mark_referenced(value.right, scope, component);
+
+ unpack_destructuring({
+ contexts,
+ node: value.left,
+ modifier: (node) => x`${modifier(node)}.${key.name}`,
+ default_modifier: (node, to_ctx) =>
+ x`${node} !== undefined ? ${node} : ${update_reference(
+ contexts,
+ n,
+ value.right,
+ to_ctx
+ )}` as Node,
+ scope,
+ component
+ });
} else {
- unpack_destructuring(contexts, value, node => x`${modifier(node)}.${key.name}` as Node, default_modifier);
+ unpack_destructuring({
+ contexts,
+ node: value,
+ modifier: (node) => x`${modifier(node)}.${key.name}` as Node,
+ default_modifier,
+ scope,
+ component
+ });
}
}
});
}
}
-function update_reference(contexts: Context[], n: number, expression: Expression, to_ctx: (name: string) => Node): Node {
+function update_reference(
+ contexts: Context[],
+ n: number,
+ expression: Expression,
+ to_ctx: (name: string) => Node
+): Node {
const find_from_context = (node: Identifier) => {
for (let i = n; i < contexts.length; i++) {
const { key } = contexts[i];
@@ -85,7 +162,12 @@ function update_reference(contexts: Context[], n: number, expression: Expression
expression = clone(expression) as Expression;
walk(expression, {
enter(node, parent: Node) {
- if (is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition)) {
+ if (
+ is_reference(
+ node as NodeWithPropertyDefinition,
+ parent as NodeWithPropertyDefinition
+ )
+ ) {
this.replace(find_from_context(node as Identifier));
this.skip();
}
@@ -94,3 +176,20 @@ function update_reference(contexts: Context[], n: number, expression: Expression
return expression;
}
+
+function mark_referenced(
+ node: Node,
+ scope: TemplateScope,
+ component: Component
+) {
+ walk(node, {
+ enter(node: any, parent: any) {
+ if (is_reference(node, parent)) {
+ const { name } = flatten_reference(node);
+ if (!scope.is_let(name) && !scope.names.has(name)) {
+ component.add_reference(name);
+ }
+ }
+ }
+ });
+}
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/Binding.ts b/src/compiler/compile/render_dom/wrappers/Element/Binding.ts
index 7c8a339d00..bc218a510b 100644
--- a/src/compiler/compile/render_dom/wrappers/Element/Binding.ts
+++ b/src/compiler/compile/render_dom/wrappers/Element/Binding.ts
@@ -70,6 +70,30 @@ export default class BindingWrapper {
return dependencies;
}
+ get_update_dependencies() {
+ const object = this.object;
+ const dependencies = new Set();
+ if (this.node.expression.template_scope.names.has(object)) {
+ this.node.expression.template_scope.dependencies_for_name
+ .get(object)
+ .forEach((name) => dependencies.add(name));
+ } else {
+ dependencies.add(object);
+ }
+
+ const result = new Set(dependencies);
+ dependencies.forEach((dependency) => {
+ const indirect_dependencies = this.parent.renderer.component.indirect_dependencies.get(dependency);
+ if (indirect_dependencies) {
+ indirect_dependencies.forEach(indirect_dependency => {
+ result.add(indirect_dependency);
+ });
+ }
+ });
+
+ return result;
+ }
+
is_readonly_media_attribute() {
return this.node.is_readonly_media_attribute();
}
diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts
index 9ec36b12d7..80b4c7ca7a 100644
--- a/src/compiler/compile/render_dom/wrappers/Element/index.ts
+++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts
@@ -199,7 +199,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);
}
@@ -340,6 +340,7 @@ export default class ElementWrapper extends Wrapper {
this.add_transitions(block);
this.add_animation(block);
this.add_classes(block);
+ this.add_styles(block);
this.add_manual_style_scoping(block);
if (nodes && this.renderer.options.hydratable && !this.void) {
@@ -466,7 +467,7 @@ export default class ElementWrapper extends Wrapper {
binding_group.bindings.forEach(binding => {
// TODO this is a mess
- add_to_set(dependencies, binding.get_dependencies());
+ add_to_set(dependencies, binding.get_update_dependencies());
add_to_set(contextual_dependencies, binding.handler.contextual_dependencies);
binding.render(block, lock);
@@ -914,6 +915,43 @@ export default class ElementWrapper extends Wrapper {
});
}
+ add_styles(block: Block) {
+ const has_spread = this.node.attributes.some(attr => attr.is_spread);
+ this.node.styles.forEach((style_directive) => {
+ const { name, expression, should_cache } = style_directive;
+
+ const snippet = expression.manipulate(block);
+ let cached_snippet;
+ if (should_cache) {
+ cached_snippet = block.get_unique_name(`style_${name.replace(/-/g, '_')}`);
+ block.add_variable(cached_snippet, snippet);
+ }
+
+ const updater = b`@set_style(${this.var}, "${name}", ${should_cache ? cached_snippet : snippet}, false)`;
+
+ block.chunks.hydrate.push(updater);
+
+ const dependencies = expression.dynamic_dependencies();
+ if (has_spread) {
+ block.chunks.update.push(updater);
+ } else if (dependencies.length > 0) {
+ if (should_cache) {
+ block.chunks.update.push(b`
+ if (${block.renderer.dirty(dependencies)} && (${cached_snippet} !== (${cached_snippet} = ${snippet}))) {
+ ${updater}
+ }
+ `);
+ } else {
+ block.chunks.update.push(b`
+ if (${block.renderer.dirty(dependencies)}) {
+ ${updater}
+ }
+ `);
+ }
+ }
+ });
+ }
+
add_manual_style_scoping(block) {
if (this.node.needs_manual_style_scoping) {
const updater = b`@toggle_class(${this.var}, "${this.node.component.stylesheet.id}", true);`;
diff --git a/src/compiler/compile/render_dom/wrappers/IfBlock.ts b/src/compiler/compile/render_dom/wrappers/IfBlock.ts
index b3bbf9becd..01d49ac9dd 100644
--- a/src/compiler/compile/render_dom/wrappers/IfBlock.ts
+++ b/src/compiler/compile/render_dom/wrappers/IfBlock.ts
@@ -266,15 +266,15 @@ export default class IfBlockWrapper extends Wrapper {
if (this.needs_update) {
block.chunks.init.push(b`
function ${select_block_type}(#ctx, #dirty) {
- ${this.branches.map(({ dependencies, condition, snippet, block }) => condition
+ ${this.branches.map(({ dependencies, condition, snippet }) => {
+ return b`${snippet && dependencies.length > 0 ? b`if (${block.renderer.dirty(dependencies)}) ${condition} = null;` : null}`;
+ })}
+ ${this.branches.map(({ condition, snippet, block }) => condition
? b`
- ${snippet && (
- dependencies.length > 0
- ? b`if (${condition} == null || ${block.renderer.dirty(dependencies)}) ${condition} = !!${snippet}`
- : b`if (${condition} == null) ${condition} = !!${snippet}`
+ ${snippet && b`if (${condition} == null) ${condition} = !!${snippet}`}
+ if (${condition}) return ${block.name};`
+ : b`return ${block.name};`
)}
- if (${condition}) return ${block.name};`
- : b`return ${block.name};`)}
}
`);
} else {
@@ -387,13 +387,12 @@ export default class IfBlockWrapper extends Wrapper {
${this.needs_update
? b`
function ${select_block_type}(#ctx, #dirty) {
- ${this.branches.map(({ dependencies, condition, snippet }, i) => condition
+ ${this.branches.map(({ dependencies, condition, snippet }) => {
+ return b`${snippet && dependencies.length > 0 ? b`if (${block.renderer.dirty(dependencies)}) ${condition} = null;` : null}`;
+ })}
+ ${this.branches.map(({ condition, snippet }, i) => condition
? b`
- ${snippet && (
- dependencies.length > 0
- ? b`if (${condition} == null || ${block.renderer.dirty(dependencies)}) ${condition} = !!${snippet}`
- : b`if (${condition} == null) ${condition} = !!${snippet}`
- )}
+ ${snippet && b`if (${condition} == null) ${condition} = !!${snippet}`}
if (${condition}) return ${i};`
: b`return ${i};`)}
${!has_else && b`return -1;`}
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_dom/wrappers/shared/bind_this.ts b/src/compiler/compile/render_dom/wrappers/shared/bind_this.ts
index 1b8c4f9db8..af440b6341 100644
--- a/src/compiler/compile/render_dom/wrappers/shared/bind_this.ts
+++ b/src/compiler/compile/render_dom/wrappers/shared/bind_this.ts
@@ -12,7 +12,7 @@ export default function bind_this(component: Component, block: Block, binding: B
const callee = block.renderer.reference(fn.name);
const { contextual_dependencies, mutation } = binding.handler;
- const dependencies = binding.get_dependencies();
+ const dependencies = binding.get_update_dependencies();
const body = b`
${mutation}
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/Element.ts b/src/compiler/compile/render_ssr/handlers/Element.ts
index c0bf826a8e..dd096ae79b 100644
--- a/src/compiler/compile/render_ssr/handlers/Element.ts
+++ b/src/compiler/compile/render_ssr/handlers/Element.ts
@@ -3,7 +3,7 @@ import { get_attribute_expression, get_attribute_value, get_class_attribute_valu
import { boolean_attributes } from './shared/boolean_attributes';
import Renderer, { RenderOptions } from '../Renderer';
import Element from '../../nodes/Element';
-import { x } from 'code-red';
+import { p, x } from 'code-red';
import Expression from '../../nodes/shared/Expression';
import remove_whitespace_children from './utils/remove_whitespace_children';
import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing';
@@ -36,6 +36,15 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
class_expression_list.length > 0 &&
class_expression_list.reduce((lhs, rhs) => x`${lhs} + ' ' + ${rhs}`);
+ const style_expression_list = node.styles.map(style_directive => {
+ const { name, expression: { node: expression } } = style_directive;
+ return p`"${name}": ${expression}`;
+ });
+
+ const style_expression =
+ style_expression_list.length > 0 &&
+ x`{ ${style_expression_list} }`;
+
if (node.attributes.some(attr => attr.is_spread)) {
// TODO dry this out
const args = [];
@@ -65,9 +74,10 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
}
});
- renderer.add_expression(x`@spread([${args}], ${class_expression})`);
+ renderer.add_expression(x`@spread([${args}], { classes: ${class_expression}, styles: ${style_expression} })`);
} else {
let add_class_attribute = !!class_expression;
+ let add_style_attribute = !!style_expression;
node.attributes.forEach(attribute => {
const name = attribute.name.toLowerCase();
const attr_name = node.namespace === namespaces.foreign ? attribute.name : fix_attribute_casing(attribute.name);
@@ -88,6 +98,9 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
renderer.add_string(` ${attr_name}="`);
renderer.add_expression(x`[${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim()`);
renderer.add_string('"');
+ } else if (name === 'style' && style_expression) {
+ add_style_attribute = false;
+ renderer.add_expression(x`@add_styles(@merge_ssr_styles(${get_attribute_value(attribute)}, ${style_expression}))`);
} else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') {
const snippet = (attribute.chunks[0] as Expression).node;
renderer.add_expression(x`@add_attribute("${attr_name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`);
@@ -98,7 +111,10 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
}
});
if (add_class_attribute) {
- renderer.add_expression(x`@add_classes([${class_expression}].join(' ').trim())`);
+ renderer.add_expression(x`@add_classes((${class_expression}).trim())`);
+ }
+ if (add_style_attribute) {
+ renderer.add_expression(x`@add_styles(${style_expression})`);
}
}
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 b999fbd803..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,16 +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'
+| 'StyleDirective'
| 'EventHandler'
| 'Let'
| 'Ref'
| 'Transition';
interface BaseDirective extends BaseNode {
+ type: DirectiveType;
+ name: string;
+}
+
+interface BaseExpressionDirective extends BaseDirective {
type: DirectiveType;
expression: null | Node;
name: string;
@@ -63,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 832f47aacc..7de0f88413 100644
--- a/src/compiler/parse/state/tag.ts
+++ b/src/compiler/parse/state/tag.ts
@@ -1,13 +1,13 @@
-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 } from '../../interfaces';
-import fuzzymatch from '../../utils/fuzzymatch';
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 { 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\-]*/;
@@ -365,9 +365,25 @@ function read_attribute(parser: Parser, unique_names: Set) {
parser.error(parser_errors.invalid_ref_directive(directive_name), start);
}
- if (value[0]) {
- if ((value as any[]).length > 1 || value[0].type === 'Text') {
- parser.error(parser_errors.invalid_directive_value, value[0].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 (attribute_contains_text) {
+ parser.error(parser_errors.invalid_directive_value, first_value.start);
+ } else {
+ expression = first_value.expression;
}
}
@@ -377,7 +393,7 @@ function read_attribute(parser: Parser, unique_names: Set) {
type,
name: directive_name,
modifiers,
- expression: (value[0] && value[0].expression) || null
+ expression
};
if (type === 'Transition') {
@@ -386,6 +402,7 @@ function read_attribute(parser: Parser, unique_names: Set) {
directive.outro = direction === 'out' || direction === 'transition';
}
+ // Directive name is expression, e.g.
if (!directive.expression && (type === 'Binding' || type === 'Class')) {
directive.expression = {
start: directive.start + colon_index + 1,
@@ -414,6 +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 'StyleDirective';
if (name === 'on') return 'EventHandler';
if (name === 'let') return 'Let';
if (name === 'ref') return 'Ref';
@@ -471,6 +489,8 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
data: null
};
+ const chunks: TemplateNode[] = [];
+
function flush(end: number) {
if (current_chunk.raw) {
current_chunk.data = decode_character_references(current_chunk.raw);
@@ -479,8 +499,6 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
}
}
- const chunks: TemplateNode[] = [];
-
while (parser.index < parser.template.length) {
const index = parser.index;
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/compiler/utils/namespaces.ts b/src/compiler/utils/namespaces.ts
index 01d9bd60b3..7da64afc8c 100644
--- a/src/compiler/utils/namespaces.ts
+++ b/src/compiler/utils/namespaces.ts
@@ -1,6 +1,6 @@
// The `foreign` namespace covers all DOM implementations that aren't HTML5.
// It opts out of HTML5-specific a11y checks and case-insensitive attribute names.
-export const foreign = 'https://svelte.dev/docs#svelte_options';
+export const foreign = 'https://svelte.dev/docs#template-syntax-svelte-options';
export const html = 'http://www.w3.org/1999/xhtml';
export const mathml = 'http://www.w3.org/1998/Math/MathML';
export const svg = 'http://www.w3.org/2000/svg';
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 255c071ac4..1059281140 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);
@@ -530,7 +530,11 @@ export function set_input_type(input, type) {
}
export function set_style(node, key, value, important) {
- node.style.setProperty(key, value, important ? 'important' : '');
+ if (value === null) {
+ node.style.removeProperty(key);
+ } else {
+ node.style.setProperty(key, value, important ? 'important' : '');
+ }
}
export function select_option(select, value) {
diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts
index fd72aa1548..aeaf6e8de8 100644
--- a/src/runtime/internal/ssr.ts
+++ b/src/runtime/internal/ssr.ts
@@ -6,15 +6,29 @@ export const invalid_attribute_name_character = /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFF
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter
-export function spread(args, classes_to_add) {
+export function spread(args, attrs_to_add) {
const attributes = Object.assign({}, ...args);
- if (classes_to_add) {
- if (attributes.class == null) {
- attributes.class = classes_to_add;
- } else {
- attributes.class += ' ' + classes_to_add;
+ if (attrs_to_add) {
+ const classes_to_add = attrs_to_add.classes;
+ const styles_to_add = attrs_to_add.styles;
+
+ if (classes_to_add) {
+ if (attributes.class == null) {
+ attributes.class = classes_to_add;
+ } else {
+ attributes.class += ' ' + classes_to_add;
+ }
+ }
+
+ if (styles_to_add) {
+ if (attributes.style == null) {
+ attributes.style = style_object_to_string(styles_to_add);
+ } else {
+ attributes.style = style_object_to_string(merge_ssr_styles(attributes.style, styles_to_add));
+ }
}
}
+
let str = '';
Object.keys(attributes).forEach(name => {
@@ -32,6 +46,28 @@ export function spread(args, classes_to_add) {
return str;
}
+export function merge_ssr_styles(style_attribute, style_directive) {
+ const style_object = {};
+ for (const individual_style of style_attribute.split(';')) {
+ const colon_index = individual_style.indexOf(':');
+ const name = individual_style.slice(0, colon_index).trim();
+ const value = individual_style.slice(colon_index + 1).trim();
+ if (!name) continue;
+ style_object[name] = value;
+ }
+
+ for (const name in style_directive) {
+ const value = style_directive[name];
+ if (value) {
+ style_object[name] = value;
+ } else {
+ delete style_object[name];
+ }
+ }
+
+ return style_object;
+}
+
export const escaped = {
'"': '"',
"'": ''',
@@ -141,9 +177,22 @@ export function create_ssr_component(fn) {
export function add_attribute(name, value, boolean) {
if (value == null || (boolean && !value)) return '';
- return ` ${name}${value === true ? '' : `=${typeof value === 'string' ? JSON.stringify(escape(value)) : `"${value}"`}`}`;
+ return ` ${name}${value === true && boolean_attributes.has(name) ? '' : `=${typeof value === 'string' ? JSON.stringify(escape(value)) : `"${value}"`}`}`;
}
export function add_classes(classes) {
return classes ? ` class="${classes}"` : '';
}
+
+function style_object_to_string(style_object) {
+ return Object.keys(style_object)
+ .filter(key => style_object[key])
+ .map(key => `${key}: ${style_object[key]};`)
+ .join(' ');
+}
+
+export function add_styles(style_object) {
+ const styles = style_object_to_string(style_object);
+
+ return styles ? ` style="${styles}"` : '';
+}
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/custom-elements/index.ts b/test/custom-elements/index.ts
index 35df156879..42b27eebd3 100644
--- a/test/custom-elements/index.ts
+++ b/test/custom-elements/index.ts
@@ -4,7 +4,7 @@ import * as http from 'http';
import { rollup } from 'rollup';
import virtual from '@rollup/plugin-virtual';
import puppeteer from 'puppeteer';
-import { addLineNumbers, loadConfig, loadSvelte } from '../helpers';
+import { addLineNumbers, loadConfig, loadSvelte, retryAsync, executeBrowserTest } from '../helpers';
import { deepEqual } from 'assert';
const page = `
@@ -17,8 +17,8 @@ const page = `
const assert = fs.readFileSync(`${__dirname}/assert.js`, 'utf-8');
describe('custom-elements', function() {
+ // Note: Increase the timeout in preparation for restarting Chromium due to SIGSEGV.
this.timeout(10000);
-
let svelte;
let server;
let browser;
@@ -44,12 +44,16 @@ describe('custom-elements', function() {
});
}
+ async function launchPuppeteer() {
+ return await retryAsync(() => puppeteer.launch());
+ }
+
before(async () => {
svelte = loadSvelte();
console.log('[custom-element] Loaded Svelte');
server = await create_server();
console.log('[custom-element] Started server');
- browser = await puppeteer.launch();
+ browser = await launchPuppeteer();
console.log('[custom-element] Launched puppeteer browser');
});
@@ -108,26 +112,7 @@ describe('custom-elements', function() {
const result = await bundle.generate({ format: 'iife', name: 'test' });
code = result.output[0].code;
- const page = await browser.newPage();
-
- page.on('console', (type) => {
- console[type._type](type._text);
- });
-
- page.on('error', error => {
- console.log('>>> an error happened');
- console.error(error);
- });
-
- try {
- await page.goto('http://localhost:6789');
-
- const result = await page.evaluate(() => test(document.querySelector('main')));
- if (result) console.log(result);
- } catch (err) {
- console.log(addLineNumbers(code));
- throw err;
- } finally {
+ function assertWarnings() {
if (expected_warnings) {
deepEqual(warnings.map(w => ({
code: w.code,
@@ -138,6 +123,15 @@ describe('custom-elements', function() {
})), expected_warnings);
}
}
+
+ browser = await executeBrowserTest(
+ browser,
+ launchPuppeteer,
+ assertWarnings,
+ () => {
+ console.log(addLineNumbers(code));
+ assertWarnings();
+ });
});
});
});
diff --git a/test/helpers.ts b/test/helpers.ts
index 101490d4dc..1ea0b19880 100644
--- a/test/helpers.ts
+++ b/test/helpers.ts
@@ -8,7 +8,7 @@ export const assert = (assert$1 as unknown) as typeof assert$1 & { htmlEqual: (a
// for coverage purposes, we need to test source files,
// but for sanity purposes, we need to test dist files
-export function loadSvelte(test) {
+export function loadSvelte(test: boolean = false) {
process.env.TEST = test ? 'true' : '';
const resolved = require.resolve('../compiler.js');
@@ -55,7 +55,7 @@ export function cleanRequireCache() {
const virtualConsole = new jsdom.VirtualConsole();
virtualConsole.sendTo(console);
-const window = new jsdom.JSDOM(' ', {virtualConsole}).window;
+const window = new jsdom.JSDOM(' ', { virtualConsole }).window;
global.document = window.document;
global.navigator = window.navigator;
global.getComputedStyle = window.getComputedStyle;
@@ -279,3 +279,50 @@ export function prettyPrintPuppeteerAssertionError(message) {
assert.equal(match[1], match[2]);
}
}
+
+export async function retryAsync(fn: () => Promise, maxAttempts: number = 3, interval: number = 1000): Promise {
+ let attempts = 0;
+ while (attempts <= maxAttempts) {
+ try {
+ return await fn();
+ } catch (err) {
+ if (++attempts >= maxAttempts) throw err;
+ await new Promise(resolve => setTimeout(resolve, interval));
+ }
+ }
+}
+
+// NOTE: Chromium may exit with SIGSEGV, so retry in that case
+export async function executeBrowserTest(browser, launchPuppeteer: () => Promise, additionalAssertion: () => void, onError: (err: Error) => void) {
+ let count = 0;
+ do {
+ count++;
+ try {
+ const page = await browser.newPage();
+
+ page.on('console', (type) => {
+ console[type._type](type._text);
+ });
+
+ page.on('error', error => {
+ console.log('>>> an error happened');
+ console.error(error);
+ });
+ await page.goto('http://localhost:6789');
+ const result = await page.evaluate(() => test(document.querySelector('main')));
+ if (result) console.log(result);
+ additionalAssertion();
+ await page.close();
+ break;
+ } catch (err) {
+ if (count === 5 || browser.isConnected()) {
+ onError(err);
+ throw err;
+ }
+ console.debug(err.stack || err);
+ console.log('RESTARTING Chromium...');
+ browser = await launchPuppeteer();
+ }
+ } while (count <= 5);
+ return browser;
+}
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/js/samples/src-attribute-check-in-foreign/expected.js b/test/js/samples/src-attribute-check-in-foreign/expected.js
index 3d781b72d4..2eb4c0713c 100644
--- a/test/js/samples/src-attribute-check-in-foreign/expected.js
+++ b/test/js/samples/src-attribute-check-in-foreign/expected.js
@@ -18,8 +18,8 @@ function create_fragment(ctx) {
return {
c() {
- svg = document.createElementNS("https://svelte.dev/docs#svelte_options", "svg");
- img = document.createElementNS("https://svelte.dev/docs#svelte_options", "img");
+ svg = document.createElementNS("https://svelte.dev/docs#template-syntax-svelte-options", "svg");
+ img = document.createElementNS("https://svelte.dev/docs#template-syntax-svelte-options", "img");
this.h();
},
l(nodes) {
diff --git a/test/parser/samples/attribute-class-directive/input.svelte b/test/parser/samples/attribute-class-directive/input.svelte
new file mode 100644
index 0000000000..629df2e29f
--- /dev/null
+++ b/test/parser/samples/attribute-class-directive/input.svelte
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test/parser/samples/attribute-class-directive/output.json b/test/parser/samples/attribute-class-directive/output.json
new file mode 100644
index 0000000000..abb2e0c0be
--- /dev/null
+++ b/test/parser/samples/attribute-class-directive/output.json
@@ -0,0 +1,41 @@
+{
+ "html": {
+ "start": 0,
+ "end": 29,
+ "type": "Fragment",
+ "children": [
+ {
+ "start": 0,
+ "end": 29,
+ "type": "Element",
+ "name": "div",
+ "attributes": [
+ {
+ "start": 5,
+ "end": 22,
+ "type": "Class",
+ "name": "foo",
+ "modifiers": [],
+ "expression": {
+ "type": "Identifier",
+ "start": 16,
+ "end": 21,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 16
+ },
+ "end": {
+ "line": 1,
+ "column": 21
+ }
+ },
+ "name": "isFoo"
+ }
+ }
+ ],
+ "children": []
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/test/parser/samples/attribute-style-directive-shorthand/input.svelte b/test/parser/samples/attribute-style-directive-shorthand/input.svelte
new file mode 100644
index 0000000000..3e2c66f218
--- /dev/null
+++ b/test/parser/samples/attribute-style-directive-shorthand/input.svelte
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test/parser/samples/attribute-style-directive-shorthand/output.json b/test/parser/samples/attribute-style-directive-shorthand/output.json
new file mode 100644
index 0000000000..ac658408e6
--- /dev/null
+++ b/test/parser/samples/attribute-style-directive-shorthand/output.json
@@ -0,0 +1,25 @@
+{
+ "html": {
+ "start": 0,
+ "end": 23,
+ "type": "Fragment",
+ "children": [
+ {
+ "start": 0,
+ "end": 23,
+ "type": "Element",
+ "name": "div",
+ "attributes": [
+ {
+ "start": 5,
+ "end": 16,
+ "type": "StyleDirective",
+ "name": "color",
+ "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
new file mode 100644
index 0000000000..dd8ec8dc94
--- /dev/null
+++ b/test/parser/samples/attribute-style-directive-string/input.svelte
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000000..e81e7be3c9
--- /dev/null
+++ b/test/parser/samples/attribute-style-directive-string/output.json
@@ -0,0 +1,355 @@
+{
+ "html": {
+ "start": 0,
+ "end": 252,
+ "type": "Fragment",
+ "children": [
+ {
+ "start": 0,
+ "end": 29,
+ "type": "Element",
+ "name": "div",
+ "attributes": [
+ {
+ "start": 5,
+ "end": 22,
+ "type": "StyleDirective",
+ "name": "color",
+ "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
+ }
+ },
+ "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": []
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/test/parser/samples/attribute-style-directive/input.svelte b/test/parser/samples/attribute-style-directive/input.svelte
new file mode 100644
index 0000000000..536d162326
--- /dev/null
+++ b/test/parser/samples/attribute-style-directive/input.svelte
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test/parser/samples/attribute-style-directive/output.json b/test/parser/samples/attribute-style-directive/output.json
new file mode 100644
index 0000000000..f36ce6b01e
--- /dev/null
+++ b/test/parser/samples/attribute-style-directive/output.json
@@ -0,0 +1,47 @@
+{
+ "html": {
+ "start": 0,
+ "end": 33,
+ "type": "Fragment",
+ "children": [
+ {
+ "start": 0,
+ "end": 33,
+ "type": "Element",
+ "name": "div",
+ "attributes": [
+ {
+ "start": 5,
+ "end": 26,
+ "type": "StyleDirective",
+ "name": "color",
+ "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"
+ }
+ }
+ ]
+ }
+ ],
+ "children": []
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/test/parser/samples/attribute-style/input.svelte b/test/parser/samples/attribute-style/input.svelte
new file mode 100644
index 0000000000..12872605ad
--- /dev/null
+++ b/test/parser/samples/attribute-style/input.svelte
@@ -0,0 +1 @@
+red
\ No newline at end of file
diff --git a/test/parser/samples/attribute-style/output.json b/test/parser/samples/attribute-style/output.json
new file mode 100644
index 0000000000..95c0c8a7a4
--- /dev/null
+++ b/test/parser/samples/attribute-style/output.json
@@ -0,0 +1,41 @@
+{
+ "html": {
+ "start": 0,
+ "end": 34,
+ "type": "Fragment",
+ "children": [
+ {
+ "start": 0,
+ "end": 34,
+ "type": "Element",
+ "name": "div",
+ "attributes": [
+ {
+ "start": 5,
+ "end": 24,
+ "type": "Attribute",
+ "name": "style",
+ "value": [
+ {
+ "start": 12,
+ "end": 23,
+ "type": "Text",
+ "raw": "color: red;",
+ "data": "color: red;"
+ }
+ ]
+ }
+ ],
+ "children": [
+ {
+ "start": 25,
+ "end": 28,
+ "type": "Text",
+ "raw": "red",
+ "data": "red"
+ }
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/test/runtime-puppeteer/index.ts b/test/runtime-puppeteer/index.ts
index b700d47945..8e6f813de4 100644
--- a/test/runtime-puppeteer/index.ts
+++ b/test/runtime-puppeteer/index.ts
@@ -9,7 +9,9 @@ import {
loadConfig,
loadSvelte,
mkdirp,
- prettyPrintPuppeteerAssertionError
+ prettyPrintPuppeteerAssertionError,
+ retryAsync,
+ executeBrowserTest
} from '../helpers';
import { deepEqual } from 'assert';
@@ -48,15 +50,21 @@ function create_server() {
});
}
+async function launchPuppeteer() {
+ return await retryAsync(() => puppeteer.launch());
+}
+
const assert = fs.readFileSync(`${__dirname}/assert.js`, 'utf-8');
-describe('runtime (puppeteer)', () => {
+describe('runtime (puppeteer)', function() {
+ // Note: Increase the timeout in preparation for restarting Chromium due to SIGSEGV.
+ this.timeout(10000);
before(async () => {
svelte = loadSvelte(false);
console.log('[runtime-puppeteer] Loaded Svelte');
server = await create_server();
console.log('[runtime-puppeteer] Started server');
- browser = await puppeteer.launch();
+ browser = await launchPuppeteer();
console.log('[runtime-puppeteer] Launched puppeteer browser');
});
@@ -205,27 +213,7 @@ describe('runtime (puppeteer)', () => {
const result = await bundle.generate({ format: 'iife', name: 'test' });
code = result.output[0].code;
- const page = await browser.newPage();
-
- page.on('console', (type) => {
- console[type._type](type._text);
- });
-
- page.on('error', error => {
- console.log('>>> an error happened');
- console.error(error);
- });
-
- try {
- await page.goto('http://localhost:6789');
-
- const result = await page.evaluate(() => test(document.querySelector('main')));
- if (result) console.log(result);
- } catch (err) {
- failed.add(dir);
- prettyPrintPuppeteerAssertionError(err.message);
- throw err;
- } finally {
+ function assertWarnings() {
if (config.warnings) {
deepEqual(warnings.map(w => ({
code: w.code,
@@ -240,6 +228,16 @@ describe('runtime (puppeteer)', () => {
throw new Error('Received unexpected warnings');
}
}
+
+ browser = await executeBrowserTest(
+ browser,
+ launchPuppeteer,
+ assertWarnings,
+ (err) => {
+ failed.add(dir);
+ prettyPrintPuppeteerAssertionError(err.message);
+ assertWarnings();
+ });
});
}
diff --git a/test/runtime/index.ts b/test/runtime/index.ts
index e777196ae2..f3c962f1d9 100644
--- a/test/runtime/index.ts
+++ b/test/runtime/index.ts
@@ -65,7 +65,7 @@ describe('runtime', () => {
}
const testName = `${dir} ${hydrate ? `(with hydration${from_ssr_html ? ' from ssr rendered html' : ''})` : ''}`;
- (config.skip ? it.skip : solo ? it.only : it)(testName, () => {
+ (config.skip ? it.skip : solo ? it.only : it)(testName, (done) => {
if (failed.has(dir)) {
// this makes debugging easier, by only printing compiled output once
throw new Error('skipping test, already failed');
@@ -96,7 +96,7 @@ describe('runtime', () => {
glob('**/*.svelte', { cwd }).forEach(file => {
if (file[0] === '_') return;
- const dir = `${cwd}/_output/${hydrate ? 'hydratable' : 'normal'}`;
+ const dir = `${cwd}/_output/${hydrate ? 'hydratable' : 'normal'}`;
const out = `${dir}/${file.replace(/\.svelte$/, '.js')}`;
if (fs.existsSync(out)) {
@@ -120,7 +120,7 @@ describe('runtime', () => {
}
});
- return Promise.resolve()
+ Promise.resolve()
.then(() => {
// hack to support transition tests
clear_loops();
@@ -245,6 +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(err);
throw err;
})
.then(() => {
@@ -255,6 +256,7 @@ describe('runtime', () => {
flush();
if (config.after_test) config.after_test();
+ done();
});
});
}
diff --git a/test/runtime/samples/attribute-boolean-true/_config.js b/test/runtime/samples/attribute-boolean-true/_config.js
index 79c8ea1ad6..89b2fc7c21 100644
--- a/test/runtime/samples/attribute-boolean-true/_config.js
+++ b/test/runtime/samples/attribute-boolean-true/_config.js
@@ -1,7 +1,8 @@
export default {
- html: '',
+ html: '',
test({ assert, target }) {
const textarea = target.querySelector('textarea');
+ assert.equal(textarea.dataset.attr, 'true');
assert.ok(textarea.readOnly);
}
};
diff --git a/test/runtime/samples/attribute-boolean-true/main.svelte b/test/runtime/samples/attribute-boolean-true/main.svelte
index 04d5cab8f9..6474644749 100644
--- a/test/runtime/samples/attribute-boolean-true/main.svelte
+++ b/test/runtime/samples/attribute-boolean-true/main.svelte
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/test/runtime/samples/binding-input-member-expression-update/_config.js b/test/runtime/samples/binding-input-member-expression-update/_config.js
new file mode 100644
index 0000000000..c0dfc3bbe8
--- /dev/null
+++ b/test/runtime/samples/binding-input-member-expression-update/_config.js
@@ -0,0 +1,18 @@
+// binding member expression shouldn't invalidate the property name
+export default {
+ async test({ assert, component, target, window }) {
+ const input = target.querySelector('input');
+ assert.deepEqual(component.logs.length, 1);
+ assert.equal(input.value, 'abc');
+
+ input.value = 'hij';
+ await input.dispatchEvent(new window.Event('input'));
+
+ assert.deepEqual(component.values.a, 'hij');
+ assert.deepEqual(component.logs.length, 1);
+
+ component.paths = ['b'];
+ assert.deepEqual(component.logs.length, 2);
+ assert.equal(input.value, 'def');
+ }
+};
diff --git a/test/runtime/samples/binding-input-member-expression-update/main.svelte b/test/runtime/samples/binding-input-member-expression-update/main.svelte
new file mode 100644
index 0000000000..dc72b229ba
--- /dev/null
+++ b/test/runtime/samples/binding-input-member-expression-update/main.svelte
@@ -0,0 +1,9 @@
+
+
+
diff --git a/test/runtime/samples/binding-this-each-key/_config.js b/test/runtime/samples/binding-this-each-key/_config.js
new file mode 100644
index 0000000000..332e14fda8
--- /dev/null
+++ b/test/runtime/samples/binding-this-each-key/_config.js
@@ -0,0 +1,10 @@
+export default {
+ html: 'content
content
content
',
+
+ test({ assert, target, component }) {
+ const divs = target.querySelectorAll('div');
+ assert.equal(component.refs[0], divs[0]);
+ assert.equal(component.refs[1], divs[1]);
+ assert.equal(component.refs[2], divs[2]);
+ }
+};
diff --git a/test/runtime/samples/binding-this-each-key/main.svelte b/test/runtime/samples/binding-this-each-key/main.svelte
new file mode 100644
index 0000000000..cff2ffa226
--- /dev/null
+++ b/test/runtime/samples/binding-this-each-key/main.svelte
@@ -0,0 +1,15 @@
+
+
+{#each list as { id }, index (id)}
+
+ content
+
+{/each}
diff --git a/test/runtime/samples/binding-this-member-expression-update/_config.js b/test/runtime/samples/binding-this-member-expression-update/_config.js
new file mode 100644
index 0000000000..782d44311c
--- /dev/null
+++ b/test/runtime/samples/binding-this-member-expression-update/_config.js
@@ -0,0 +1,8 @@
+// binding member expression shouldn't invalidate the property name
+export default {
+ test({ assert, component, target }) {
+ const div = target.querySelector('div');
+ assert.equal(div, component.container.a);
+ assert.deepEqual(component.logs.length, 1);
+ }
+};
diff --git a/test/runtime/samples/binding-this-member-expression-update/main.svelte b/test/runtime/samples/binding-this-member-expression-update/main.svelte
new file mode 100644
index 0000000000..327d63d494
--- /dev/null
+++ b/test/runtime/samples/binding-this-member-expression-update/main.svelte
@@ -0,0 +1,9 @@
+
+
+
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
+ `,
+ 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
+ `);
+
+ 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/if-block-else-update/_config.js b/test/runtime/samples/if-block-else-update/_config.js
new file mode 100644
index 0000000000..db35ca8488
--- /dev/null
+++ b/test/runtime/samples/if-block-else-update/_config.js
@@ -0,0 +1,46 @@
+export default {
+ async test({ assert, component, target, window }) {
+ const [btn1, btn2] = target.querySelectorAll('button');
+
+ const clickEvent = new window.MouseEvent('click');
+
+ await btn2.dispatchEvent(clickEvent);
+ assert.htmlEqual(target.innerHTML, `
+ Toggle foo
+ Toggle bar
+
+ foo: false, bar: true
+
+ bar!
+ `);
+
+ await btn1.dispatchEvent(clickEvent);
+ assert.htmlEqual(target.innerHTML, `
+ Toggle foo
+ Toggle bar
+
+ foo: true, bar: true
+
+ foo!
+ `);
+
+ await btn2.dispatchEvent(clickEvent);
+ assert.htmlEqual(target.innerHTML, `
+ Toggle foo
+ Toggle bar
+
+ foo: true, bar: false
+
+ foo!
+ `);
+
+ await btn1.dispatchEvent(clickEvent);
+ assert.htmlEqual(target.innerHTML, `
+ Toggle foo
+ Toggle bar
+
+ foo: false, bar: false
+
+ `);
+ }
+};
diff --git a/test/runtime/samples/if-block-else-update/main.svelte b/test/runtime/samples/if-block-else-update/main.svelte
new file mode 100644
index 0000000000..258d0422ba
--- /dev/null
+++ b/test/runtime/samples/if-block-else-update/main.svelte
@@ -0,0 +1,21 @@
+
+
+ foo = !foo}>
+ Toggle foo
+
+ bar[0] = !bar[0]}>
+ Toggle bar
+
+
+
+{@html `foo: ${foo}, bar: ${bar.every(x => x)}`}
+
+
+{#if foo}
+ foo!
+{:else if bar.every(x => x)}
+ bar!
+{/if}
\ No newline at end of file
diff --git a/test/runtime/samples/inline-style-directive-and-style-attr-merged/_config.js b/test/runtime/samples/inline-style-directive-and-style-attr-merged/_config.js
new file mode 100644
index 0000000000..f5780daf80
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-and-style-attr-merged/_config.js
@@ -0,0 +1,13 @@
+export default {
+ html: `
+
+ `,
+
+ test({ assert, target, window }) {
+ const p = target.querySelector('p');
+
+ const styles = window.getComputedStyle(p);
+ assert.equal(styles.color, 'red');
+ assert.equal(styles.height, '40px');
+ }
+};
diff --git a/test/runtime/samples/inline-style-directive-and-style-attr-merged/main.svelte b/test/runtime/samples/inline-style-directive-and-style-attr-merged/main.svelte
new file mode 100644
index 0000000000..6f3e86b9bf
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-and-style-attr-merged/main.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/test/runtime/samples/inline-style-directive-and-style-attr/_config.js b/test/runtime/samples/inline-style-directive-and-style-attr/_config.js
new file mode 100644
index 0000000000..f5780daf80
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-and-style-attr/_config.js
@@ -0,0 +1,13 @@
+export default {
+ html: `
+
+ `,
+
+ test({ assert, target, window }) {
+ const p = target.querySelector('p');
+
+ const styles = window.getComputedStyle(p);
+ assert.equal(styles.color, 'red');
+ assert.equal(styles.height, '40px');
+ }
+};
diff --git a/test/runtime/samples/inline-style-directive-and-style-attr/main.svelte b/test/runtime/samples/inline-style-directive-and-style-attr/main.svelte
new file mode 100644
index 0000000000..e2ba0fa591
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-and-style-attr/main.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/test/runtime/samples/inline-style-directive-css-vars/_config.js b/test/runtime/samples/inline-style-directive-css-vars/_config.js
new file mode 100644
index 0000000000..c8b239a42b
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-css-vars/_config.js
@@ -0,0 +1,9 @@
+export default {
+ html: '
',
+
+ test({ assert, component, target }) {
+ component.myColor = 'blue';
+
+ assert.htmlEqual(target.innerHTML, '
');
+ }
+};
diff --git a/test/runtime/samples/inline-style-directive-css-vars/main.svelte b/test/runtime/samples/inline-style-directive-css-vars/main.svelte
new file mode 100644
index 0000000000..689a2333f5
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-css-vars/main.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/test/runtime/samples/inline-style-directive-dynamic/_config.js b/test/runtime/samples/inline-style-directive-dynamic/_config.js
new file mode 100644
index 0000000000..a2214fd872
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-dynamic/_config.js
@@ -0,0 +1,10 @@
+export default {
+ html: `
+
+ `,
+
+ test({ assert, component, target }) {
+ component.myColor = 'blue';
+ assert.htmlEqual(target.innerHTML, '
');
+ }
+};
diff --git a/test/runtime/samples/inline-style-directive-dynamic/main.svelte b/test/runtime/samples/inline-style-directive-dynamic/main.svelte
new file mode 100644
index 0000000000..1d60aefa6f
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-dynamic/main.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/test/runtime/samples/inline-style-directive-multiple/_config.js b/test/runtime/samples/inline-style-directive-multiple/_config.js
new file mode 100644
index 0000000000..725d03c4fe
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-multiple/_config.js
@@ -0,0 +1,27 @@
+export default {
+ html: `
+
+ `,
+
+ test({ assert, component, target, window }) {
+ const p = target.querySelector('p');
+
+ let styles = window.getComputedStyle(p);
+ assert.equal(styles.color, 'red');
+
+ component.myColor = 'pink';
+ component.width = '100vh';
+ component.absolute = true;
+ component.bold = false;
+
+ styles = window.getComputedStyle(p);
+ assert.htmlEqual(
+ target.innerHTML,
+ '
'
+ );
+ assert.equal(styles.color, 'pink');
+ assert.equal(styles.width, '100vh');
+ assert.equal(styles.fontWeight, '100');
+ assert.equal(styles.position, 'absolute');
+ }
+};
diff --git a/test/runtime/samples/inline-style-directive-multiple/main.svelte b/test/runtime/samples/inline-style-directive-multiple/main.svelte
new file mode 100644
index 0000000000..06f03206d5
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-multiple/main.svelte
@@ -0,0 +1,13 @@
+
+
+
diff --git a/test/runtime/samples/inline-style-directive-shorthand/_config.js b/test/runtime/samples/inline-style-directive-shorthand/_config.js
new file mode 100644
index 0000000000..352d4a139c
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-shorthand/_config.js
@@ -0,0 +1,25 @@
+export default {
+ html: `
+
+
+ `,
+
+ test({ assert, component, target, window }) {
+ const [p1, p2] = target.querySelectorAll('p');
+
+ assert.equal(window.getComputedStyle(p1).color, 'red');
+ assert.equal(window.getComputedStyle(p2).color, 'red');
+
+ component.color = 'blue';
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+ `
+ );
+
+ 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
new file mode 100644
index 0000000000..2b5f8e3db7
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-shorthand/main.svelte
@@ -0,0 +1,9 @@
+
+
+
+
+{#each [1] as _}
+
+{/each}
diff --git a/test/runtime/samples/inline-style-directive-spread-and-attr-empty/_config.js b/test/runtime/samples/inline-style-directive-spread-and-attr-empty/_config.js
new file mode 100644
index 0000000000..f3dcbc8823
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-spread-and-attr-empty/_config.js
@@ -0,0 +1,5 @@
+export default {
+ html: `
+
+ `
+};
diff --git a/test/runtime/samples/inline-style-directive-spread-and-attr-empty/main.svelte b/test/runtime/samples/inline-style-directive-spread-and-attr-empty/main.svelte
new file mode 100644
index 0000000000..c2d5cb4d3f
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-spread-and-attr-empty/main.svelte
@@ -0,0 +1,11 @@
+
+
+
diff --git a/test/runtime/samples/inline-style-directive-spread-and-attr/_config.js b/test/runtime/samples/inline-style-directive-spread-and-attr/_config.js
new file mode 100644
index 0000000000..b3660bd5ca
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-spread-and-attr/_config.js
@@ -0,0 +1,43 @@
+export default {
+ html: `
+
+ `,
+
+ test({ assert, component, target, window }) {
+ const p = target.querySelector('p');
+
+ let styles = window.getComputedStyle(p);
+ assert.equal(styles.color, 'green');
+
+ component.color = null;
+ assert.htmlEqual(target.innerHTML, '
');
+ styles = window.getComputedStyle(p);
+ assert.equal(styles.color, '');
+
+ component.spread = { style: 'color: yellow; padding: 30px;' };
+
+ assert.htmlEqual(target.innerHTML, '
');
+ styles = window.getComputedStyle(p);
+ assert.equal(styles.color, '');
+ assert.equal(styles.padding, '30px');
+
+ component.spread = {};
+ component.style = 'color: blue; background-color: green;';
+ assert.htmlEqual(
+ target.innerHTML,
+ '
'
+ );
+ styles = window.getComputedStyle(p);
+ assert.equal(styles.color, '');
+ assert.equal(styles.backgroundColor, 'green');
+
+ component.color = 'purple';
+ assert.htmlEqual(
+ target.innerHTML,
+ '
'
+ );
+ styles = window.getComputedStyle(p);
+ assert.equal(styles.color, 'purple');
+ assert.equal(styles.backgroundColor, 'green');
+ }
+};
diff --git a/test/runtime/samples/inline-style-directive-spread-and-attr/main.svelte b/test/runtime/samples/inline-style-directive-spread-and-attr/main.svelte
new file mode 100644
index 0000000000..a9696df675
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-spread-and-attr/main.svelte
@@ -0,0 +1,11 @@
+
+
+
diff --git a/test/runtime/samples/inline-style-directive-spread-dynamic/_config.js b/test/runtime/samples/inline-style-directive-spread-dynamic/_config.js
new file mode 100644
index 0000000000..152e4036a0
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-spread-dynamic/_config.js
@@ -0,0 +1,39 @@
+export default {
+ html: `
+
+ `,
+
+ test({ assert, component, target, window }) {
+ const p = target.querySelector('p');
+
+ const styles = window.getComputedStyle(p);
+ assert.equal(styles.color, 'blue');
+ assert.equal(styles.width, '65px');
+ assert.equal(p.id, 'my-id');
+
+ component.color = 'red';
+
+ assert.htmlEqual(
+ target.innerHTML,
+ '
'
+ );
+
+ component.obj = { style: 'height: 72px;' };
+
+ assert.htmlEqual(
+ target.innerHTML,
+ '
'
+ );
+
+ component.obj = { style: 'border-radius: 2px; color: orange' };
+
+ assert.htmlEqual(
+ target.innerHTML,
+ '
'
+ );
+
+ component.obj = {};
+
+ assert.htmlEqual(target.innerHTML, '
');
+ }
+};
diff --git a/test/runtime/samples/inline-style-directive-spread-dynamic/main.svelte b/test/runtime/samples/inline-style-directive-spread-dynamic/main.svelte
new file mode 100644
index 0000000000..a514e4856f
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-spread-dynamic/main.svelte
@@ -0,0 +1,5 @@
+
+
diff --git a/test/runtime/samples/inline-style-directive-spread/_config.js b/test/runtime/samples/inline-style-directive-spread/_config.js
new file mode 100644
index 0000000000..adb8a26544
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-spread/_config.js
@@ -0,0 +1,14 @@
+export default {
+ html: `
+
+ `,
+
+ test({ assert, target, window }) {
+ const p = target.querySelector('p');
+
+ const styles = window.getComputedStyle(p);
+ assert.equal(styles.color, 'blue');
+ assert.equal(styles.width, '65px');
+ assert.equal(p.id, 'my-id');
+ }
+};
diff --git a/test/runtime/samples/inline-style-directive-spread/main.svelte b/test/runtime/samples/inline-style-directive-spread/main.svelte
new file mode 100644
index 0000000000..51291146c7
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-spread/main.svelte
@@ -0,0 +1 @@
+
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/runtime/samples/inline-style-directive-string-variable/_config.js b/test/runtime/samples/inline-style-directive-string-variable/_config.js
new file mode 100644
index 0000000000..411a42db8e
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-string-variable/_config.js
@@ -0,0 +1,23 @@
+export default {
+ html: `
+
+ `,
+
+ test({ assert, component, target, window }) {
+ const p = target.querySelector('p');
+
+ const styles = window.getComputedStyle(p);
+ assert.equal(styles.color, 'green');
+ assert.equal(styles.transform, 'translateX(45px)');
+ assert.equal(styles.border, '100px solid pink');
+
+ component.translate_x = '100%';
+ component.border_width = 20;
+ component.border_color = 'yellow';
+
+ assert.htmlEqual(
+ target.innerHTML,
+ '
'
+ );
+ }
+};
diff --git a/test/runtime/samples/inline-style-directive-string-variable/main.svelte b/test/runtime/samples/inline-style-directive-string-variable/main.svelte
new file mode 100644
index 0000000000..1f0b5019f4
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-string-variable/main.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/test/runtime/samples/inline-style-directive-string/_config.js b/test/runtime/samples/inline-style-directive-string/_config.js
new file mode 100644
index 0000000000..ec32e157e5
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-string/_config.js
@@ -0,0 +1,12 @@
+export default {
+ html: `
+
+ `,
+
+ test({ assert, target, window }) {
+ const p = target.querySelector('p');
+
+ const styles = window.getComputedStyle(p);
+ assert.equal(styles.color, 'red');
+ }
+};
diff --git a/test/runtime/samples/inline-style-directive-string/main.svelte b/test/runtime/samples/inline-style-directive-string/main.svelte
new file mode 100644
index 0000000000..4e7ce22b22
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive-string/main.svelte
@@ -0,0 +1 @@
+
diff --git a/test/runtime/samples/inline-style-directive/_config.js b/test/runtime/samples/inline-style-directive/_config.js
new file mode 100644
index 0000000000..ec32e157e5
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive/_config.js
@@ -0,0 +1,12 @@
+export default {
+ html: `
+
+ `,
+
+ test({ assert, target, window }) {
+ const p = target.querySelector('p');
+
+ const styles = window.getComputedStyle(p);
+ assert.equal(styles.color, 'red');
+ }
+};
diff --git a/test/runtime/samples/inline-style-directive/main.svelte b/test/runtime/samples/inline-style-directive/main.svelte
new file mode 100644
index 0000000000..1d60aefa6f
--- /dev/null
+++ b/test/runtime/samples/inline-style-directive/main.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/test/runtime/samples/inline-style/_config.js b/test/runtime/samples/inline-style/_config.js
new file mode 100644
index 0000000000..3e984d4c69
--- /dev/null
+++ b/test/runtime/samples/inline-style/_config.js
@@ -0,0 +1,12 @@
+export default {
+ html: `
+
+ `,
+
+ test({ assert, component, target, window }) {
+ const p = target.querySelector('div');
+
+ const styles = window.getComputedStyle(p);
+ assert.equal(styles.color, 'red');
+ }
+};
diff --git a/test/runtime/samples/inline-style/main.svelte b/test/runtime/samples/inline-style/main.svelte
new file mode 100644
index 0000000000..0f37a04c99
--- /dev/null
+++ b/test/runtime/samples/inline-style/main.svelte
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test/server-side-rendering/index.ts b/test/server-side-rendering/index.ts
index cc3f20d637..98d19a6006 100644
--- a/test/server-side-rendering/index.ts
+++ b/test/server-side-rendering/index.ts
@@ -52,21 +52,23 @@ describe('ssr', () => {
throw new Error('Forgot to remove `solo: true` from test');
}
- (solo ? it.only : it)(dir, () => {
- dir = path.resolve(`${__dirname}/samples`, dir);
+ (solo ? it.only : it)(dir, (done) => {
- cleanRequireCache();
+ try {
- const compileOptions = {
- sveltePath,
- ...config.compileOptions,
- generate: 'ssr',
- format: 'cjs'
- };
+ dir = path.resolve(`${__dirname}/samples`, dir);
- require('../../register')(compileOptions);
+ cleanRequireCache();
+
+ const compileOptions = {
+ sveltePath,
+ ...config.compileOptions,
+ generate: 'ssr',
+ format: 'cjs'
+ };
+
+ require('../../register')(compileOptions);
- try {
const Component = require(`${dir}/main.svelte`).default;
const expectedHtml = tryToReadFile(`${dir}/_expected.html`);
@@ -126,10 +128,11 @@ 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);
}
diff --git a/test/validator/samples/a11y-no-redundant-roles/input.svelte b/test/validator/samples/a11y-no-redundant-roles/input.svelte
new file mode 100644
index 0000000000..05525effb6
--- /dev/null
+++ b/test/validator/samples/a11y-no-redundant-roles/input.svelte
@@ -0,0 +1,44 @@
+a link
+
+
+
+
+
+
+
+
+
+
+
+
+
+heading
+heading
+heading
+heading
+heading
+heading
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/validator/samples/a11y-no-redundant-roles/warnings.json b/test/validator/samples/a11y-no-redundant-roles/warnings.json
new file mode 100644
index 0000000000..8aa3520b32
--- /dev/null
+++ b/test/validator/samples/a11y-no-redundant-roles/warnings.json
@@ -0,0 +1,632 @@
+[
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 23,
+ "column": 23,
+ "line": 1
+ },
+ "message": "A11y: Redundant role 'link'",
+ "pos": 12,
+ "start": {
+ "character": 12,
+ "column": 12,
+ "line": 1
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 58,
+ "column": 23,
+ "line": 2
+ },
+ "message": "A11y: Redundant role 'article'",
+ "pos": 44,
+ "start": {
+ "character": 44,
+ "column": 9,
+ "line": 2
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 89,
+ "column": 27,
+ "line": 3
+ },
+ "message": "A11y: Redundant role 'complementary'",
+ "pos": 69,
+ "start": {
+ "character": 69,
+ "column": 7,
+ "line": 3
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 114,
+ "column": 21,
+ "line": 4
+ },
+ "message": "A11y: Redundant role 'document'",
+ "pos": 99,
+ "start": {
+ "character": 99,
+ "column": 6,
+ "line": 4
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 139,
+ "column": 21,
+ "line": 5
+ },
+ "message": "A11y: Redundant role 'button'",
+ "pos": 126,
+ "start": {
+ "character": 126,
+ "column": 8,
+ "line": 5
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 167,
+ "column": 24,
+ "line": 6
+ },
+ "message": "A11y: Redundant role 'listbox'",
+ "pos": 153,
+ "start": {
+ "character": 153,
+ "column": 10,
+ "line": 6
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 192,
+ "column": 21,
+ "line": 7
+ },
+ "message": "A11y: Redundant role 'definition'",
+ "pos": 175,
+ "start": {
+ "character": 175,
+ "column": 4,
+ "line": 7
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 212,
+ "column": 16,
+ "line": 8
+ },
+ "message": "A11y: Redundant role 'term'",
+ "pos": 201,
+ "start": {
+ "character": 201,
+ "column": 5,
+ "line": 8
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 237,
+ "column": 21,
+ "line": 9
+ },
+ "message": "A11y: Redundant role 'group'",
+ "pos": 225,
+ "start": {
+ "character": 225,
+ "column": 9,
+ "line": 9
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 262,
+ "column": 21,
+ "line": 10
+ },
+ "message": "A11y: Redundant role 'dialog'",
+ "pos": 249,
+ "start": {
+ "character": 249,
+ "column": 8,
+ "line": 10
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 281,
+ "column": 15,
+ "line": 11
+ },
+ "message": "A11y: Redundant role 'term'",
+ "pos": 270,
+ "start": {
+ "character": 270,
+ "column": 4,
+ "line": 11
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 307,
+ "column": 22,
+ "line": 12
+ },
+ "message": "A11y: Redundant role 'group'",
+ "pos": 295,
+ "start": {
+ "character": 295,
+ "column": 10,
+ "line": 12
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 332,
+ "column": 21,
+ "line": 13
+ },
+ "message": "A11y: Redundant role 'figure'",
+ "pos": 319,
+ "start": {
+ "character": 319,
+ "column": 8,
+ "line": 13
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 353,
+ "column": 17,
+ "line": 14
+ },
+ "message": "A11y: Redundant role 'form'",
+ "pos": 342,
+ "start": {
+ "character": 342,
+ "column": 6,
+ "line": 14
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 383,
+ "column": 18,
+ "line": 15
+ },
+ "message": "A11y: Redundant role 'heading'",
+ "pos": 369,
+ "start": {
+ "character": 369,
+ "column": 4,
+ "line": 15
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 415,
+ "column": 18,
+ "line": 16
+ },
+ "message": "A11y: Redundant role 'heading'",
+ "pos": 401,
+ "start": {
+ "character": 401,
+ "column": 4,
+ "line": 16
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 447,
+ "column": 18,
+ "line": 17
+ },
+ "message": "A11y: Redundant role 'heading'",
+ "pos": 433,
+ "start": {
+ "character": 433,
+ "column": 4,
+ "line": 17
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 479,
+ "column": 18,
+ "line": 18
+ },
+ "message": "A11y: Redundant role 'heading'",
+ "pos": 465,
+ "start": {
+ "character": 465,
+ "column": 4,
+ "line": 18
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 511,
+ "column": 18,
+ "line": 19
+ },
+ "message": "A11y: Redundant role 'heading'",
+ "pos": 497,
+ "start": {
+ "character": 497,
+ "column": 4,
+ "line": 19
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 543,
+ "column": 18,
+ "line": 20
+ },
+ "message": "A11y: Redundant role 'heading'",
+ "pos": 529,
+ "start": {
+ "character": 529,
+ "column": 4,
+ "line": 20
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 577,
+ "column": 20,
+ "line": 21
+ },
+ "message": "A11y: Redundant role 'separator'",
+ "pos": 561,
+ "start": {
+ "character": 561,
+ "column": 4,
+ "line": 21
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 600,
+ "column": 19,
+ "line": 22
+ },
+ "message": "A11y: Redundant role 'listitem'",
+ "pos": 585,
+ "start": {
+ "character": 585,
+ "column": 4,
+ "line": 22
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 621,
+ "column": 17,
+ "line": 23
+ },
+ "message": "A11y: Redundant role 'link'",
+ "pos": 610,
+ "start": {
+ "character": 610,
+ "column": 6,
+ "line": 23
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 642,
+ "column": 17,
+ "line": 24
+ },
+ "message": "A11y: Redundant role 'main'",
+ "pos": 631,
+ "start": {
+ "character": 631,
+ "column": 6,
+ "line": 24
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 668,
+ "column": 17,
+ "line": 25
+ },
+ "message": "A11y: Redundant role 'list'",
+ "pos": 657,
+ "start": {
+ "character": 657,
+ "column": 6,
+ "line": 25
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 694,
+ "column": 22,
+ "line": 26
+ },
+ "message": "A11y: Redundant role 'navigation'",
+ "pos": 677,
+ "start": {
+ "character": 677,
+ "column": 5,
+ "line": 26
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 713,
+ "column": 15,
+ "line": 27
+ },
+ "message": "A11y: Redundant role 'list'",
+ "pos": 702,
+ "start": {
+ "character": 702,
+ "column": 4,
+ "line": 27
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 739,
+ "column": 22,
+ "line": 28
+ },
+ "message": "A11y: Redundant role 'group'",
+ "pos": 727,
+ "start": {
+ "character": 727,
+ "column": 10,
+ "line": 28
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 764,
+ "column": 21,
+ "line": 29
+ },
+ "message": "A11y: Redundant role 'option'",
+ "pos": 751,
+ "start": {
+ "character": 751,
+ "column": 8,
+ "line": 29
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 789,
+ "column": 21,
+ "line": 30
+ },
+ "message": "A11y: Redundant role 'status'",
+ "pos": 776,
+ "start": {
+ "character": 776,
+ "column": 8,
+ "line": 30
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 821,
+ "column": 28,
+ "line": 31
+ },
+ "message": "A11y: Redundant role 'progressbar'",
+ "pos": 803,
+ "start": {
+ "character": 803,
+ "column": 10,
+ "line": 31
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 847,
+ "column": 22,
+ "line": 32
+ },
+ "message": "A11y: Redundant role 'region'",
+ "pos": 834,
+ "start": {
+ "character": 834,
+ "column": 9,
+ "line": 32
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 873,
+ "column": 22,
+ "line": 33
+ },
+ "message": "A11y: Redundant role 'button'",
+ "pos": 860,
+ "start": {
+ "character": 860,
+ "column": 9,
+ "line": 33
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 896,
+ "column": 19,
+ "line": 34
+ },
+ "message": "A11y: Redundant role 'table'",
+ "pos": 884,
+ "start": {
+ "character": 884,
+ "column": 7,
+ "line": 34
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 922,
+ "column": 22,
+ "line": 35
+ },
+ "message": "A11y: Redundant role 'rowgroup'",
+ "pos": 907,
+ "start": {
+ "character": 907,
+ "column": 7,
+ "line": 35
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 950,
+ "column": 24,
+ "line": 36
+ },
+ "message": "A11y: Redundant role 'textbox'",
+ "pos": 936,
+ "start": {
+ "character": 936,
+ "column": 10,
+ "line": 36
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 976,
+ "column": 22,
+ "line": 37
+ },
+ "message": "A11y: Redundant role 'rowgroup'",
+ "pos": 961,
+ "start": {
+ "character": 961,
+ "column": 7,
+ "line": 37
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 1002,
+ "column": 22,
+ "line": 38
+ },
+ "message": "A11y: Redundant role 'rowgroup'",
+ "pos": 987,
+ "start": {
+ "character": 987,
+ "column": 7,
+ "line": 38
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 1020,
+ "column": 14,
+ "line": 39
+ },
+ "message": "A11y: Redundant role 'row'",
+ "pos": 1010,
+ "start": {
+ "character": 1010,
+ "column": 4,
+ "line": 39
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 1039,
+ "column": 15,
+ "line": 40
+ },
+ "message": "A11y: Redundant role 'list'",
+ "pos": 1028,
+ "start": {
+ "character": 1028,
+ "column": 4,
+ "line": 40
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 1125,
+ "column": 21,
+ "line": 43
+ },
+ "message": "A11y: Redundant role 'banner'",
+ "pos": 1112,
+ "start": {
+ "character": 1112,
+ "column": 8,
+ "line": 43
+ }
+ },
+ {
+ "code": "a11y-no-redundant-roles",
+ "end": {
+ "character": 1162,
+ "column": 26,
+ "line": 44
+ },
+ "message": "A11y: Redundant role 'contentinfo'",
+ "pos": 1144,
+ "start": {
+ "character": 1144,
+ "column": 8,
+ "line": 44
+ }
+ }
+]
\ No newline at end of file
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}
+ b ++} />
+{/each}
\ No newline at end of file
diff --git a/test/validator/samples/const-tag-readonly-2/errors.json b/test/validator/samples/const-tag-readonly-2/errors.json
new file mode 100644
index 0000000000..746a21e86a
--- /dev/null
+++ b/test/validator/samples/const-tag-readonly-2/errors.json
@@ -0,0 +1,9 @@
+[
+ {
+ "code": "invalid-binding",
+ "message": "Cannot bind to a variable declared with {@const ...}",
+ "start": { "line": 7, "column": 9, "character": 89 },
+ "end": { "line": 7, "column": 23, "character": 103 },
+ "pos": 89
+ }
+]
diff --git a/test/validator/samples/const-tag-readonly-2/input.svelte b/test/validator/samples/const-tag-readonly-2/input.svelte
new file mode 100644
index 0000000000..466aac9804
--- /dev/null
+++ b/test/validator/samples/const-tag-readonly-2/input.svelte
@@ -0,0 +1,8 @@
+
+
+{#each array as a}
+ {@const b = a + 1}
+
+{/each}
\ No newline at end of file
diff --git a/test/validator/samples/unreferenced-variables-each/input.svelte b/test/validator/samples/unreferenced-variables-each/input.svelte
new file mode 100644
index 0000000000..a96f9189a2
--- /dev/null
+++ b/test/validator/samples/unreferenced-variables-each/input.svelte
@@ -0,0 +1,20 @@
+
+
+
+
+ {#each array as default_value_5}
+ {#each array as { a = default_value_1, b: { b = [default_value_2, default_value_3], c: [c = default_value_4] }, d = default_value_5, e = default_value_6 }}
+ {a}{b}{c}{d}{e}
+ {/each}
+ {/each}
+
+
diff --git a/test/validator/samples/unreferenced-variables-each/warnings.json b/test/validator/samples/unreferenced-variables-each/warnings.json
new file mode 100644
index 0000000000..fa89da0e68
--- /dev/null
+++ b/test/validator/samples/unreferenced-variables-each/warnings.json
@@ -0,0 +1,32 @@
+[
+ {
+ "code": "unused-export-let",
+ "message": "Component_1 has unused export property 'default_value_5'. If it is for external reference only, please consider using `export const default_value_5`",
+ "pos": 172,
+ "start": {
+ "character": 172,
+ "column": 12,
+ "line": 8
+ },
+ "end": {
+ "character": 187,
+ "column": 27,
+ "line": 8
+ }
+ },
+ {
+ "code": "unused-export-let",
+ "message": "Component_1 has unused export property 'default_value_6'. If it is for external reference only, please consider using `export const default_value_6`",
+ "pos": 201,
+ "start": {
+ "character": 201,
+ "column": 12,
+ "line": 9
+ },
+ "end": {
+ "character": 216,
+ "column": 27,
+ "line": 9
+ }
+ }
+]