Merge branch 'master' into dynamic-elements-implementation

pull/6898/head
baseballyama 5 years ago
commit dc25e2e0cf

2
.gitignore vendored

@ -16,8 +16,6 @@ node_modules
/transition /transition
/animate /animate
/scratch/ /scratch/
/coverage/
/coverage.lcov
/test/*/samples/_ /test/*/samples/_
/yarn-error.log /yarn-error.log
_actual*.* _actual*.*

@ -1,5 +1,37 @@
# Svelte changelog # Svelte changelog
## 3.46.4
* Avoid `maximum call stack size exceeded` errors on large components ([#4694](https://github.com/sveltejs/svelte/issues/4694))
* Preserve leading space with `preserveWhitespace: true` ([#4731](https://github.com/sveltejs/svelte/issues/4731))
* Preserve leading space in `<pre>` tags ([#6437](https://github.com/sveltejs/svelte/issues/6437))
* Improve error message when trying to use `style:` directives on inline components ([#7177](https://github.com/sveltejs/svelte/issues/7177))
* Add `FormData` as a known global ([#7199](https://github.com/sveltejs/svelte/pull/7199))
* Mark `css`/`instance`/`module` AST properties as optional in types ([#7204](https://github.com/sveltejs/svelte/pull/7204))
## 3.46.3
* Ignore whitespace in `{#each}` blocks when containing elements with `animate:` ([#5477](https://github.com/sveltejs/svelte/pull/5477))
* Throw compiler error when variable in `context="instance"` collides with import in `context="module"` ([#7090](https://github.com/sveltejs/svelte/issues/7090))
* Fix compiler crash when `{@const}` contains arrow functions ([#7134](https://github.com/sveltejs/svelte/issues/7134))
## 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 ## 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 non-boolean attribute rendering in SSR to render truthy values as-is ([#6121](https://github.com/sveltejs/svelte/issues/6121))

@ -0,0 +1,3 @@
This repository is governed by the Svelte Code of Conduct.
https://github.com/sveltejs/community/blob/main/CODE_OF_CONDUCT.md

705
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.45.0", "version": "3.46.4",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"module": "index.mjs", "module": "index.mjs",
"main": "index", "main": "index",
@ -86,10 +86,6 @@
"test": "mocha --exit", "test": "mocha --exit",
"test:unit": "mocha --require sucrase/register --recursive src/**/__test__.ts --exit", "test:unit": "mocha --require sucrase/register --recursive src/**/__test__.ts --exit",
"quicktest": "mocha", "quicktest": "mocha",
"precoverage": "c8 mocha",
"coverage": "c8 report --reporter=text-lcov > coverage.lcov && c8 report --reporter=html",
"codecov": "codecov",
"precodecov": "npm run coverage",
"build": "rollup -c && npm run tsd", "build": "rollup -c && npm run tsd",
"prepare": "npm run build", "prepare": "npm run build",
"dev": "rollup -cw", "dev": "rollup -cw",
@ -131,9 +127,7 @@
"@typescript-eslint/parser": "^4.31.2", "@typescript-eslint/parser": "^4.31.2",
"acorn": "^8.4.1", "acorn": "^8.4.1",
"agadoo": "^1.1.0", "agadoo": "^1.1.0",
"c8": "^5.0.1", "code-red": "^0.2.5",
"code-red": "^0.2.4",
"codecov": "^3.5.0",
"css-tree": "^1.1.2", "css-tree": "^1.1.2",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-plugin-import": "^2.24.2", "eslint-plugin-import": "^2.24.2",

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

@ -0,0 +1,106 @@
---
title: "What's new in Svelte: February 2022"
description: "Rapid-fire releases across Svelte, SvelteKit and the community"
author: Daniel Sandoval
authorURL: https://desandoval.net
---
Happy February, everyone! Over the last month or so, we've seen Svelte and SvelteKit [develop at rapid speed](accelerating-sveltes-development), new community rules across the [Reddit](https://www.reddit.com/r/sveltejs/comments/s9n8ou/new_rules/), [GitHub](https://github.com/sveltejs/community/blob/main/CODE_OF_CONDUCT.md) and [Discord](https://discord.com/channels/457912077277855764/831611707667382303/935264550436102315), and quite a few amazing apps, tutorials and libraries.
Let's take a look...
## Highlights from the Svelte changelog
- **3.45.0** brought a [new a11y warning `a11y-no-redundant-roles`](https://svelte.dev/docs#accessibility-warnings-a11y-no-redundant-roles), destructuring and caching fixes
- **3.46.0** added the much requested [`{@const}` tag](https://svelte.dev/docs#template-syntax-const) and [`style:` directive](https://svelte.dev/docs#template-syntax-element-directives-style-property)
- Check out **3.46.1 - 3.46.3** for fixes to the `{@const}` tag and `style:` directive, along with a number of fixes to animations
- [AST output is now available in the Svelte REPL](https://svelte.dev/repl/hello-world)
## What's new in SvelteKit
- `inlineStyleThreshold` allows you to specify where inline stylesheets are inserted into the page ([Docs](https://kit.svelte.dev/docs#configuration-inlinestylethreshold), [#2620](https://github.com/sveltejs/kit/pull/2620))
- `beforeNavigate`/`afterNavigate` lifecycle functions lets you add functionality before or after a page navigation ([Docs](https://kit.svelte.dev/docs#modules-$app-navigation), [#3293](https://github.com/sveltejs/kit/pull/3293))
- Platform context can now be passed from adapters ([Docs](https://kit.svelte.dev/docs#adapters-supported-environments-platform-specific-context), [#3429](https://github.com/sveltejs/kit/pull/3429))
- Hooks now have an `ssr` parameter in `resolve` to make it easier to skip SSR, when needed ([Docs](https://kit.svelte.dev/docs#hooks-handle), [#2804](https://github.com/sveltejs/kit/pull/2804))
- `$page.stuff` provides a mechanism for pages to pass data 'upward' to layouts ([Docs](https://kit.svelte.dev/docs#loading-input-stuff), [#3252](https://github.com/sveltejs/kit/pull/3252))
- Fallthrough routes let you specify where to route when an route can't be loaded ([Docs](https://kit.svelte.dev/docs#routing-advanced-fallthrough-routes), [#3217](https://github.com/sveltejs/kit/pull/3217))
**New configs**
- Content Security Policy (CSP) is now supported for increased security when using inline javascript or stylesheets ([Docs](https://kit.svelte.dev/docs#configuration-csp), [#3499](https://github.com/sveltejs/kit/pull/3499))
- `kit.routes` config allows you to customise public/private modules during build ([Docs](https://kit.svelte.dev/docs#configuration-routes), [#3576](https://github.com/sveltejs/kit/pull/3576))
- `prerender.createIndexFiles` config lets you prerender index.html files as their subfolder's name ([Docs](https://kit.svelte.dev/docs#configuration-prerender), [#2632](https://github.com/sveltejs/kit/pull/2632))
- HTTP methods can now be overridden using `kit.methodOverride` ([Docs](https://kit.svelte.dev/docs#routing-endpoints-http-method-overrides), [#2989](https://github.com/sveltejs/kit/pull/2989))
**Config changes**
- `config.kit.hydrate` and `config.kit.router` are now nested under `config.kit.browser` ([Docs](https://kit.svelte.dev/docs#configuration-browser), [3578](https://github.com/sveltejs/kit/pull/3578))
**Breaking change**
- use `Request` and `Response` objects in endpoints and hooks ([#3384](https://github.com/sveltejs/kit/pull/3384))
---
## Community Showcase
**Apps & Sites**
- [timb(re)](https://paullj.github.io/timb) is a live music programming environment
- [Music for Programming](https://musicforprogramming.net/latest/) is a series of mixes intended for listening while `${task}` to focus the brain and inspire the mind
- [Team Tale](https://teamtale.app/) allows two authors to write the same story in a tag-team sort of fashion
- [Puzzlez](https://www.puzzlez.io/) is an online place to play Sudoku and Wordle
- [Closed Caption Creator](https://www.closedcaptioncreator.com/) makes it easy to add subtitles to your video on Windows, Mac and Google Chrome
- [SC3Lab](https://sc3-lab.netlify.app/) is a code generator for experimenting with svelte-cubed and three.js
- [Donkeytype](https://github.com/0ql/Donkeytype) is a minimalistic and lightweight typingtest inspired by Monkeytype.
- [Above](https://above.silas.pro/) is a visual routine timer built for the ADHD/autistic mind
- [base.report](https://base.report/) is a modern research platform for serious investors
- [String](https://string.kampsy.xyz/) turns your Phone into a secure portable audio recorder, making it easy to capture and share personal notes, family moments, classroom lectures, and more
- [The Raytracer Challenge REPL](https://github.com/jakobwesthoff/the_raytracer_challenge_repl) provides a live editor interface to configure a raytraced scene and render it live in any modern browser
- [awesome-svelte-kit](https://github.com/janosh/awesome-svelte-kit) is a list of awesome examples of SvelteKit in the wild
- [Map Projection Explorer](https://www.geo-projections.com/) lets you explore different map projections and explains their differences
- [Rubiks](https://github.com/MeharGaur/rubiks) is a Rubik's Cube simulator
- [Pianisto](https://pianisto.net/) is a working piano made with SVG, ToneJS and a lot of patience
Want to work on a SvelteKit site with others, [try contributing to the Svelte Society site](https://github.com/svelte-society/sveltesociety-2021/issues)!
**Learning and Listening**
_To Read_
- [Accelerating Svelte's Development](https://svelte.dev/blog/accelerating-sveltes-development) by Ben McCann
- [Storybook for Vite](https://storybook.js.org/blog/storybook-for-vite/)
- [Let's learn SvelteKit by building a static Markdown blog from scratch](https://joshcollinsworth.com/blog/build-static-sveltekit-markdown-blog) by Josh Collinsworth
- [Building an iOS app with Svelte, Capacitor and Firebase](https://harryherskowitz.com/2022/01/05/tapedrop-app.html) by Harry Herskowitz
- [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) and [Workaround for Bubbling Custom Events in Svelte](https://dev.to/mohamadharith/workaround-for-bubbling-custom-events-in-svelte-3khk) by Mohamad Harith
- [How to build a full stack serverless application with Svelte and GraphQL](https://dev.to/shadid12/how-to-build-a-full-stack-serverless-application-with-svelte-graphql-and-fauna-5427) by Shadid Haque
- [How to Deploy SvelteKit Apps to Github Pages](https://sveltesaas.com/articles/sveltekit-github-pages-guide/)
- [Creating a dApp with SvelteKit](https://anthonyriley.org/2021/12/31/creating-a-dapp-with-sveltekit/) by Anthony Riley
- [Comparing Svelte Reactivity Options](https://opendirective.net/2022/01/06/comparing-svelte-reactivity-options/) by Steve Lee
_To Watch_
- [Integrating Storybook with SvelteKit](https://www.youtube.com/watch?v=Kc1ULlfyUcw) and [Integrating FaunaDB with Svelte](https://www.youtube.com/watch?v=zaoLZc76uZM) by the Svelte Sirens
- [SvelteKit Crash Course Tutorial](https://www.youtube.com/watch?v=9OlLxkaeVvw&list=PL4cUxeGkcC9hpM9ARM59Ve3jqcb54dqiP) by The Net Ninja
- [Svelte for Beginners](https://www.youtube.com/watch?v=BrkrOjknC_E&list=PLA9WiRZ-IS_ylnMYxIFCsZN6xVVSvLuHk) by Joy of Code
- [SvelteKit For Beginners | Movie App Tutorial](https://www.youtube.com/watch?v=ydR_M0fw9Xc) by Dev Ed
- [SvelteKit $app/stores](https://www.youtube.com/watch?v=gBPhr1xbgaQ) by lihautan
- [Sveltekit - Get All Routes/Pages](https://www.youtube.com/watch?v=Y_NE2R3HuOU) by WebJeda
_To Listen To_
- [New Year, New Svelte!?](https://share.transistor.fm/s/36212cdc) from Svelte Radio
- [So much Sveltey goodness (featuring Rich Harris)](https://changelog.com/jsparty/205) from JS Party
- [The Other Side of Tech: A Documentarian Perspective (with Stefan Kingham)](https://codingcat.dev/podcast/2-4-the-other-side-of-tech-a-documentarian-perspective) from Purrfect.dev
**Libraries, Tools & Components**
- [threlte](https://github.com/grischaerbe/threlte) is a three.js component library for Svelte
- [svelte-formify](https://github.com/nodify-at/svelte-formify) is a library to manage and validate forms that uses decorators to define validations
- [gQuery](https://github.com/leveluptuts/gQuery) is a GraphQL Fetcher & Cache for Svelte Kit
- [Unlock-protocol](https://github.com/novum-insights/sveltekit-unlock-firebase) is an integration to help login with MetaMask, Firebase, and paywall customers
- [AgnosticUI](https://github.com/AgnosticUI/agnosticui) is a set of UI primitives that start their lives in clean HTML and CSS
- [Vitebook](https://github.com/vitebook/vitebook) is a fast and lightweight alternative to Storybook that's powered by Vite
- [SwyxKit](https://swyxkit.netlify.app/) is an opinionated blog starter for SvelteKit + Tailwind + Netlify. Refreshed for 2022!
- [svelte-themes](https://github.com/beynar/svelte-themes) is an abstraction for themes in your SvelteKit app
- [svelte-transition](https://www.npmjs.com/package/svelte-transition) is a Svelte component to make using CSS class based transitions easier - ideally suited for use with TailwindCSS
- [Svelte Inview](https://www.npmjs.com/package/svelte-inview) is a Svelte action that monitors an element enters or leaves the viewport/parent element
- [svelte-inline-compile](https://github.com/DockYard/svelte-inline-compile) is a babel transform that allows for a much more pleasant experience when testing svelte components using Jest and `@testing-library/svelte`
- [@feltcoop/svelte-mutable-store](https://github.com/feltcoop/svelte-mutable-store) is a Svelte store for mutable values with an `immutable` compiler option
- [headless-svelte-ui](https://www.npmjs.com/package/@bojalelabs/headless-svelte-ui) is a group of headless components that can be used in building Svelte Apps.
Did we miss something? 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!

@ -57,7 +57,7 @@ In development mode (see the [compiler options](/docs#compile-time-svelte-compil
If you export a `const`, `class` or `function`, it is readonly from outside the component. Function *expressions* are valid props, however. If you export a `const`, `class` or `function`, it is readonly from outside the component. Function *expressions* are valid props, however.
Readonly props can be accessed as properties on the element, tied to the component using [`bind:this` syntax](/docs#template-syntax-element-directives-bind-element). Readonly props can be accessed as properties on the element, tied to the component using [`bind:this` syntax](/docs#template-syntax-component-directives-bind-this).
```sv ```sv
<script> <script>

@ -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. The `{@debug}` tag without any arguments will insert a `debugger` statement that gets triggered when *any* state changes, as opposed to the specified variables.
### {@const ...}
```sv
{@const assignment}
```
---
The `{@const ...}` tag defines a local constant.
```sv
<script>
export let boxes;
</script>
{#each boxes as box}
{@const area = box.width * box.height}
{box.width} * {box.height} = {area}
{/each}
```
`{@const}` is only allowed as direct child of `{#each}`, `{:then}`, `{:catch}`, `<Component />` or `<svelte:fragment />`.
### Element directives ### Element directives
@ -1670,6 +1693,7 @@ All except `scrollX` and `scrollY` are readonly.
<svelte:window bind:scrollY={y}/> <svelte:window bind:scrollY={y}/>
``` ```
> Note that the page will not be scrolled to the initial value to avoid accessibility issues. Only subsequent changes to the bound variable of `scrollX` and `scrollY` will cause scrolling. However, if the scrolling behaviour is desired, call `scrollTo()` in `onMount()`.
### `<svelte:body>` ### `<svelte:body>`
@ -1734,7 +1758,7 @@ The `<svelte:options>` element provides a place to specify per-component compile
### `<svelte:fragment>` ### `<svelte:fragment>`
The `<svelte:fragment>` element allows you to place content in a [named slot](/docs#template-syntax-slot-slot-name) without wrapping it in a container DOM element. This keeps the flow layout of your document intact. The `<svelte:fragment>` element allows you to place content in a [named slot](/docs#template-syntax-slot-slot-name-name) without wrapping it in a container DOM element. This keeps the flow layout of your document intact.
```sv ```sv
<!-- Widget.svelte --> <!-- Widget.svelte -->

@ -226,7 +226,7 @@ dispatch: ((name: string, detail?: any) => void) = createEventDispatcher();
--- ---
Creates an event dispatcher that can be used to dispatch [component events](/docs#template-syntax-component-directives-on-component-event). Event dispatchers are functions that can take two arguments: `name` and `detail`. Creates an event dispatcher that can be used to dispatch [component events](/docs#template-syntax-component-directives-on-eventname). Event dispatchers are functions that can take two arguments: `name` and `detail`.
Component events created with `createEventDispatcher` create a [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent). These events do not [bubble](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_bubbling_and_capture) and are not cancellable with `event.preventDefault()`. The `detail` argument corresponds to the [CustomEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail) property and can contain any type of data. Component events created with `createEventDispatcher` create a [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent). These events do not [bubble](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_bubbling_and_capture) and are not cancellable with `event.preventDefault()`. The `detail` argument corresponds to the [CustomEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail) property and can contain any type of data.

@ -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 ```js

@ -213,6 +213,20 @@ Enforce that `on:mouseover` and `on:mouseout` are accompanied by `on:focus` and
--- ---
### `a11y-no-redundant-roles`
Some HTML elements have default ARIA roles. Giving these elements an ARIA role that is already set by the browser [has no effect](https://www.w3.org/TR/using-aria/#aria-does-nothing) and is redundant.
```sv
<!-- A11y: Redundant role 'button' -->
<button role="button" />
<!-- A11y: Redundant role 'img' -->
<img role="img" src="foo.jpg" />
```
---
### `a11y-positive-tabindex` ### `a11y-positive-tabindex`
Avoid positive `tabindex` property values. This will move elements out of the expected tab order, creating a confusing experience for keyboard users. Avoid positive `tabindex` property values. This will move elements out of the expected tab order, creating a confusing experience for keyboard users.

@ -4,14 +4,14 @@
import Decrementer from './Decrementer.svelte'; import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte'; import Resetter from './Resetter.svelte';
let count_value; let countValue;
const unsubscribe = count.subscribe(value => { const unsubscribe = count.subscribe(value => {
count_value = value; countValue = value;
}); });
</script> </script>
<h1>The count is {count_value}</h1> <h1>The count is {countValue}</h1>
<Incrementer/> <Incrementer/>
<Decrementer/> <Decrementer/>

@ -7,7 +7,7 @@
<svelte:window bind:scrollY={y}/> <svelte:window bind:scrollY={y}/>
<a class="parallax-container" href="https://www.firewatchgame.com"> <a class="parallax-container" href="https://www.firewatchgame.com">
{#each [0, 1, 2, 3, 4, 5, 6, 7, 8] as layer} {#each layers as layer}
<img <img
style="transform: translate(0,{-y * layer / (layers.length - 1)}px)" style="transform: translate(0,{-y * layer / (layers.length - 1)}px)"
src="https://www.firewatchgame.com/images/parallax/parallax{layer}.png" src="https://www.firewatchgame.com/images/parallax/parallax{layer}.png"

@ -4,7 +4,7 @@ question: How do I document my components?
In editors which use the Svelte Language Server you can document Components, functions and exports using specially formatted comments. In editors which use the Svelte Language Server you can document Components, functions and exports using specially formatted comments.
````svelte ````sv
<script> <script>
/** What should we call the user? */ /** What should we call the user? */
export let name = 'world'; export let name = 'world';

@ -6,14 +6,14 @@ You need to install a preprocessor such as [svelte-preprocess](https://github.co
To declare the type of a reactive variable in a Svelte template, you should use the following syntax: To declare the type of a reactive variable in a Svelte template, you should use the following syntax:
``` ```ts
let x: number; let x: number;
$: x = count + 1; $: x = count + 1;
``` ```
To import a type or interface make sure to use [TypeScript's `type` modifier](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export): To import a type or interface make sure to use [TypeScript's `type` modifier](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export):
``` ```ts
import type { SomeInterface } from './SomeFile'; import type { SomeInterface } from './SomeFile';
``` ```

@ -4,14 +4,14 @@
import Decrementer from './Decrementer.svelte'; import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte'; import Resetter from './Resetter.svelte';
let count_value; let countValue;
count.subscribe(value => { count.subscribe(value => {
count_value = value; countValue = value;
}); });
</script> </script>
<h1>The count is {count_value}</h1> <h1>The count is {countValue}</h1>
<Incrementer/> <Incrementer/>
<Decrementer/> <Decrementer/>

@ -4,14 +4,14 @@
import Decrementer from './Decrementer.svelte'; import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte'; import Resetter from './Resetter.svelte';
let count_value; let countValue;
count.subscribe(value => { count.subscribe(value => {
count_value = value; countValue = value;
}); });
</script> </script>
<h1>The count is {count_value}</h1> <h1>The count is {countValue}</h1>
<Incrementer/> <Incrementer/>
<Decrementer/> <Decrementer/>

@ -4,7 +4,7 @@ title: Writable stores
Not all application state belongs inside your application's component hierarchy. Sometimes, you'll have values that need to be accessed by multiple unrelated components, or by a regular JavaScript module. Not all application state belongs inside your application's component hierarchy. Sometimes, you'll have values that need to be accessed by multiple unrelated components, or by a regular JavaScript module.
In Svelte, we do this with *stores*. A store is simply an object with a `subscribe` method that allows interested parties to be notified whenever the store value changes. In `App.svelte`, `count` is a store, and we're setting `count_value` in the `count.subscribe` callback. In Svelte, we do this with *stores*. A store is simply an object with a `subscribe` method that allows interested parties to be notified whenever the store value changes. In `App.svelte`, `count` is a store, and we're setting `countValue` in the `count.subscribe` callback.
Click the `stores.js` tab to see the definition of `count`. It's a *writable* store, which means it has `set` and `update` methods in addition to `subscribe`. Click the `stores.js` tab to see the definition of `count`. It's a *writable* store, which means it has `set` and `update` methods in addition to `subscribe`.

@ -4,14 +4,14 @@
import Decrementer from './Decrementer.svelte'; import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte'; import Resetter from './Resetter.svelte';
let count_value; let countValue;
count.subscribe(value => { count.subscribe(value => {
count_value = value; countValue = value;
}); });
</script> </script>
<h1>The count is {count_value}</h1> <h1>The count is {countValue}</h1>
<Incrementer/> <Incrementer/>
<Decrementer/> <Decrementer/>

@ -8,7 +8,7 @@ Start by declaring `unsubscribe` in `App.svelte`:
```js ```js
const unsubscribe = count.subscribe(value => { const unsubscribe = count.subscribe(value => {
count_value = value; countValue = value;
}); });
``` ```
> Calling a `subscribe` method returns an `unsubscribe` function. > Calling a `subscribe` method returns an `unsubscribe` function.
@ -23,16 +23,16 @@ You now declared `unsubscribe`, but it still needs to be called, for example thr
import Decrementer from './Decrementer.svelte'; import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte'; import Resetter from './Resetter.svelte';
let count_value; let countValue;
const unsubscribe = count.subscribe(value => { const unsubscribe = count.subscribe(value => {
count_value = value; countValue = value;
}); });
onDestroy(unsubscribe); onDestroy(unsubscribe);
</script> </script>
<h1>The count is {count_value}</h1> <h1>The count is {countValue}</h1>
``` ```
It starts to get a bit boilerplatey though, especially if your component subscribes to multiple stores. Instead, Svelte has a trick up its sleeve — you can reference a store value by prefixing the store name with `$`: It starts to get a bit boilerplatey though, especially if your component subscribes to multiple stores. Instead, Svelte has a trick up its sleeve — you can reference a store value by prefixing the store name with `$`:

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

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

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

@ -191,27 +191,33 @@ export default class Component {
this.stylesheet.warn_on_unused_selectors(this); this.stylesheet.warn_on_unused_selectors(this);
} }
add_var(variable: Var, add_to_lookup = true) { add_var(node: Node, variable: Var, add_to_lookup = true) {
this.vars.push(variable); this.vars.push(variable);
if (add_to_lookup) { if (add_to_lookup) {
if (this.var_lookup.has(variable.name)) {
const exists_var = this.var_lookup.get(variable.name);
if (exists_var.module && exists_var.imported) {
this.error(node as any, compiler_errors.illegal_variable_declaration);
}
}
this.var_lookup.set(variable.name, variable); this.var_lookup.set(variable.name, variable);
} }
} }
add_reference(name: string) { add_reference(node: Node, name: string) {
const variable = this.var_lookup.get(name); const variable = this.var_lookup.get(name);
if (variable) { if (variable) {
variable.referenced = true; variable.referenced = true;
} else if (is_reserved_keyword(name)) { } else if (is_reserved_keyword(name)) {
this.add_var({ this.add_var(node, {
name, name,
injected: true, injected: true,
referenced: true referenced: true
}); });
} else if (name[0] === '$') { } else if (name[0] === '$') {
this.add_var({ this.add_var(node, {
name, name,
injected: true, injected: true,
referenced: true, referenced: true,
@ -228,7 +234,7 @@ export default class Component {
} }
} else { } else {
if (this.compile_options.varsReport === 'full') { if (this.compile_options.varsReport === 'full') {
this.add_var({ name, referenced: true }, false); this.add_var(node, { name, referenced: true }, false);
} }
this.used_names.add(name); this.used_names.add(name);
@ -599,12 +605,14 @@ export default class Component {
} }
const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let'); const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let');
const imported = node.type.startsWith('Import');
this.add_var({ this.add_var(node, {
name, name,
module: true, module: true,
hoistable: true, hoistable: true,
writable writable,
imported
}); });
}); });
@ -612,7 +620,7 @@ export default class Component {
if (name[0] === '$') { if (name[0] === '$') {
return this.error(node as any, compiler_errors.illegal_subscription); return this.error(node as any, compiler_errors.illegal_subscription);
} else { } else {
this.add_var({ this.add_var(node, {
name, name,
global: true, global: true,
hoistable: true hoistable: true
@ -674,7 +682,7 @@ export default class Component {
const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let'); const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let');
const imported = node.type.startsWith('Import'); const imported = node.type.startsWith('Import');
this.add_var({ this.add_var(node, {
name, name,
initialised: instance_scope.initialised_declarations.has(name), initialised: instance_scope.initialised_declarations.has(name),
writable, writable,
@ -697,7 +705,7 @@ export default class Component {
const node = globals.get(name); const node = globals.get(name);
if (this.injected_reactive_declaration_vars.has(name)) { if (this.injected_reactive_declaration_vars.has(name)) {
this.add_var({ this.add_var(node, {
name, name,
injected: true, injected: true,
writable: true, writable: true,
@ -705,7 +713,7 @@ export default class Component {
initialised: true initialised: true
}); });
} else if (is_reserved_keyword(name)) { } else if (is_reserved_keyword(name)) {
this.add_var({ this.add_var(node, {
name, name,
injected: true injected: true
}); });
@ -714,14 +722,14 @@ export default class Component {
return this.error(node as any, compiler_errors.illegal_global(name)); return this.error(node as any, compiler_errors.illegal_global(name));
} }
this.add_var({ this.add_var(node, {
name, name,
injected: true, injected: true,
mutated: true, mutated: true,
writable: true writable: true
}); });
this.add_reference(name.slice(1)); this.add_reference(node, name.slice(1));
const variable = this.var_lookup.get(name.slice(1)); const variable = this.var_lookup.get(name.slice(1));
if (variable) { if (variable) {
@ -729,7 +737,7 @@ export default class Component {
variable.referenced_from_script = true; variable.referenced_from_script = true;
} }
} else { } else {
this.add_var({ this.add_var(node, {
name, name,
global: true, global: true,
hoistable: true hoistable: true

@ -40,6 +40,10 @@ export default {
code: 'invalid-binding', code: 'invalid-binding',
message: 'Cannot bind to a variable declared with {#await ... then} or {:catch} blocks' message: 'Cannot bind to a variable declared with {#await ... then} or {:catch} blocks'
}, },
invalid_binding_const: {
code: 'invalid-binding',
message: 'Cannot bind to a variable declared with {@const ...}'
},
invalid_binding_writibale: { invalid_binding_writibale: {
code: 'invalid-binding', code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable' message: 'Cannot bind to a variable which is not writable'
@ -186,6 +190,10 @@ export default {
code: 'illegal-global', code: 'illegal-global',
message: `${name} is an illegal variable name` message: `${name} is an illegal variable name`
}), }),
illegal_variable_declaration: {
code: 'illegal-variable-declaration',
message: 'Cannot declare same variable name which is imported inside <script context="module">'
},
cyclical_reactive_declaration: (cycle: string[]) => ({ cyclical_reactive_declaration: (cycle: string[]) => ({
code: 'cyclical-reactive-declaration', code: 'cyclical-reactive-declaration',
message: `Cyclical dependency detected: ${cycle.join(' → ')}` message: `Cyclical dependency detected: ${cycle.join(' → ')}`
@ -245,5 +253,25 @@ export default {
invalid_directive_value: { invalid_directive_value: {
code: '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]`)' message: 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
},
invalid_const_placement: {
code: 'invalid-const-placement',
message: '{@const} must be the immediate child of {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>'
},
invalid_const_declaration: (name: string) => ({
code: 'invalid-const-declaration',
message: `'${name}' has already been declared`
}),
invalid_const_update: (name: string) => ({
code: 'invalid-const-update',
message: `'${name}' is declared using {@const ...} and is read-only`
}),
cyclical_const_tags: (cycle: string[]) => ({
code: 'cyclical-const-tags',
message: `Cyclical dependency detected: ${cycle.join(' → ')}`
}),
invalid_component_style_directive: {
code: 'invalid-component-style-directive',
message: 'Style directives cannot be used on components'
} }
}; };

@ -8,6 +8,7 @@ import { CssNode } from './interfaces';
import hash from '../utils/hash'; import hash from '../utils/hash';
import compiler_warnings from '../compiler_warnings'; import compiler_warnings from '../compiler_warnings';
import { extract_ignores_above_position } from '../../utils/extract_svelte_ignore'; import { extract_ignores_above_position } from '../../utils/extract_svelte_ignore';
import { push_array } from '../../utils/push_array';
function remove_css_prefix(name: string): string { function remove_css_prefix(name: string): string {
return name.replace(/^-((webkit)|(moz)|(o)|(ms))-/, ''); return name.replace(/^-((webkit)|(moz)|(o)|(ms))-/, '');
@ -351,7 +352,7 @@ export default class Stylesheet {
const at_rule_declarations = node.block.children const at_rule_declarations = node.block.children
.filter(node => node.type === 'Declaration') .filter(node => node.type === 'Declaration')
.map(node => new Declaration(node)); .map(node => new Declaration(node));
atrule.declarations.push(...at_rule_declarations); push_array(atrule.declarations, at_rule_declarations);
} }
current_atrule = atrule; current_atrule = atrule;

@ -18,7 +18,7 @@ export default class Action extends Node {
component.warn_if_undefined(object, info, scope); component.warn_if_undefined(object, info, scope);
this.name = info.name; this.name = info.name;
component.add_reference(object); component.add_reference(this as any, object);
this.expression = info.expression this.expression = info.expression
? new Expression(component, this, scope, info.expression) ? new Expression(component, this, scope, info.expression)

@ -18,7 +18,7 @@ export default class Animation extends Node {
component.warn_if_undefined(info.name, info, scope); component.warn_if_undefined(info.name, info, scope);
this.name = info.name; this.name = info.name;
component.add_reference(info.name.split('.')[0]); component.add_reference(this as any, info.name.split('.')[0]);
if (parent.animation) { if (parent.animation) {
component.error(this, compiler_errors.duplicate_animation); component.error(this, compiler_errors.duplicate_animation);

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

@ -1,13 +1,15 @@
import map_children from './shared/map_children';
import TemplateScope from './shared/TemplateScope'; import TemplateScope from './shared/TemplateScope';
import AbstractBlock from './shared/AbstractBlock'; import AbstractBlock from './shared/AbstractBlock';
import AwaitBlock from './AwaitBlock'; import AwaitBlock from './AwaitBlock';
import Component from '../Component'; import Component from '../Component';
import { TemplateNode } from '../../interfaces'; import { TemplateNode } from '../../interfaces';
import get_const_tags from './shared/get_const_tags';
import ConstTag from './ConstTag';
export default class CatchBlock extends AbstractBlock { export default class CatchBlock extends AbstractBlock {
type: 'CatchBlock'; type: 'CatchBlock';
scope: TemplateScope; scope: TemplateScope;
const_tags: ConstTag[];
constructor(component: Component, parent: AwaitBlock, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: AwaitBlock, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); 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.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) { if (!info.skip) {
this.warn_if_empty_block(); this.warn_if_empty_block();

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

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

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

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

@ -29,7 +29,7 @@ export default class InlineComponent extends Node {
if (info.name !== 'svelte:component' && info.name !== 'svelte:self') { if (info.name !== 'svelte:component' && info.name !== 'svelte:self') {
const name = info.name.split('.')[0]; // accommodate namespaces const name = info.name.split('.')[0]; // accommodate namespaces
component.warn_if_undefined(name, info, scope); component.warn_if_undefined(name, info, scope);
component.add_reference(name); component.add_reference(this as any, name);
} }
this.name = info.name; this.name = info.name;
@ -72,6 +72,9 @@ export default class InlineComponent extends Node {
case 'Transition': case 'Transition':
return component.error(node, compiler_errors.invalid_transition); return component.error(node, compiler_errors.invalid_transition);
case 'StyleDirective':
return component.error(node, compiler_errors.invalid_component_style_directive);
default: default:
throw new Error(`Not implemented: ${node.type}`); throw new Error(`Not implemented: ${node.type}`);
} }
@ -126,6 +129,14 @@ export default class InlineComponent extends Node {
slot_template.attributes.push(attribute); 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); children.push(slot_template);
info.children.splice(i, 1); info.children.splice(i, 1);

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

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

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

@ -43,4 +43,21 @@ export default class Text extends Node {
return parent_element.namespace || elements_without_text.has(parent_element.name); return parent_element.namespace || elements_without_text.has(parent_element.name);
} }
keep_space(): boolean {
if (this.component.component_options.preserveWhitespace) return true;
return this.within_pre();
}
within_pre(): boolean {
let node = this.parent;
while (node) {
if (node.type === 'Element' && node.name === 'pre') {
return true;
}
node = node.parent;
}
return false;
}
} }

@ -1,13 +1,15 @@
import map_children from './shared/map_children';
import TemplateScope from './shared/TemplateScope'; import TemplateScope from './shared/TemplateScope';
import AbstractBlock from './shared/AbstractBlock'; import AbstractBlock from './shared/AbstractBlock';
import AwaitBlock from './AwaitBlock'; import AwaitBlock from './AwaitBlock';
import Component from '../Component'; import Component from '../Component';
import { TemplateNode } from '../../interfaces'; import { TemplateNode } from '../../interfaces';
import get_const_tags from './shared/get_const_tags';
import ConstTag from './ConstTag';
export default class ThenBlock extends AbstractBlock { export default class ThenBlock extends AbstractBlock {
type: 'ThenBlock'; type: 'ThenBlock';
scope: TemplateScope; scope: TemplateScope;
const_tags: ConstTag[];
constructor(component: Component, parent: AwaitBlock, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: AwaitBlock, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); 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.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) { if (!info.skip) {
this.warn_if_empty_block(); this.warn_if_empty_block();

@ -19,7 +19,7 @@ export default class Transition extends Node {
component.warn_if_undefined(info.name, info, scope); component.warn_if_undefined(info.name, info, scope);
this.name = info.name; this.name = info.name;
component.add_reference(info.name.split('.')[0]); component.add_reference(this as any, info.name.split('.')[0]);
this.directive = info.intro && info.outro ? 'transition' : info.intro ? 'in' : 'out'; this.directive = info.intro && info.outro ? 'transition' : info.intro ? 'in' : 'out';
this.is_local = info.modifiers.includes('local'); this.is_local = info.modifiers.includes('local');

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

@ -187,7 +187,7 @@ function mark_referenced(
if (is_reference(node, parent)) { if (is_reference(node, parent)) {
const { name } = flatten_reference(node); const { name } = flatten_reference(node);
if (!scope.is_let(name) && !scope.names.has(name)) { if (!scope.is_let(name) && !scope.names.has(name)) {
component.add_reference(name); component.add_reference(node, name);
} }
} }
} }

@ -110,7 +110,7 @@ export default class Expression {
dependencies.add(name); dependencies.add(name);
} }
component.add_reference(name); component.add_reference(node, name);
component.warn_if_undefined(name, nodes[0], template_scope); component.warn_if_undefined(name, nodes[0], template_scope);
} }
@ -133,6 +133,10 @@ export default class Expression {
if (names) { if (names) {
names.forEach(name => { names.forEach(name => {
if (template_scope.names.has(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 => { template_scope.dependencies_for_name.get(name).forEach(name => {
const variable = component.var_lookup.get(name); const variable = component.var_lookup.get(name);
if (variable) variable[deep ? 'mutated' : 'reassigned'] = true; if (variable) variable[deep ? 'mutated' : 'reassigned'] = true;
@ -140,7 +144,7 @@ export default class Expression {
const each_block = template_scope.get_owner(name); const each_block = template_scope.get_owner(name);
(each_block as EachBlock).has_binding = true; (each_block as EachBlock).has_binding = true;
} else { } else {
component.add_reference(name); component.add_reference(node, name);
const variable = component.var_lookup.get(name); const variable = component.var_lookup.get(name);
if (variable) variable[deep ? 'mutated' : 'reassigned'] = true; if (variable) variable[deep ? 'mutated' : 'reassigned'] = true;
@ -172,7 +176,7 @@ export default class Expression {
} }
// TODO move this into a render-dom wrapper? // 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 // TODO ideally we wouldn't end up calling this method
// multiple times // multiple times
if (this.manipulated) return this.manipulated; if (this.manipulated) return this.manipulated;
@ -216,10 +220,10 @@ export default class Expression {
}); });
} else { } else {
dependencies.add(name); dependencies.add(name);
component.add_reference(name); // TODO is this redundant/misplaced? component.add_reference(node, name); // TODO is this redundant/misplaced?
} }
} else if (is_contextual(component, template_scope, name)) { } else if (is_contextual(component, template_scope, name)) {
const reference = block.renderer.reference(node); const reference = block.renderer.reference(node, ctx);
this.replace(reference); this.replace(reference);
} }
@ -249,13 +253,21 @@ export default class Expression {
const declaration = b`const ${id} = ${node}`; const declaration = b`const ${id} = ${node}`;
if (dependencies.size === 0 && contextual_dependencies.size === 0) { if (owner.type === 'ConstTag') {
walk(node, {
enter(node: Node) {
if (node.type === 'Identifier') {
this.replace(block.renderer.reference(node, ctx));
}
}
});
} else if (dependencies.size === 0 && contextual_dependencies.size === 0) {
// we can hoist this out of the component completely // we can hoist this out of the component completely
component.fully_hoisted.push(declaration); component.fully_hoisted.push(declaration);
this.replace(id as any); this.replace(id as any);
component.add_var({ component.add_var(node, {
name: id.name, name: id.name,
internal: true, internal: true,
hoistable: true, hoistable: true,

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

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

@ -1,5 +1,6 @@
import AwaitBlock from '../AwaitBlock'; import AwaitBlock from '../AwaitBlock';
import Body from '../Body'; import Body from '../Body';
import ConstTag from '../ConstTag';
import Comment from '../Comment'; import Comment from '../Comment';
import EachBlock from '../EachBlock'; import EachBlock from '../EachBlock';
import Element from '../Element'; import Element from '../Element';
@ -17,6 +18,7 @@ import Text from '../Text';
import Title from '../Title'; import Title from '../Title';
import Window from '../Window'; import Window from '../Window';
import { TemplateNode } from '../../../interfaces'; import { TemplateNode } from '../../../interfaces';
import { push_array } from '../../../utils/push_array';
export type Children = ReturnType<typeof map_children>; export type Children = ReturnType<typeof map_children>;
@ -25,6 +27,7 @@ function get_constructor(type) {
case 'AwaitBlock': return AwaitBlock; case 'AwaitBlock': return AwaitBlock;
case 'Body': return Body; case 'Body': return Body;
case 'Comment': return Comment; case 'Comment': return Comment;
case 'ConstTag': return ConstTag;
case 'EachBlock': return EachBlock; case 'EachBlock': return EachBlock;
case 'Element': return Element; case 'Element': return Element;
case 'Head': return Head; case 'Head': return Head;
@ -58,7 +61,7 @@ export default function map_children(component, parent, scope, children: Templat
if (use_ignores) component.pop_ignores(), ignores = []; if (use_ignores) component.pop_ignores(), ignores = [];
if (node.type === 'Comment' && node.ignores.length) { if (node.type === 'Comment' && node.ignores.length) {
ignores.push(...node.ignores); push_array(ignores, node.ignores);
} }
if (last) last.next = node; if (last) last.next = node;

@ -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') { if (typeof node === 'string') {
node = { type: 'Identifier', name: node }; node = { type: 'Identifier', name: node };
} }
@ -264,11 +264,11 @@ export default class Renderer {
// TODO is this correct? // TODO is this correct?
if (this.component.var_lookup.get(name)) { if (this.component.var_lookup.get(name)) {
this.component.add_reference(name); this.component.add_reference(node, name);
} }
if (member !== undefined) { 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; if (nodes[0].loc) replacement.object.loc = nodes[0].loc;
nodes[0] = replacement; nodes[0] = replacement;

@ -11,6 +11,7 @@ import { apply_preprocessor_sourcemap } from '../../utils/mapped_code';
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types'; import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
import { flatten } from '../../utils/flatten'; import { flatten } from '../../utils/flatten';
import check_enable_sourcemap from '../utils/check_enable_sourcemap'; import check_enable_sourcemap from '../utils/check_enable_sourcemap';
import { push_array } from '../../utils/push_array';
export default function dom( export default function dom(
component: Component, component: Component,
@ -67,7 +68,7 @@ export default function dom(
// TODO the deconflicted names of blocks are reversed... should set them here // TODO the deconflicted names of blocks are reversed... should set them here
const blocks = renderer.blocks.slice().reverse(); const blocks = renderer.blocks.slice().reverse();
body.push(...blocks.map(block => { push_array(body, blocks.map(block => {
// TODO this is a horrible mess — renderer.blocks // TODO this is a horrible mess — renderer.blocks
// contains a mixture of Blocks and Nodes // contains a mixture of Blocks and Nodes
if ((block as Block).render) return (block as Block).render(); if ((block as Block).render) return (block as Block).render();
@ -562,7 +563,7 @@ export default function dom(
}); });
} }
declaration.body.body.push(...accessors); push_array(declaration.body.body, accessors);
body.push(declaration); body.push(declaration);
@ -599,7 +600,7 @@ export default function dom(
} }
`[0] as ClassDeclaration; `[0] as ClassDeclaration;
declaration.body.body.push(...accessors); push_array(declaration.body.body, accessors);
body.push(declaration); body.push(declaration);
} }

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

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

@ -27,6 +27,7 @@ import MustacheTagWrapper from '../MustacheTag';
import RawMustacheTagWrapper from '../RawMustacheTag'; import RawMustacheTagWrapper from '../RawMustacheTag';
import is_dynamic from '../shared/is_dynamic'; import is_dynamic from '../shared/is_dynamic';
import create_debugging_comment from '../shared/create_debugging_comment'; import create_debugging_comment from '../shared/create_debugging_comment';
import { push_array } from '../../../../utils/push_array';
interface BindingGroup { interface BindingGroup {
events: string[]; events: string[];
@ -223,7 +224,7 @@ export default class ElementWrapper extends Wrapper {
block.add_dependencies(node.tag_expr.dependencies); block.add_dependencies(node.tag_expr.dependencies);
// add directive and handler dependencies // 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) { if (directive && directive.expression) {
block.add_dependencies(directive.expression.dependencies); block.add_dependencies(directive.expression.dependencies);
} }
@ -707,7 +708,7 @@ export default class ElementWrapper extends Wrapper {
this.attributes.forEach((attribute) => { this.attributes.forEach((attribute) => {
if (attribute.node.name === 'class') { if (attribute.node.name === 'class') {
const dependencies = attribute.node.get_dependencies(); const dependencies = attribute.node.get_dependencies();
this.class_dependencies.push(...dependencies); push_array(this.class_dependencies, dependencies);
} }
}); });
@ -1033,7 +1034,7 @@ export default class ElementWrapper extends Wrapper {
const snippet = expression.manipulate(block); const snippet = expression.manipulate(block);
let cached_snippet; let cached_snippet;
if (should_cache) { if (should_cache) {
cached_snippet = block.get_unique_name(`style_${name}`); cached_snippet = block.get_unique_name(`style_${name.replace(/-/g, '_')}`);
block.add_variable(cached_snippet, snippet); block.add_variable(cached_snippet, snippet);
} }

@ -95,7 +95,7 @@ export default class FragmentWrapper {
next_sibling ? (next_sibling.node.type === 'Text' && /^\s/.test(next_sibling.node.data) && trimmable_at(child, next_sibling)) : !child.has_ancestor('EachBlock') next_sibling ? (next_sibling.node.type === 'Text' && /^\s/.test(next_sibling.node.data) && trimmable_at(child, next_sibling)) : !child.has_ancestor('EachBlock')
); );
if (should_trim) { if (should_trim && !child.keep_space()) {
data = trim_end(data); data = trim_end(data);
if (!data) continue; if (!data) continue;
} }
@ -127,7 +127,7 @@ export default class FragmentWrapper {
if (strip_whitespace) { if (strip_whitespace) {
const first = this.nodes[0] as Text; const first = this.nodes[0] as Text;
if (first && first.node.type === 'Text') { if (first && first.node.type === 'Text' && !first.node.keep_space()) {
first.data = trim_start(first.data); first.data = trim_start(first.data);
if (!first.data) { if (!first.data) {
first.var = null; first.var = null;

@ -10,6 +10,7 @@ import { b, x } from 'code-red';
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import { is_head } from './shared/is_head'; import { is_head } from './shared/is_head';
import { Identifier, Node } from 'estree'; import { Identifier, Node } from 'estree';
import { push_array } from '../../../utils/push_array';
function is_else_if(node: ElseBlock) { function is_else_if(node: ElseBlock) {
return ( return (
@ -166,7 +167,7 @@ export default class IfBlockWrapper extends Wrapper {
block.has_outro_method = has_outros; block.has_outro_method = has_outros;
}); });
renderer.blocks.push(...blocks); push_array(renderer.blocks, blocks);
} }
render( render(

@ -4,23 +4,16 @@ import Block from '../Block';
import FragmentWrapper from './Fragment'; import FragmentWrapper from './Fragment';
import create_debugging_comment from './shared/create_debugging_comment'; import create_debugging_comment from './shared/create_debugging_comment';
import { get_slot_definition } from './shared/get_slot_definition'; 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 { sanitize } from '../../../utils/names';
import { Identifier } from 'estree'; import { Identifier } from 'estree';
import InlineComponentWrapper from './InlineComponent'; import InlineComponentWrapper from './InlineComponent';
import { extract_names } from 'periscopic'; import { extract_names } from 'periscopic';
import { INode } from '../../nodes/interfaces'; import SlotTemplate from '../../nodes/SlotTemplate';
import Let from '../../nodes/Let'; import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
import TemplateScope from '../../nodes/shared/TemplateScope';
type NodeWithLets = INode & {
scope: TemplateScope;
lets: Let[];
slot_template_name: string;
};
export default class SlotTemplateWrapper extends Wrapper { export default class SlotTemplateWrapper extends Wrapper {
node: NodeWithLets; node: SlotTemplate;
fragment: FragmentWrapper; fragment: FragmentWrapper;
block: Block; block: Block;
parent: InlineComponentWrapper; parent: InlineComponentWrapper;
@ -29,13 +22,13 @@ export default class SlotTemplateWrapper extends Wrapper {
renderer: Renderer, renderer: Renderer,
block: Block, block: Block,
parent: Wrapper, parent: Wrapper,
node: NodeWithLets, node: SlotTemplate,
strip_whitespace: boolean, strip_whitespace: boolean,
next_sibling: Wrapper next_sibling: Wrapper
) { ) {
super(renderer, block, parent, node); 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 => { lets.forEach(l => {
extract_names(l.value || l.name).forEach(name => { 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({ this.block = block.child({
comment: create_debugging_comment(this.node, this.renderer.component), comment: create_debugging_comment(this.node, this.renderer.component),
name: this.renderer.component.get_unique_name( name: this.renderer.component.get_unique_name(
@ -76,5 +71,21 @@ export default class SlotTemplateWrapper extends Wrapper {
render() { render() {
this.fragment.render(this.block, null, x`#nodes` as Identifier); 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)`);
}
} }
} }

@ -29,15 +29,7 @@ export default class TextWrapper extends Wrapper {
if (this.renderer.component.component_options.preserveWhitespace) return false; if (this.renderer.component.component_options.preserveWhitespace) return false;
if (/[\S\u00A0]/.test(this.data)) return false; if (/[\S\u00A0]/.test(this.data)) return false;
let node = this.parent && this.parent.node; return !this.node.within_pre();
while (node) {
if (node.type === 'Element' && node.name === 'pre') {
return false;
}
node = node.parent;
}
return true;
} }
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {

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

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

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

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

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

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

@ -26,7 +26,7 @@ export default function remove_whitespace_children(children: INode[], next?: INo
trimmable_at(child, next) trimmable_at(child, next)
: !child.has_ancestor('EachBlock'); : !child.has_ancestor('EachBlock');
if (should_trim) { if (should_trim && !child.keep_space()) {
data = trim_end(data); data = trim_end(data);
if (!data) continue; if (!data) continue;
} }
@ -47,7 +47,7 @@ export default function remove_whitespace_children(children: INode[], next?: INo
} }
const first = nodes[0]; const first = nodes[0];
if (first && first.type === 'Text') { if (first && first.type === 'Text' && !first.keep_space()) {
first.data = trim_start(first.data); first.data = trim_start(first.data);
if (!first.data) { if (!first.data) {
first.var = null; first.var = null;

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

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

@ -375,6 +375,28 @@ export default function mustache(parser: Parser) {
type: 'DebugTag', type: 'DebugTag',
identifiers 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 { } else {
const expression = read_expression(parser); const expression = read_expression(parser);

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

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

@ -2,6 +2,7 @@ import { DecodedSourceMap, RawSourceMap, SourceMapLoader } from '@ampproject/rem
import remapping from '@ampproject/remapping'; import remapping from '@ampproject/remapping';
import { SourceMap } from 'magic-string'; import { SourceMap } from 'magic-string';
import { Source, Processed } from '../preprocess/types'; import { Source, Processed } from '../preprocess/types';
import { push_array } from './push_array';
export type SourceLocation = { export type SourceLocation = {
line: number; line: number;
@ -60,14 +61,6 @@ function merge_tables<T>(this_table: T[], other_table: T[]): [T[], number[], boo
return [new_table, idx_map, val_changed, idx_changed]; return [new_table, idx_map, val_changed, idx_changed];
} }
function pushArray<T>(_this: T[], other: T[]) {
// We use push to mutate in place for memory and perf reasons
// We use the for loop instead of _this.push(...other) to avoid the JS engine's function argument limit (65,535 in JavascriptCore)
for (let i = 0; i < other.length; i++) {
_this.push(other[i]);
}
}
export class MappedCode { export class MappedCode {
string: string; string: string;
map: DecodedSourceMap; map: DecodedSourceMap;
@ -159,10 +152,10 @@ export class MappedCode {
} }
// combine last line + first line // combine last line + first line
pushArray(m1.mappings[m1.mappings.length - 1], m2.mappings.shift()); push_array(m1.mappings[m1.mappings.length - 1], m2.mappings.shift());
// append other lines // append other lines
pushArray(m1.mappings, m2.mappings); push_array(m1.mappings, m2.mappings);
return this; return this;
} }

@ -22,6 +22,7 @@ export const globals = new Set([
'Event', 'Event',
'EventSource', 'EventSource',
'fetch', 'fetch',
'FormData',
'global', 'global',
'globalThis', 'globalThis',
'history', 'history',

@ -0,0 +1,12 @@
/**
* Pushes all `items` into `array` using `push`, therefore mutating the array.
* We do this for memory and perf reasons, and because `array.push(...items)` would
* run into a "max call stack size exceeded" error with too many items (~65k).
* @param array
* @param items
*/
export function push_array<T>(array: T[], items: T[]): void {
for (let i = 0; i < items.length; i++) {
array.push(items[i]);
}
}

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

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

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

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

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

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

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

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

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

@ -1,7 +1,7 @@
{ {
"html": { "html": {
"start": 0, "start": 0,
"end": 29, "end": 252,
"type": "Fragment", "type": "Fragment",
"children": [ "children": [
{ {
@ -13,24 +13,340 @@
{ {
"start": 5, "start": 5,
"end": 22, "end": 22,
"type": "Style", "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", "name": "color",
"modifiers": [], "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": { "expression": {
"type": "TemplateLiteral", "type": "TemplateLiteral",
"expressions": [], "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": [ "quasis": [
{ {
"type": "TemplateElement", "type": "TemplateElement",
"start": 225,
"end": 233,
"loc": {
"start": {
"line": 7,
"column": 19
},
"end": {
"line": 7,
"column": 27
}
},
"value": { "value": {
"raw": "red", "raw": "template",
"cooked": null "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 "tail": true
} }
] ]
} }
} }
]
}
], ],
"children": [] "children": []
} }

@ -13,9 +13,13 @@
{ {
"start": 5, "start": 5,
"end": 26, "end": 26,
"type": "Style", "type": "StyleDirective",
"name": "color", "name": "color",
"modifiers": [], "value": [
{
"start": 17,
"end": 26,
"type": "MustacheTag",
"expression": { "expression": {
"type": "Identifier", "type": "Identifier",
"start": 18, "start": 18,
@ -33,6 +37,8 @@
"name": "myColor" "name": "myColor"
} }
} }
]
}
], ],
"children": [] "children": []
} }

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,47 @@
export default {
html: `
<p>#FF0000</p>
<p>#00FF00</p>
<p>#0000FF</p>
`,
async test({ component, target, assert }) {
component.constant = 20;
assert.htmlEqual(target.innerHTML, `
<p>#FF0000</p>
<p>#00FF00</p>
<p>#0000FF</p>
`);
component.tags = [
{
name: 'Red',
color: '#FF0000'
},
{
name: 'Green',
color: '#00FF00'
},
{
name: 'Blue',
color: '#0000FF'
},
{
name: 'Black',
color: '#000000'
},
{
name: 'White',
color: '#FFFFFF'
}
];
assert.htmlEqual(target.innerHTML, `
<p>#FF0000</p>
<p>#00FF00</p>
<p>#0000FF</p>
<p>#000000</p>
<p>#FFFFFF</p>
`);
}
};

@ -0,0 +1,21 @@
<script>
export let tags = [
{
name: 'Red',
color: '#FF0000'
},
{
name: 'Green',
color: '#00FF00'
},
{
name: 'Blue',
color: '#0000FF'
}
];
</script>
{#each tags as tag}
{@const tagColor = tags.find(t => t.name === tag.name).color}
<p>{tagColor}</p>
{/each}

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

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

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

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

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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save