Merge branch 'master' into dispatch-cancelable

pull/7064/head
bluwy 5 years ago
commit 58568930f9

52
.github/stale.yml vendored

@ -1,52 +0,0 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- popular
- pinned
- security
- bug
- "[Status] Maybe Later"
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: false
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: false
# Label to use when marking as stale
staleLabel: stale-bot
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 1
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
pulls:
daysUntilStale: 210
daysUntilClose: 25
markComment: >
This pull request has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
closeComment: >
This pull request has been closed as it was previously marked as stale and saw no subsequent activity.
issues:
daysUntilStale: 180
daysUntilClose: 14
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
closeComment: >
This issue has been closed as it was previously marked as stale and saw no subsequent activity.

@ -1,5 +1,35 @@
# Svelte changelog # Svelte changelog
## Unreleased
* Ignore whitespace in `{#each}` blocks when containing elements with `animate:` ([#5477](https://github.com/sveltejs/svelte/pull/5477))
## 3.46.2
* Export `FlipParams` interface from `svelte/animate` ([#7103](https://github.com/sveltejs/svelte/issues/7103))
* Fix `style:` directive reactivity inside `{#each}` block ([#7136](https://github.com/sveltejs/svelte/issues/7136))
## 3.46.1
* Handle `style:kebab-case` directives ([#7122](https://github.com/sveltejs/svelte/issues/7122))
* Improve AST produced for `style:` directives ([#7127](https://github.com/sveltejs/svelte/pull/7127))
## 3.46.0
* Implement `{@const}` tag ([RFC #33](https://github.com/sveltejs/rfcs/pull/33), [#6413](https://github.com/sveltejs/svelte/pull/6413))
* Implement `style:` directive ([RFC #42](https://github.com/sveltejs/rfcs/pull/42), [#5923](https://github.com/sveltejs/svelte/pull/5923))
* Fix style manager conflicts when using multiple Svelte instances ([#7026](https://github.com/sveltejs/svelte/issues/7026))
* Fix hydration when using `{@html}` ([#7115](https://github.com/sveltejs/svelte/issues/7115))
## 3.45.0
* Fix non-boolean attribute rendering in SSR to render truthy values as-is ([#6121](https://github.com/sveltejs/svelte/issues/6121))
* Fix binding to a member expression also invalidating the member property ([#6921](https://github.com/sveltejs/svelte/issues/6921))
* Fix default values in `{#each}`/etc. destructurings not being considered references for the purposes of compiler warnings ([#6964](https://github.com/sveltejs/svelte/issues/6964))
* Fix `{:else if}` value incorrectly being cached ([#7043](https://github.com/sveltejs/svelte/pull/7043))
* Add `a11y-no-redundant-roles` warning ([#7067](https://github.com/sveltejs/svelte/pull/7067))
* Fix code generation error with arrow functions whose bodies are object destructuring assignments ([#7087](https://github.com/sveltejs/svelte/issues/7087))
## 3.44.3 ## 3.44.3
* Fix `bind:this` binding inside `onMount` for manually instantiated component ([#6760](https://github.com/sveltejs/svelte/issues/6760)) * Fix `bind:this` binding inside `onMount` for manually instantiated component ([#6760](https://github.com/sveltejs/svelte/issues/6760))

@ -1,4 +1,4 @@
Copyright (c) 2016-21 [these people](https://github.com/sveltejs/svelte/graphs/contributors) Copyright (c) 2016-22 [these people](https://github.com/sveltejs/svelte/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

18
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.44.3", "version": "3.46.2",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "svelte", "name": "svelte",
"version": "3.44.3", "version": "3.46.2",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@ampproject/remapping": "^0.3.0", "@ampproject/remapping": "^0.3.0",
@ -25,7 +25,7 @@
"acorn": "^8.4.1", "acorn": "^8.4.1",
"agadoo": "^1.1.0", "agadoo": "^1.1.0",
"c8": "^5.0.1", "c8": "^5.0.1",
"code-red": "^0.2.3", "code-red": "^0.2.4",
"codecov": "^3.5.0", "codecov": "^3.5.0",
"css-tree": "^1.1.2", "css-tree": "^1.1.2",
"eslint": "^7.32.0", "eslint": "^7.32.0",
@ -1082,9 +1082,9 @@
} }
}, },
"node_modules/code-red": { "node_modules/code-red": {
"version": "0.2.3", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/code-red/-/code-red-0.2.3.tgz", "resolved": "https://registry.npmjs.org/code-red/-/code-red-0.2.4.tgz",
"integrity": "sha512-l9MRiYO9iNx3dCpoZBZkaHAVtbhig8TBddEHq7ssWcZRAjaYR8NoRFzZ56VJ20TIg7hEQegCVCH3fVus+2Ol4Q==", "integrity": "sha512-tAJQiZviSyB2KUhz+rocKFzCHPkVooX2aFrdpfWDRvxWJaBQTYFJ/Z2TcWqbjXj5oJJBlqd2GxBXdtAhOXySVQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/estree": "^0.0.50", "@types/estree": "^0.0.50",
@ -6376,9 +6376,9 @@
} }
}, },
"code-red": { "code-red": {
"version": "0.2.3", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/code-red/-/code-red-0.2.3.tgz", "resolved": "https://registry.npmjs.org/code-red/-/code-red-0.2.4.tgz",
"integrity": "sha512-l9MRiYO9iNx3dCpoZBZkaHAVtbhig8TBddEHq7ssWcZRAjaYR8NoRFzZ56VJ20TIg7hEQegCVCH3fVus+2Ol4Q==", "integrity": "sha512-tAJQiZviSyB2KUhz+rocKFzCHPkVooX2aFrdpfWDRvxWJaBQTYFJ/Z2TcWqbjXj5oJJBlqd2GxBXdtAhOXySVQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/estree": "^0.0.50", "@types/estree": "^0.0.50",

@ -1,6 +1,6 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.44.3", "version": "3.46.2",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"module": "index.mjs", "module": "index.mjs",
"main": "index", "main": "index",
@ -83,8 +83,8 @@
}, },
"types": "types/runtime/index.d.ts", "types": "types/runtime/index.d.ts",
"scripts": { "scripts": {
"test": "mocha", "test": "mocha --exit",
"test:unit": "mocha --require sucrase/register --recursive src/**/__test__.ts", "test:unit": "mocha --require sucrase/register --recursive src/**/__test__.ts --exit",
"quicktest": "mocha", "quicktest": "mocha",
"precoverage": "c8 mocha", "precoverage": "c8 mocha",
"coverage": "c8 report --reporter=text-lcov > coverage.lcov && c8 report --reporter=html", "coverage": "c8 report --reporter=text-lcov > coverage.lcov && c8 report --reporter=html",
@ -132,7 +132,7 @@
"acorn": "^8.4.1", "acorn": "^8.4.1",
"agadoo": "^1.1.0", "agadoo": "^1.1.0",
"c8": "^5.0.1", "c8": "^5.0.1",
"code-red": "^0.2.3", "code-red": "^0.2.4",
"codecov": "^3.5.0", "codecov": "^3.5.0",
"css-tree": "^1.1.2", "css-tree": "^1.1.2",
"eslint": "^7.32.0", "eslint": "^7.32.0",

@ -94,4 +94,4 @@ We don't take this lightly: hopefully once you've experienced Svelte 3 you'll un
As grueling as this release has been, we're nowhere near finished. We have a ton of ideas for generating smarter, more compact code, and a long feature wish-list. [Sapper](https://sapper.svelte.dev), our Next.js-style app framework, is still in the middle of being updated to use Svelte 3. The [Svelte Native](https://svelte-native.technology/) community project, which allows you to write Android and iOS apps in Svelte, is making solid progress but deserves more complete support from core. We don't yet have the bounty of editor extensions, syntax highlighters, component kits, devtools and so on that other frameworks have, and we should fix that. We *really* want to add first-class TypeScript support. As grueling as this release has been, we're nowhere near finished. We have a ton of ideas for generating smarter, more compact code, and a long feature wish-list. [Sapper](https://sapper.svelte.dev), our Next.js-style app framework, is still in the middle of being updated to use Svelte 3. The [Svelte Native](https://svelte-native.technology/) community project, which allows you to write Android and iOS apps in Svelte, is making solid progress but deserves more complete support from core. We don't yet have the bounty of editor extensions, syntax highlighters, component kits, devtools and so on that other frameworks have, and we should fix that. We *really* want to add first-class TypeScript support.
But in the meantime we think Svelte 3 is the best way to build web apps yet. Take an hour to go through the [tutorial](/tutorial) and we hope to convince you of the same. Either way, we'd love to see you in our [Discord chatroom](/chat) and on [GitHub](https://github.com/sveltejs/svelte) — everyone is welcome, especially you. But in the meantime we think Svelte 3 is the best way to build web apps yet. Take an hour to go through the [tutorial](/tutorial) and we hope to convince you of the same. Either way, we'd love to see you in our [Discord chatroom](https://svelte.dev/chat) and on [GitHub](https://github.com/sveltejs/svelte) — everyone is welcome, especially you.

@ -0,0 +1,77 @@
---
title: "What's new in Svelte: January 2022"
description: "Faster builds with SvelteKit and a much anticipated REPL feature"
author: Daniel Sandoval
authorURL: https://desandoval.net
---
Happy new year, Svelte Community! Lots to share this month across Svelte, SvelteKit, Language Tools and the Showcase. Thanks to everyone who made 2021 a great year to use Svelte. Looking forward to the next one 🚀
## What's new in SvelteKit
- `@sveltejs/adapter-static` for SvelteKit now has a `precompress` option to make brotli compression of assets and pages easier to do out of the box ([#3079](https://github.com/sveltejs/kit/pull/3079))
- Concurrency mode in SvelteKit will now prerender pages in parallel ([#3120](https://github.com/sveltejs/kit/pull/3120)). It is enabled by default in `1.0.0-next.205` and later
- CSS is now automatically included before JS for improved page performance ([d13efe](https://github.com/sveltejs/kit/commit/d138efe21692f5925f1e89afc0a33f42d6a1a711))
- A new config option adds the ability to disable service worker registration to do your own custom registration ([#2988](https://github.com/sveltejs/kit/pull/2988))
- SSR route-splitting is here - breaking monolithic builds into smaller pieces for improved startup and routing performance ([#2931](https://github.com/sveltejs/kit/pull/2931))
- `request.origin/path/query` is now `request.url` - simplifying the config and page `load` functions ([#3126](https://github.com/sveltejs/kit/pull/3126))
- After the [update to Vite 2.7](https://github.com/sveltejs/kit/pull/3018), SvelteKit users are [reporting significant performance improvements](https://www.reddit.com/r/sveltejs/comments/rljhfc/sveltekit_massive_compiler_improvement_by/) and loading third-parties libraries in SSR has also been greatly improved
- SvelteKit server will now automatically restart when the config files is changed ([vite-plugin-svelte#237](https://github.com/sveltejs/vite-plugin-svelte/pull/237))
## Other new bits from `svelte/*`
- [Svelte 3.44.3](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md#3443) is out with a few bug fixes in the binding and loop code
- Svelte Language Tools has introduced support for the then/catch shorthands from Svelte 3.41 and TypeScript's "go to" functionality ([105.8.0 and later](https://github.com/sveltejs/language-tools/releases/tag/extensions-105.8.0))
- The Svelte REPL got a nice upgrade as well - letting you delete saved REPLs. Try it out by logging in at [svelte.dev/apps](https://svelte.dev/apps)
---
## Community Showcase
**Apps & Sites**
- [Discover Twitter Spaces](https://github.com/navneetsharmaui/discover-twitter-spaces) is a tool that helps you find the Twitter Spaces
- [Modern Fluid Typography Editor](https://github.com/codeAdrian/modern-fluid-typography-editor) helps create beautiful fluid typography using CSS clamp
- [Unnwhiteboard](https://github.com/AviKKi/unnwhiteboard) is a job board for companies (or teams) that don't do "whiteboard" interviews
- [Secret Santa](https://gitlab.com/arturoguzman/secret-santa-sveltekit) is a gift giving coordination app developed with easiness in mind
- [LogSnag](https://logsnag.com/) notifies you of your projects' events and provides you with a timeline to keep track of anything important that happens
- [Version 0.2 of Tangent](http://tangentnotes.com/Download), a Svelte-based note writing app, is now in beta
- [Intl Explorer](https://github.com/jesperorb/intl-explorer) is a tool for viewing output for all possible formatters for Intl
A lot of work this month has gone into migrating the Svelte main website and Svelte REPL to live in the https://github.com/sveltejs/sites repository - including a brand new homepage for [svelte.dev](https://svelte.dev/). Thanks to all the contributors who made this possible!
If you're looking for a fun SvelteKit project to work on, [you can contribute to the Svelte Society site rewrite](https://github.com/svelte-society/sveltesociety-2021/issues) 💅
**Learning and Listening**
_To Read_
- [Mutating Query Params in SvelteKit Without Page Reloads or Navigations](https://dev.to/mohamadharith/mutating-query-params-in-sveltekit-without-page-reloads-or-navigations-2i2b) by Mohamad Harith
- [Svelte for Reactaholics : A guide for React developers](https://www.100ms.live/blog/svelte-guide-for-react-developers) by Puru Vijay
- [Svelte's lifecycle methods can be used anywhere](https://geoffrich.net/posts/svelte-lifecycle-examples/) and [The many meanings of $ in Svelte](https://geoffrich.net/posts/svelte-$-meanings/) by Geoff Rich
- [Vercel and Svelte: A Perfect Match for Web Developers](https://thenewstack.io/vercel-and-svelte-a-perfect-match-for-web-developers/) by Darryl K. Taft
- [User-defined TailwindCSS Color Scheme with Svelte Stores](https://blog.dayslice.io/user-defined-tailwindcss-color-scheme-with-svelte-stores-ad80ca2cf038) by jeremy zaborowski
- [Ionic 6 + Svelte 🚀](https://medium.com/@raymondboswel/ionic-6-svelte-ae904caa82df) by Raymond Boswel
- [What happened in #Svelte language tools this year](https://twitter.com/dummdidumm_/status/1474158105395179525?t=ytj2K2Q52iD5-lNyLnQaAQ&s=19) by Simon H
_To Watch_
- [The Future of Svelte (Interview with Rich Harris)](https://www.youtube.com/watch?v=uQntFkK8Z54) by Lee Robinson, Director of Developer Relations at Vercel
- [Svelte is becoming the go-to framework](https://www.youtube.com/watch?v=fo6BKY2xR2w&t=1834s) for Obsidian plugin developers
- [Sveltekit WordPress Headless Blog](https://www.youtube.com/watch?v=c0UDVgjPxFw) by WebJeda
- [Getting started with SvelteKit](https://www.youtube.com/watch?v=i2suPKMPUFA) by Lihau Tan
- [Deploy a full-stack SvelteKit app on Cloudflare Pages](https://www.youtube.com/watch?v=Wc1_U6Dy5Tw) by 1nf
_To Listen To_
- [Syntax podcast: How To Do Things In Svelte](https://podcasts.apple.com/ca/podcast/how-to-do-things-in-svelte/id1253186678?i=1000544796072)
- [JS Party #205: So much Sveltey goodness (w/ Rich Harris)](https://changelog.com/jsparty/205)
**Libraries, Tools & Components**
- [svelte-headlessui](https://github.com/rgossiaux/svelte-headlessui) is an unofficial, complete Svelte port of the Headless UI component library
- [svelte-forms v2](https://chainlist.github.io/svelte-forms/) has been released - the author is [looking for feedback](https://www.reddit.com/r/sveltejs/comments/r6354j/svelteforms_v2_has_been_released/)
- [Percival](https://github.com/ekzhang/percival) is a declarative data query and visualization language
- [Svelte FlatList](https://github.com/snuffyDev/svelte-flatlist) is a mobile-friendly, simple, and customizable draggable menu
- [svelte-keyed](https://github.com/bryanmylee/svelte-keyed) is a writable derived store for objects and arrays
- [Svemix](https://github.com/svemix/svemix) is Remix for Svelte - providing server scripts inside your Svelte components/routes, which will be transformed into endpoints
Want to add something to the showcase? Need help bringing your next idea to life in Svelte? Join us on [Reddit](https://www.reddit.com/r/sveltejs/) or [Discord](https://discord.com/invite/yy75DKs).
See ya next month!

@ -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.

@ -6,6 +6,6 @@ This page contains detailed API reference documentation. It's intended to be a r
If that's not you (yet), you may prefer to visit the [interactive tutorial](/tutorial) or the [examples](/examples) before consulting this reference. If that's not you (yet), you may prefer to visit the [interactive tutorial](/tutorial) or the [examples](/examples) before consulting this reference.
Don't be shy about asking for help in the [Discord chatroom](/chat). Don't be shy about asking for help in the [Discord chatroom](https://svelte.dev/chat).
Using an older version of Svelte? Have a look at the [v2 docs](https://v2.svelte.dev). Using an older version of Svelte? Have a look at the [v2 docs](https://v2.svelte.dev).

@ -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
@ -821,6 +844,46 @@ A `class:` directive provides a shorter way of toggling a class on an element.
<div class:active class:inactive={!active} class:isAdmin>...</div> <div class:active class:inactive={!active} class:isAdmin>...</div>
``` ```
#### style:*property*
```sv
style:property={value}
```
```sv
style:property="value"
```
```sv
style:property
```
---
The `style:` directive provides a shorthand for setting multiple styles on an element.
```sv
<!-- These are equivalent -->
<div style:color="red">...</div>
<div style="color: red;">...</div>
<!-- Variables can be used -->
<div style:color={myColor}>...</div>
<!-- Shorthand, for when property and variable name match -->
<div style:color>...</div>
<!-- Multiple styles can be included -->
<div style:color style:width="12rem" style:background-color={darkMode ? "black" : "white"}>...</div>
```
---
When `style:` directives are combined with `style` attributes, the directives will take precedence:
```sv
<div style="color: blue;" style:color="red">This will be red</div>
```
#### use:*action* #### use:*action*
@ -1608,6 +1671,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>`

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

@ -50,7 +50,7 @@
</script> </script>
<h1>Caminandes: Llamigos</h1> <h1>Caminandes: Llamigos</h1>
<p>From <a href="https://cloud.blender.org/open-projects">Blender Open Projects</a>. CC-BY</p> <p>From <a href="https://studio.blender.org/films">Blender Studio</a>. CC-BY</p>
<div> <div>
<video <video

@ -50,7 +50,7 @@
</script> </script>
<h1>Caminandes: Llamigos</h1> <h1>Caminandes: Llamigos</h1>
<p>From <a href="https://cloud.blender.org/open-projects">Blender Open Projects</a>. CC-BY</p> <p>From <a href="https://studio.blender.org/films">Blender Studio</a>. CC-BY</p>
<div> <div>
<video <video

@ -50,7 +50,7 @@
</script> </script>
<h1>Caminandes: Llamigos</h1> <h1>Caminandes: Llamigos</h1>
<p>From <a href="https://cloud.blender.org/open-projects">Blender Open Projects</a>. CC-BY</p> <p>From <a href="https://studio.blender.org/films">Blender Studio</a>. CC-BY</p>
<div> <div>
<video <video

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

@ -8,4 +8,4 @@ To get set up in your local development environment, check out [the quickstart g
If you're looking for a more expansive framework that includes routing, server-side rendering and everything else, take a look at [SvelteKit](https://kit.svelte.dev). If you're looking for a more expansive framework that includes routing, server-side rendering and everything else, take a look at [SvelteKit](https://kit.svelte.dev).
Most importantly: since you're now a member of the Svelte community, you should [join our friendly Discord chatroom](/chat). That's where you'll find fellow Svelte users, and it's where we plan the future of the framework. Most importantly: since you're now a member of the Svelte community, you should [join our friendly Discord chatroom](https://svelte.dev/chat). That's where you'll find fellow Svelte users, and it's where we plan the future of the framework.

@ -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'
@ -241,5 +245,21 @@ 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(' → ')}`
})
}; };

@ -76,6 +76,10 @@ export default {
code: 'a11y-unknown-role', code: 'a11y-unknown-role',
message: `A11y: Unknown role '${role}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '') message: `A11y: Unknown role '${role}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '')
}), }),
a11y_no_redundant_roles: (role: string | boolean) => ({
code: 'a11y-no-redundant-roles',
message: `A11y: Redundant role '${role}'`
}),
a11y_accesskey: { a11y_accesskey: {
code: 'a11y-accesskey', code: 'a11y-accesskey',
message: 'A11y: Avoid using accesskey' message: 'A11y: Avoid using accesskey'

@ -33,12 +33,12 @@ export default class AwaitBlock extends Node {
if (this.then_node) { if (this.then_node) {
this.then_contexts = []; this.then_contexts = [];
unpack_destructuring(this.then_contexts, info.value); unpack_destructuring({ contexts: this.then_contexts, node: info.value, scope, component });
} }
if (this.catch_node) { if (this.catch_node) {
this.catch_contexts = []; this.catch_contexts = [];
unpack_destructuring(this.catch_contexts, info.error); unpack_destructuring({ contexts: this.catch_contexts, node: info.error, scope, component });
} }
this.pending = new PendingBlock(component, this, scope, info.pending); this.pending = new PendingBlock(component, this, scope, info.pending);

@ -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;
@ -39,7 +42,7 @@ export default class EachBlock extends AbstractBlock {
this.scope = scope.child(); this.scope = scope.child();
this.contexts = []; this.contexts = [];
unpack_destructuring(this.contexts, info.context); unpack_destructuring({ contexts: this.contexts, node: info.context, scope, component });
this.contexts.forEach(context => { this.contexts.forEach(context => {
this.scope.add(context.key.name, this.expression.dependencies, this); this.scope.add(context.key.name, this.expression.dependencies, this);
@ -57,9 +60,11 @@ export default class EachBlock extends AbstractBlock {
this.has_animation = false; this.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,6 +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 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';
@ -70,6 +71,46 @@ const a11y_labelable = new Set([
'textarea' 'textarea'
]); ]);
const a11y_nested_implicit_semantics = new Map([
['header', 'banner'],
['footer', 'contentinfo']
]);
const a11y_implicit_semantics = new Map([
['a', 'link'],
['aside', 'complementary'],
['body', 'document'],
['datalist', 'listbox'],
['dd', 'definition'],
['dfn', 'term'],
['details', 'group'],
['dt', 'term'],
['fieldset', 'group'],
['form', 'form'],
['h1', 'heading'],
['h2', 'heading'],
['h3', 'heading'],
['h4', 'heading'],
['h5', 'heading'],
['h6', 'heading'],
['hr', 'separator'],
['li', 'listitem'],
['menu', 'list'],
['nav', 'navigation'],
['ol', 'list'],
['optgroup', 'group'],
['output', 'status'],
['progress', 'progressbar'],
['section', 'region'],
['summary', 'button'],
['tbody', 'rowgroup'],
['textarea', 'textbox'],
['tfoot', 'rowgroup'],
['thead', 'rowgroup'],
['tr', 'row'],
['ul', 'list']
]);
const invisible_elements = new Set(['meta', 'html', 'script', 'style']); const invisible_elements = new Set(['meta', 'html', 'script', 'style']);
const valid_modifiers = new Set([ const valid_modifiers = new Set([
@ -98,6 +139,23 @@ const react_attributes = new Map([
const attributes_to_compact_whitespace = ['class', 'style']; const attributes_to_compact_whitespace = ['class', 'style'];
function is_parent(parent: INode, elements: string[]) {
let check = false;
while (parent) {
const parent_name = (parent as Element).name;
if (elements.includes(parent_name)) {
check = true;
break;
}
if (parent.type === 'Element') {
break;
}
parent = parent.parent;
}
return check;
}
function get_namespace(parent: Element, element: Element, explicit_namespace: string) { function get_namespace(parent: Element, element: Element, explicit_namespace: string) {
const parent_element = parent.find_nearest(/^Element/); const parent_element = parent.find_nearest(/^Element/);
@ -123,6 +181,7 @@ export default class Element extends Node {
actions: Action[] = []; actions: Action[] = [];
bindings: Binding[] = []; bindings: Binding[] = [];
classes: Class[] = []; classes: Class[] = [];
styles: StyleDirective[] = [];
handlers: EventHandler[] = []; handlers: EventHandler[] = [];
lets: Let[] = []; lets: Let[] = [];
intro?: Transition = null; intro?: Transition = null;
@ -206,6 +265,10 @@ 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 'StyleDirective':
this.styles.push(new StyleDirective(component, this, scope, node));
break;
case 'EventHandler': case 'EventHandler':
this.handlers.push(new EventHandler(component, this, scope, node)); this.handlers.push(new EventHandler(component, this, scope, node));
break; break;
@ -351,6 +414,22 @@ export default class Element extends Node {
const match = fuzzymatch(value, aria_roles); const match = fuzzymatch(value, aria_roles);
component.warn(attribute, compiler_warnings.a11y_unknown_role(value, match)); component.warn(attribute, compiler_warnings.a11y_unknown_role(value, match));
} }
// no-redundant-roles
const has_redundant_role = value === a11y_implicit_semantics.get(this.name);
if (this.name === value || has_redundant_role) {
component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(value));
}
// Footers and headers are special cases, and should not have redundant roles unless they are the children of sections or articles.
const is_parent_section_or_article = is_parent(this.parent, ['section', 'article']);
if (!is_parent_section_or_article) {
const has_nested_redundant_role = value === a11y_nested_implicit_semantics.get(this.name);
if (has_nested_redundant_role) {
component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(value));
}
}
} }
// no-access-key // no-access-key

@ -126,6 +126,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() {

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

@ -1,13 +1,15 @@
import map_children from './shared/map_children';
import TemplateScope from './shared/TemplateScope'; import 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();

@ -8,7 +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 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';
@ -26,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';
@ -44,6 +45,7 @@ export type INode = Action
| CatchBlock | CatchBlock
| Class | Class
| Comment | Comment
| ConstTag
| DebugTag | DebugTag
| EachBlock | EachBlock
| Element | Element
@ -61,10 +63,17 @@ export type INode = Action
| RawMustacheTag | RawMustacheTag
| Slot | Slot
| SlotTemplate | SlotTemplate
| DefaultSlotTemplate | StyleDirective
| Tag | Tag
| Text | Text
| ThenBlock | ThenBlock
| Title | Title
| Transition | Transition
| Window; | Window;
export type INodeAllowConstTag =
| EachBlock
| CatchBlock
| ThenBlock
| InlineComponent
| SlotTemplate;

@ -3,6 +3,9 @@ import { Node, Identifier, Expression } from 'estree';
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import is_reference, { NodeWithPropertyDefinition } from 'is-reference'; import is_reference, { NodeWithPropertyDefinition } from 'is-reference';
import { clone } from '../../../utils/clone'; import { clone } from '../../../utils/clone';
import Component from '../../Component';
import flatten_reference from '../../utils/flatten_reference';
import TemplateScope from './TemplateScope';
export interface Context { export interface Context {
key: Identifier; key: Identifier;
@ -11,7 +14,21 @@ export interface Context {
default_modifier: (node: Node, to_ctx: (name: string) => Node) => Node; default_modifier: (node: Node, to_ctx: (name: string) => Node) => Node;
} }
export function unpack_destructuring(contexts: Context[], node: Node, modifier: Context['modifier'] = node => node, default_modifier: Context['default_modifier'] = node => node) { export function unpack_destructuring({
contexts,
node,
modifier = (node) => node,
default_modifier = (node) => node,
scope,
component
}: {
contexts: Context[];
node: Node;
modifier?: Context['modifier'];
default_modifier?: Context['default_modifier'];
scope: TemplateScope;
component: Component;
}) {
if (!node) return; if (!node) return;
if (node.type === 'Identifier') { if (node.type === 'Identifier') {
@ -29,13 +46,41 @@ export function unpack_destructuring(contexts: Context[], node: Node, modifier:
} else if (node.type === 'ArrayPattern') { } else if (node.type === 'ArrayPattern') {
node.elements.forEach((element, i) => { node.elements.forEach((element, i) => {
if (element && element.type === 'RestElement') { if (element && element.type === 'RestElement') {
unpack_destructuring(contexts, element, node => x`${modifier(node)}.slice(${i})` as Node, default_modifier); unpack_destructuring({
contexts,
node: element,
modifier: (node) => x`${modifier(node)}.slice(${i})` as Node,
default_modifier,
scope,
component
});
} else if (element && element.type === 'AssignmentPattern') { } else if (element && element.type === 'AssignmentPattern') {
const n = contexts.length; const n = contexts.length;
mark_referenced(element.right, scope, component);
unpack_destructuring(contexts, element.left, node => x`${modifier(node)}[${i}]`, (node, to_ctx) => x`${node} !== undefined ? ${node} : ${update_reference(contexts, n, element.right, to_ctx)}` as Node); unpack_destructuring({
contexts,
node: element.left,
modifier: (node) => x`${modifier(node)}[${i}]`,
default_modifier: (node, to_ctx) =>
x`${node} !== undefined ? ${node} : ${update_reference(
contexts,
n,
element.right,
to_ctx
)}` as Node,
scope,
component
});
} else { } else {
unpack_destructuring(contexts, element, node => x`${modifier(node)}[${i}]` as Node, default_modifier); unpack_destructuring({
contexts,
node: element,
modifier: (node) => x`${modifier(node)}[${i}]` as Node,
default_modifier,
scope,
component
});
} }
}); });
} else if (node.type === 'ObjectPattern') { } else if (node.type === 'ObjectPattern') {
@ -43,12 +88,17 @@ export function unpack_destructuring(contexts: Context[], node: Node, modifier:
node.properties.forEach((property) => { node.properties.forEach((property) => {
if (property.type === 'RestElement') { if (property.type === 'RestElement') {
unpack_destructuring( unpack_destructuring({
contexts, contexts,
property.argument, node: property.argument,
node => x`@object_without_properties(${modifier(node)}, [${used_properties}])` as Node, modifier: (node) =>
default_modifier x`@object_without_properties(${modifier(
); node
)}, [${used_properties}])` as Node,
default_modifier,
scope,
component
});
} else { } else {
const key = property.key as Identifier; const key = property.key as Identifier;
const value = property.value; const value = property.value;
@ -57,16 +107,43 @@ export function unpack_destructuring(contexts: Context[], node: Node, modifier:
if (value.type === 'AssignmentPattern') { if (value.type === 'AssignmentPattern') {
const n = contexts.length; const n = contexts.length;
unpack_destructuring(contexts, value.left, node => x`${modifier(node)}.${key.name}`, (node, to_ctx) => x`${node} !== undefined ? ${node} : ${update_reference(contexts, n, value.right, to_ctx)}` as Node); mark_referenced(value.right, scope, component);
unpack_destructuring({
contexts,
node: value.left,
modifier: (node) => x`${modifier(node)}.${key.name}`,
default_modifier: (node, to_ctx) =>
x`${node} !== undefined ? ${node} : ${update_reference(
contexts,
n,
value.right,
to_ctx
)}` as Node,
scope,
component
});
} else { } else {
unpack_destructuring(contexts, value, node => x`${modifier(node)}.${key.name}` as Node, default_modifier); unpack_destructuring({
contexts,
node: value,
modifier: (node) => x`${modifier(node)}.${key.name}` as Node,
default_modifier,
scope,
component
});
} }
} }
}); });
} }
} }
function update_reference(contexts: Context[], n: number, expression: Expression, to_ctx: (name: string) => Node): Node { function update_reference(
contexts: Context[],
n: number,
expression: Expression,
to_ctx: (name: string) => Node
): Node {
const find_from_context = (node: Identifier) => { const find_from_context = (node: Identifier) => {
for (let i = n; i < contexts.length; i++) { for (let i = n; i < contexts.length; i++) {
const { key } = contexts[i]; const { key } = contexts[i];
@ -85,7 +162,12 @@ function update_reference(contexts: Context[], n: number, expression: Expression
expression = clone(expression) as Expression; expression = clone(expression) as Expression;
walk(expression, { walk(expression, {
enter(node, parent: Node) { enter(node, parent: Node) {
if (is_reference(node as NodeWithPropertyDefinition, parent as NodeWithPropertyDefinition)) { if (
is_reference(
node as NodeWithPropertyDefinition,
parent as NodeWithPropertyDefinition
)
) {
this.replace(find_from_context(node as Identifier)); this.replace(find_from_context(node as Identifier));
this.skip(); this.skip();
} }
@ -94,3 +176,20 @@ function update_reference(contexts: Context[], n: number, expression: Expression
return expression; return expression;
} }
function mark_referenced(
node: Node,
scope: TemplateScope,
component: Component
) {
walk(node, {
enter(node: any, parent: any) {
if (is_reference(node, parent)) {
const { name } = flatten_reference(node);
if (!scope.is_let(name) && !scope.names.has(name)) {
component.add_reference(name);
}
}
}
});
}

@ -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;
@ -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;
@ -219,7 +223,7 @@ export default class Expression {
component.add_reference(name); // TODO is this redundant/misplaced? component.add_reference(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);
} }

@ -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';
@ -25,6 +26,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;

@ -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 };
} }
@ -268,7 +268,7 @@ export default class Renderer {
} }
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;

@ -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;
} }
`); `);

@ -70,6 +70,30 @@ export default class BindingWrapper {
return dependencies; return dependencies;
} }
get_update_dependencies() {
const object = this.object;
const dependencies = new Set<string>();
if (this.node.expression.template_scope.names.has(object)) {
this.node.expression.template_scope.dependencies_for_name
.get(object)
.forEach((name) => dependencies.add(name));
} else {
dependencies.add(object);
}
const result = new Set(dependencies);
dependencies.forEach((dependency) => {
const indirect_dependencies = this.parent.renderer.component.indirect_dependencies.get(dependency);
if (indirect_dependencies) {
indirect_dependencies.forEach(indirect_dependency => {
result.add(indirect_dependency);
});
}
});
return result;
}
is_readonly_media_attribute() { is_readonly_media_attribute() {
return this.node.is_readonly_media_attribute(); return this.node.is_readonly_media_attribute();
} }

@ -199,7 +199,7 @@ export default class ElementWrapper extends Wrapper {
} }
// 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);
} }
@ -340,6 +340,7 @@ export default class ElementWrapper extends Wrapper {
this.add_transitions(block); this.add_transitions(block);
this.add_animation(block); this.add_animation(block);
this.add_classes(block); this.add_classes(block);
this.add_styles(block);
this.add_manual_style_scoping(block); this.add_manual_style_scoping(block);
if (nodes && this.renderer.options.hydratable && !this.void) { if (nodes && this.renderer.options.hydratable && !this.void) {
@ -466,7 +467,7 @@ export default class ElementWrapper extends Wrapper {
binding_group.bindings.forEach(binding => { binding_group.bindings.forEach(binding => {
// TODO this is a mess // TODO this is a mess
add_to_set(dependencies, binding.get_dependencies()); add_to_set(dependencies, binding.get_update_dependencies());
add_to_set(contextual_dependencies, binding.handler.contextual_dependencies); add_to_set(contextual_dependencies, binding.handler.contextual_dependencies);
binding.render(block, lock); binding.render(block, lock);
@ -914,6 +915,43 @@ export default class ElementWrapper extends Wrapper {
}); });
} }
add_styles(block: Block) {
const has_spread = this.node.attributes.some(attr => attr.is_spread);
this.node.styles.forEach((style_directive) => {
const { name, expression, should_cache } = style_directive;
const snippet = expression.manipulate(block);
let cached_snippet;
if (should_cache) {
cached_snippet = block.get_unique_name(`style_${name.replace(/-/g, '_')}`);
block.add_variable(cached_snippet, snippet);
}
const updater = b`@set_style(${this.var}, "${name}", ${should_cache ? cached_snippet : snippet}, false)`;
block.chunks.hydrate.push(updater);
const dependencies = expression.dynamic_dependencies();
if (has_spread) {
block.chunks.update.push(updater);
} else if (dependencies.length > 0) {
if (should_cache) {
block.chunks.update.push(b`
if (${block.renderer.dirty(dependencies)} && (${cached_snippet} !== (${cached_snippet} = ${snippet}))) {
${updater}
}
`);
} else {
block.chunks.update.push(b`
if (${block.renderer.dirty(dependencies)}) {
${updater}
}
`);
}
}
});
}
add_manual_style_scoping(block) { add_manual_style_scoping(block) {
if (this.node.needs_manual_style_scoping) { if (this.node.needs_manual_style_scoping) {
const updater = b`@toggle_class(${this.var}, "${this.node.component.stylesheet.id}", true);`; const updater = b`@toggle_class(${this.var}, "${this.node.component.stylesheet.id}", true);`;

@ -266,15 +266,15 @@ export default class IfBlockWrapper extends Wrapper {
if (this.needs_update) { if (this.needs_update) {
block.chunks.init.push(b` block.chunks.init.push(b`
function ${select_block_type}(#ctx, #dirty) { function ${select_block_type}(#ctx, #dirty) {
${this.branches.map(({ dependencies, condition, snippet, block }) => condition ${this.branches.map(({ dependencies, condition, snippet }) => {
return b`${snippet && dependencies.length > 0 ? b`if (${block.renderer.dirty(dependencies)}) ${condition} = null;` : null}`;
})}
${this.branches.map(({ condition, snippet, block }) => condition
? b` ? b`
${snippet && ( ${snippet && b`if (${condition} == null) ${condition} = !!${snippet}`}
dependencies.length > 0
? b`if (${condition} == null || ${block.renderer.dirty(dependencies)}) ${condition} = !!${snippet}`
: b`if (${condition} == null) ${condition} = !!${snippet}`
)}
if (${condition}) return ${block.name};` if (${condition}) return ${block.name};`
: b`return ${block.name};`)} : b`return ${block.name};`
)}
} }
`); `);
} else { } else {
@ -387,13 +387,12 @@ export default class IfBlockWrapper extends Wrapper {
${this.needs_update ${this.needs_update
? b` ? b`
function ${select_block_type}(#ctx, #dirty) { function ${select_block_type}(#ctx, #dirty) {
${this.branches.map(({ dependencies, condition, snippet }, i) => condition ${this.branches.map(({ dependencies, condition, snippet }) => {
return b`${snippet && dependencies.length > 0 ? b`if (${block.renderer.dirty(dependencies)}) ${condition} = null;` : null}`;
})}
${this.branches.map(({ condition, snippet }, i) => condition
? b` ? b`
${snippet && ( ${snippet && b`if (${condition} == null) ${condition} = !!${snippet}`}
dependencies.length > 0
? b`if (${condition} == null || ${block.renderer.dirty(dependencies)}) ${condition} = !!${snippet}`
: b`if (${condition} == null) ${condition} = !!${snippet}`
)}
if (${condition}) return ${i};` if (${condition}) return ${i};`
: b`return ${i};`)} : b`return ${i};`)}
${!has_else && b`return -1;`} ${!has_else && b`return -1;`}

@ -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)`);
}
} }
} }

@ -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);
});
});
}

@ -12,7 +12,7 @@ export default function bind_this(component: Component, block: Block, binding: B
const callee = block.renderer.reference(fn.name); const callee = block.renderer.reference(fn.name);
const { contextual_dependencies, mutation } = binding.handler; const { contextual_dependencies, mutation } = binding.handler;
const dependencies = binding.get_dependencies(); const dependencies = binding.get_update_dependencies();
const body = b` const body = b`
${mutation} ${mutation}

@ -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();

@ -3,7 +3,7 @@ import { get_attribute_expression, get_attribute_value, get_class_attribute_valu
import { boolean_attributes } from './shared/boolean_attributes'; import { boolean_attributes } from './shared/boolean_attributes';
import Renderer, { RenderOptions } from '../Renderer'; import Renderer, { RenderOptions } from '../Renderer';
import Element from '../../nodes/Element'; import Element from '../../nodes/Element';
import { x } from 'code-red'; import { p, x } from 'code-red';
import Expression from '../../nodes/shared/Expression'; import Expression from '../../nodes/shared/Expression';
import remove_whitespace_children from './utils/remove_whitespace_children'; import remove_whitespace_children from './utils/remove_whitespace_children';
import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing'; import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing';
@ -36,6 +36,15 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
class_expression_list.length > 0 && class_expression_list.length > 0 &&
class_expression_list.reduce((lhs, rhs) => x`${lhs} + ' ' + ${rhs}`); class_expression_list.reduce((lhs, rhs) => x`${lhs} + ' ' + ${rhs}`);
const style_expression_list = node.styles.map(style_directive => {
const { name, expression: { node: expression } } = style_directive;
return p`"${name}": ${expression}`;
});
const style_expression =
style_expression_list.length > 0 &&
x`{ ${style_expression_list} }`;
if (node.attributes.some(attr => attr.is_spread)) { if (node.attributes.some(attr => attr.is_spread)) {
// TODO dry this out // TODO dry this out
const args = []; const args = [];
@ -65,9 +74,10 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
} }
}); });
renderer.add_expression(x`@spread([${args}], ${class_expression})`); renderer.add_expression(x`@spread([${args}], { classes: ${class_expression}, styles: ${style_expression} })`);
} else { } else {
let add_class_attribute = !!class_expression; let add_class_attribute = !!class_expression;
let add_style_attribute = !!style_expression;
node.attributes.forEach(attribute => { node.attributes.forEach(attribute => {
const name = attribute.name.toLowerCase(); const name = attribute.name.toLowerCase();
const attr_name = node.namespace === namespaces.foreign ? attribute.name : fix_attribute_casing(attribute.name); const attr_name = node.namespace === namespaces.foreign ? attribute.name : fix_attribute_casing(attribute.name);
@ -88,6 +98,9 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
renderer.add_string(` ${attr_name}="`); renderer.add_string(` ${attr_name}="`);
renderer.add_expression(x`[${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim()`); renderer.add_expression(x`[${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim()`);
renderer.add_string('"'); renderer.add_string('"');
} else if (name === 'style' && style_expression) {
add_style_attribute = false;
renderer.add_expression(x`@add_styles(@merge_ssr_styles(${get_attribute_value(attribute)}, ${style_expression}))`);
} else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') { } else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') {
const snippet = (attribute.chunks[0] as Expression).node; const snippet = (attribute.chunks[0] as Expression).node;
renderer.add_expression(x`@add_attribute("${attr_name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`); renderer.add_expression(x`@add_attribute("${attr_name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`);
@ -98,7 +111,10 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
} }
}); });
if (add_class_attribute) { if (add_class_attribute) {
renderer.add_expression(x`@add_classes([${class_expression}].join(' ').trim())`); renderer.add_expression(x`@add_classes((${class_expression}).trim())`);
}
if (add_style_attribute) {
renderer.add_expression(x`@add_styles(${style_expression})`);
} }
} }

@ -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
};
})
};
}

@ -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,16 +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'
| '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;
@ -63,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

@ -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,13 +1,13 @@
import read_expression from '../read/expression';
import read_script from '../read/script';
import read_style from '../read/style';
import { decode_character_references, closing_tag_omitted } from '../utils/html';
import { is_void } from '../../utils/names';
import { Parser } from '../index';
import { Directive, DirectiveType, TemplateNode, Text } from '../../interfaces'; import { Directive, DirectiveType, TemplateNode, Text } from '../../interfaces';
import fuzzymatch from '../../utils/fuzzymatch';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore'; 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_errors from '../errors';
import { Parser } from '../index';
import read_expression from '../read/expression';
import read_script from '../read/script';
import read_style from '../read/style';
import { closing_tag_omitted, decode_character_references } from '../utils/html';
// eslint-disable-next-line no-useless-escape // 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\-]*/;
@ -365,9 +365,25 @@ 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 (value[0]) { if (type === 'StyleDirective') {
if ((value as any[]).length > 1 || value[0].type === 'Text') { return {
parser.error(parser_errors.invalid_directive_value, value[0].start); start,
end,
type,
name: directive_name,
value
};
}
const first_value = value[0];
let expression = null;
if (first_value) {
const attribute_contains_text = (value as any[]).length > 1 || first_value.type === 'Text';
if (attribute_contains_text) {
parser.error(parser_errors.invalid_directive_value, first_value.start);
} else {
expression = first_value.expression;
} }
} }
@ -377,7 +393,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
type, type,
name: directive_name, name: directive_name,
modifiers, modifiers,
expression: (value[0] && value[0].expression) || null expression
}; };
if (type === 'Transition') { if (type === 'Transition') {
@ -386,6 +402,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
directive.outro = direction === 'out' || direction === 'transition'; directive.outro = direction === 'out' || direction === 'transition';
} }
// Directive name is expression, e.g. <p class:isRed />
if (!directive.expression && (type === 'Binding' || type === 'Class')) { if (!directive.expression && (type === 'Binding' || type === 'Class')) {
directive.expression = { directive.expression = {
start: directive.start + colon_index + 1, start: directive.start + colon_index + 1,
@ -414,6 +431,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 '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';
@ -471,6 +489,8 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
data: null data: null
}; };
const chunks: TemplateNode[] = [];
function flush(end: number) { function flush(end: number) {
if (current_chunk.raw) { if (current_chunk.raw) {
current_chunk.data = decode_character_references(current_chunk.raw); current_chunk.data = decode_character_references(current_chunk.raw);
@ -479,8 +499,6 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
} }
} }
const chunks: TemplateNode[] = [];
while (parser.index < parser.template.length) { while (parser.index < parser.template.length) {
const index = parser.index; const index = parser.index;

@ -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':

@ -1,6 +1,6 @@
// The `foreign` namespace covers all DOM implementations that aren't HTML5. // The `foreign` namespace covers all DOM implementations that aren't HTML5.
// It opts out of HTML5-specific a11y checks and case-insensitive attribute names. // It opts out of HTML5-specific a11y checks and case-insensitive attribute names.
export const foreign = 'https://svelte.dev/docs#svelte_options'; export const foreign = 'https://svelte.dev/docs#template-syntax-svelte-options';
export const html = 'http://www.w3.org/1999/xhtml'; export const html = 'http://www.w3.org/1999/xhtml';
export const mathml = 'http://www.w3.org/1998/Math/MathML'; export const mathml = 'http://www.w3.org/1998/Math/MathML';
export const svg = 'http://www.w3.org/2000/svg'; export const svg = 'http://www.w3.org/2000/svg';

@ -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);
@ -530,8 +530,12 @@ export function set_input_type(input, type) {
} }
export function set_style(node, key, value, important) { export function set_style(node, key, value, important) {
if (value === null) {
node.style.removeProperty(key);
} else {
node.style.setProperty(key, value, important ? 'important' : ''); node.style.setProperty(key, value, important ? 'important' : '');
} }
}
export function select_option(select, value) { export function select_option(select, value) {
for (let i = 0; i < select.options.length; i += 1) { for (let i = 0; i < select.options.length; i += 1) {

@ -6,8 +6,12 @@ export const invalid_attribute_name_character = /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFF
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter // https://infra.spec.whatwg.org/#noncharacter
export function spread(args, classes_to_add) { export function spread(args, attrs_to_add) {
const attributes = Object.assign({}, ...args); const attributes = Object.assign({}, ...args);
if (attrs_to_add) {
const classes_to_add = attrs_to_add.classes;
const styles_to_add = attrs_to_add.styles;
if (classes_to_add) { if (classes_to_add) {
if (attributes.class == null) { if (attributes.class == null) {
attributes.class = classes_to_add; attributes.class = classes_to_add;
@ -15,6 +19,16 @@ export function spread(args, classes_to_add) {
attributes.class += ' ' + classes_to_add; attributes.class += ' ' + classes_to_add;
} }
} }
if (styles_to_add) {
if (attributes.style == null) {
attributes.style = style_object_to_string(styles_to_add);
} else {
attributes.style = style_object_to_string(merge_ssr_styles(attributes.style, styles_to_add));
}
}
}
let str = ''; let str = '';
Object.keys(attributes).forEach(name => { Object.keys(attributes).forEach(name => {
@ -32,6 +46,28 @@ export function spread(args, classes_to_add) {
return str; return str;
} }
export function merge_ssr_styles(style_attribute, style_directive) {
const style_object = {};
for (const individual_style of style_attribute.split(';')) {
const colon_index = individual_style.indexOf(':');
const name = individual_style.slice(0, colon_index).trim();
const value = individual_style.slice(colon_index + 1).trim();
if (!name) continue;
style_object[name] = value;
}
for (const name in style_directive) {
const value = style_directive[name];
if (value) {
style_object[name] = value;
} else {
delete style_object[name];
}
}
return style_object;
}
export const escaped = { export const escaped = {
'"': '&quot;', '"': '&quot;',
"'": '&#39;', "'": '&#39;',
@ -141,9 +177,22 @@ export function create_ssr_component(fn) {
export function add_attribute(name, value, boolean) { export function add_attribute(name, value, boolean) {
if (value == null || (boolean && !value)) return ''; if (value == null || (boolean && !value)) return '';
return ` ${name}${value === true ? '' : `=${typeof value === 'string' ? JSON.stringify(escape(value)) : `"${value}"`}`}`; return ` ${name}${value === true && boolean_attributes.has(name) ? '' : `=${typeof value === 'string' ? JSON.stringify(escape(value)) : `"${value}"`}`}`;
} }
export function add_classes(classes) { export function add_classes(classes) {
return classes ? ` class="${classes}"` : ''; return classes ? ` class="${classes}"` : '';
} }
function style_object_to_string(style_object) {
return Object.keys(style_object)
.filter(key => style_object[key])
.map(key => `${key}: ${style_object[key]};`)
.join(' ');
}
export function add_styles(style_object) {
const styles = style_object_to_string(style_object);
return styles ? ` style="${styles}"` : '';
}

@ -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();
}); });
} }

@ -4,7 +4,7 @@ import * as http from 'http';
import { rollup } from 'rollup'; import { rollup } from 'rollup';
import virtual from '@rollup/plugin-virtual'; import virtual from '@rollup/plugin-virtual';
import puppeteer from 'puppeteer'; import puppeteer from 'puppeteer';
import { addLineNumbers, loadConfig, loadSvelte } from '../helpers'; import { addLineNumbers, loadConfig, loadSvelte, retryAsync, executeBrowserTest } from '../helpers';
import { deepEqual } from 'assert'; import { deepEqual } from 'assert';
const page = ` const page = `
@ -17,8 +17,8 @@ const page = `
const assert = fs.readFileSync(`${__dirname}/assert.js`, 'utf-8'); const assert = fs.readFileSync(`${__dirname}/assert.js`, 'utf-8');
describe('custom-elements', function() { describe('custom-elements', function() {
// Note: Increase the timeout in preparation for restarting Chromium due to SIGSEGV.
this.timeout(10000); this.timeout(10000);
let svelte; let svelte;
let server; let server;
let browser; let browser;
@ -44,12 +44,16 @@ describe('custom-elements', function() {
}); });
} }
async function launchPuppeteer() {
return await retryAsync(() => puppeteer.launch());
}
before(async () => { before(async () => {
svelte = loadSvelte(); svelte = loadSvelte();
console.log('[custom-element] Loaded Svelte'); console.log('[custom-element] Loaded Svelte');
server = await create_server(); server = await create_server();
console.log('[custom-element] Started server'); console.log('[custom-element] Started server');
browser = await puppeteer.launch(); browser = await launchPuppeteer();
console.log('[custom-element] Launched puppeteer browser'); console.log('[custom-element] Launched puppeteer browser');
}); });
@ -108,26 +112,7 @@ describe('custom-elements', function() {
const result = await bundle.generate({ format: 'iife', name: 'test' }); const result = await bundle.generate({ format: 'iife', name: 'test' });
code = result.output[0].code; code = result.output[0].code;
const page = await browser.newPage(); function assertWarnings() {
page.on('console', (type) => {
console[type._type](type._text);
});
page.on('error', error => {
console.log('>>> an error happened');
console.error(error);
});
try {
await page.goto('http://localhost:6789');
const result = await page.evaluate(() => test(document.querySelector('main')));
if (result) console.log(result);
} catch (err) {
console.log(addLineNumbers(code));
throw err;
} finally {
if (expected_warnings) { if (expected_warnings) {
deepEqual(warnings.map(w => ({ deepEqual(warnings.map(w => ({
code: w.code, code: w.code,
@ -138,6 +123,15 @@ describe('custom-elements', function() {
})), expected_warnings); })), expected_warnings);
} }
} }
browser = await executeBrowserTest(
browser,
launchPuppeteer,
assertWarnings,
() => {
console.log(addLineNumbers(code));
assertWarnings();
});
}); });
}); });
}); });

@ -8,7 +8,7 @@ export const assert = (assert$1 as unknown) as typeof assert$1 & { htmlEqual: (a
// for coverage purposes, we need to test source files, // for coverage purposes, we need to test source files,
// but for sanity purposes, we need to test dist files // but for sanity purposes, we need to test dist files
export function loadSvelte(test) { export function loadSvelte(test: boolean = false) {
process.env.TEST = test ? 'true' : ''; process.env.TEST = test ? 'true' : '';
const resolved = require.resolve('../compiler.js'); const resolved = require.resolve('../compiler.js');
@ -279,3 +279,50 @@ export function prettyPrintPuppeteerAssertionError(message) {
assert.equal(match[1], match[2]); assert.equal(match[1], match[2]);
} }
} }
export async function retryAsync<T>(fn: () => Promise<T>, maxAttempts: number = 3, interval: number = 1000): Promise<T> {
let attempts = 0;
while (attempts <= maxAttempts) {
try {
return await fn();
} catch (err) {
if (++attempts >= maxAttempts) throw err;
await new Promise(resolve => setTimeout(resolve, interval));
}
}
}
// NOTE: Chromium may exit with SIGSEGV, so retry in that case
export async function executeBrowserTest<T>(browser, launchPuppeteer: () => Promise<T>, additionalAssertion: () => void, onError: (err: Error) => void) {
let count = 0;
do {
count++;
try {
const page = await browser.newPage();
page.on('console', (type) => {
console[type._type](type._text);
});
page.on('error', error => {
console.log('>>> an error happened');
console.error(error);
});
await page.goto('http://localhost:6789');
const result = await page.evaluate(() => test(document.querySelector('main')));
if (result) console.log(result);
additionalAssertion();
await page.close();
break;
} catch (err) {
if (count === 5 || browser.isConnected()) {
onError(err);
throw err;
}
console.debug(err.stack || err);
console.log('RESTARTING Chromium...');
browser = await launchPuppeteer();
}
} while (count <= 5);
return browser;
}

@ -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>`;
}); });

@ -18,8 +18,8 @@ function create_fragment(ctx) {
return { return {
c() { c() {
svg = document.createElementNS("https://svelte.dev/docs#svelte_options", "svg"); svg = document.createElementNS("https://svelte.dev/docs#template-syntax-svelte-options", "svg");
img = document.createElementNS("https://svelte.dev/docs#svelte_options", "img"); img = document.createElementNS("https://svelte.dev/docs#template-syntax-svelte-options", "img");
this.h(); this.h();
}, },
l(nodes) { l(nodes) {

@ -0,0 +1,41 @@
{
"html": {
"start": 0,
"end": 29,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 29,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 5,
"end": 22,
"type": "Class",
"name": "foo",
"modifiers": [],
"expression": {
"type": "Identifier",
"start": 16,
"end": 21,
"loc": {
"start": {
"line": 1,
"column": 16
},
"end": {
"line": 1,
"column": 21
}
},
"name": "isFoo"
}
}
],
"children": []
}
]
}
}

@ -0,0 +1,25 @@
{
"html": {
"start": 0,
"end": 23,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 23,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 5,
"end": 16,
"type": "StyleDirective",
"name": "color",
"value": true
}
],
"children": []
}
]
}
}

@ -0,0 +1,7 @@
<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>

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

@ -0,0 +1,47 @@
{
"html": {
"start": 0,
"end": 33,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 33,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 5,
"end": 26,
"type": "StyleDirective",
"name": "color",
"value": [
{
"start": 17,
"end": 26,
"type": "MustacheTag",
"expression": {
"type": "Identifier",
"start": 18,
"end": 25,
"loc": {
"start": {
"line": 1,
"column": 18
},
"end": {
"line": 1,
"column": 25
}
},
"name": "myColor"
}
}
]
}
],
"children": []
}
]
}
}

@ -0,0 +1 @@
<div style="color: red;">red</div>

@ -0,0 +1,41 @@
{
"html": {
"start": 0,
"end": 34,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 34,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 5,
"end": 24,
"type": "Attribute",
"name": "style",
"value": [
{
"start": 12,
"end": 23,
"type": "Text",
"raw": "color: red;",
"data": "color: red;"
}
]
}
],
"children": [
{
"start": 25,
"end": 28,
"type": "Text",
"raw": "red",
"data": "red"
}
]
}
]
}
}

@ -9,7 +9,9 @@ import {
loadConfig, loadConfig,
loadSvelte, loadSvelte,
mkdirp, mkdirp,
prettyPrintPuppeteerAssertionError prettyPrintPuppeteerAssertionError,
retryAsync,
executeBrowserTest
} from '../helpers'; } from '../helpers';
import { deepEqual } from 'assert'; import { deepEqual } from 'assert';
@ -48,15 +50,21 @@ function create_server() {
}); });
} }
async function launchPuppeteer() {
return await retryAsync(() => puppeteer.launch());
}
const assert = fs.readFileSync(`${__dirname}/assert.js`, 'utf-8'); const assert = fs.readFileSync(`${__dirname}/assert.js`, 'utf-8');
describe('runtime (puppeteer)', () => { describe('runtime (puppeteer)', function() {
// Note: Increase the timeout in preparation for restarting Chromium due to SIGSEGV.
this.timeout(10000);
before(async () => { before(async () => {
svelte = loadSvelte(false); svelte = loadSvelte(false);
console.log('[runtime-puppeteer] Loaded Svelte'); console.log('[runtime-puppeteer] Loaded Svelte');
server = await create_server(); server = await create_server();
console.log('[runtime-puppeteer] Started server'); console.log('[runtime-puppeteer] Started server');
browser = await puppeteer.launch(); browser = await launchPuppeteer();
console.log('[runtime-puppeteer] Launched puppeteer browser'); console.log('[runtime-puppeteer] Launched puppeteer browser');
}); });
@ -205,27 +213,7 @@ describe('runtime (puppeteer)', () => {
const result = await bundle.generate({ format: 'iife', name: 'test' }); const result = await bundle.generate({ format: 'iife', name: 'test' });
code = result.output[0].code; code = result.output[0].code;
const page = await browser.newPage(); function assertWarnings() {
page.on('console', (type) => {
console[type._type](type._text);
});
page.on('error', error => {
console.log('>>> an error happened');
console.error(error);
});
try {
await page.goto('http://localhost:6789');
const result = await page.evaluate(() => test(document.querySelector('main')));
if (result) console.log(result);
} catch (err) {
failed.add(dir);
prettyPrintPuppeteerAssertionError(err.message);
throw err;
} finally {
if (config.warnings) { if (config.warnings) {
deepEqual(warnings.map(w => ({ deepEqual(warnings.map(w => ({
code: w.code, code: w.code,
@ -240,6 +228,16 @@ describe('runtime (puppeteer)', () => {
throw new Error('Received unexpected warnings'); throw new Error('Received unexpected warnings');
} }
} }
browser = await executeBrowserTest(
browser,
launchPuppeteer,
assertWarnings,
(err) => {
failed.add(dir);
prettyPrintPuppeteerAssertionError(err.message);
assertWarnings();
});
}); });
} }

@ -65,7 +65,7 @@ describe('runtime', () => {
} }
const testName = `${dir} ${hydrate ? `(with hydration${from_ssr_html ? ' from ssr rendered html' : ''})` : ''}`; const testName = `${dir} ${hydrate ? `(with hydration${from_ssr_html ? ' from ssr rendered html' : ''})` : ''}`;
(config.skip ? it.skip : solo ? it.only : it)(testName, () => { (config.skip ? it.skip : solo ? it.only : it)(testName, (done) => {
if (failed.has(dir)) { if (failed.has(dir)) {
// this makes debugging easier, by only printing compiled output once // this makes debugging easier, by only printing compiled output once
throw new Error('skipping test, already failed'); throw new Error('skipping test, already failed');
@ -120,7 +120,7 @@ describe('runtime', () => {
} }
}); });
return Promise.resolve() Promise.resolve()
.then(() => { .then(() => {
// hack to support transition tests // hack to support transition tests
clear_loops(); clear_loops();
@ -245,6 +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(err);
throw err; throw err;
}) })
.then(() => { .then(() => {
@ -255,6 +256,7 @@ describe('runtime', () => {
flush(); flush();
if (config.after_test) config.after_test(); if (config.after_test) config.after_test();
done();
}); });
}); });
} }

@ -1,7 +1,8 @@
export default { export default {
html: '<textarea readonly></textarea>', html: '<textarea readonly data-attr="true"></textarea>',
test({ assert, target }) { test({ assert, target }) {
const textarea = target.querySelector('textarea'); const textarea = target.querySelector('textarea');
assert.equal(textarea.dataset.attr, 'true');
assert.ok(textarea.readOnly); assert.ok(textarea.readOnly);
} }
}; };

@ -1 +1 @@
<textarea readonly="{true}"></textarea> <textarea readonly={true} data-attr={true} />

@ -0,0 +1,18 @@
// binding member expression shouldn't invalidate the property name
export default {
async test({ assert, component, target, window }) {
const input = target.querySelector('input');
assert.deepEqual(component.logs.length, 1);
assert.equal(input.value, 'abc');
input.value = 'hij';
await input.dispatchEvent(new window.Event('input'));
assert.deepEqual(component.values.a, 'hij');
assert.deepEqual(component.logs.length, 1);
component.paths = ['b'];
assert.deepEqual(component.logs.length, 2);
assert.equal(input.value, 'def');
}
};

@ -0,0 +1,9 @@
<script>
export let values = { a: 'abc', b: 'def' };
export let paths = ['a'];
export let logs = [];
$: paths && logs.push('paths updated');
</script>
<input bind:value={values[paths[0]]} />

@ -0,0 +1,10 @@
export default {
html: '<div>content</div><div>content</div><div>content</div>',
test({ assert, target, component }) {
const divs = target.querySelectorAll('div');
assert.equal(component.refs[0], divs[0]);
assert.equal(component.refs[1], divs[1]);
assert.equal(component.refs[2], divs[2]);
}
};

@ -0,0 +1,15 @@
<script>
export let data = [ { id: '1' }, { id: '2' }, { id: '3' } ];
export let refs = [];
// note that this is NOT data.slice().reverse()
// as that wouldn't have triggered an infinite loop
$: list = data.reverse();
</script>
{#each list as { id }, index (id)}
<div bind:this={refs[index]}>
content
</div>
{/each}

@ -0,0 +1,8 @@
// binding member expression shouldn't invalidate the property name
export default {
test({ assert, component, target }) {
const div = target.querySelector('div');
assert.equal(div, component.container.a);
assert.deepEqual(component.logs.length, 1);
}
};

@ -0,0 +1,9 @@
<script>
export let container = {};
export let paths = ['a'];
export let logs = [];
$: paths && logs.push('paths updated');
</script>
<div bind:this={container[paths[0]]} />

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save