Merge branch 'master' into pr/5431

pull/5431/head
Conduitry 5 years ago
commit 2a7a71426e

@ -7,6 +7,8 @@ assignees: ''
---
## Is this about svelte@next? This project is currently in a pre-release stage and breaking changes may occur at any time. Please do not post any kind of bug reports or questions on GitHub about it.
**Describe the bug**
A clear and concise description of what the bug is.
@ -35,6 +37,8 @@ If you have a stack trace to include, we recommend putting inside a `<details>`
</details>
**Information about your Svelte project:**
To make your life easier, just run `npx envinfo --system --npmPackages svelte,rollup,webpack --binaries --browsers` and paste the output here.
- Your browser and the version: (e.x. Chrome 52.1, Firefox 48.0, IE 10)
- Your operating system: (e.x. OS X 10, Ubuntu Linux 19.10, Windows XP, etc)

4
.gitignore vendored

@ -18,10 +18,6 @@ node_modules
/coverage/
/coverage.lcov
/test/*/samples/_
/test/sourcemaps/samples/*/output.js
/test/sourcemaps/samples/*/output.js.map
/test/sourcemaps/samples/*/output.css
/test/sourcemaps/samples/*/output.css.map
/yarn-error.log
_actual*.*
_output

@ -1,15 +1,14 @@
module.exports = {
file: [
'test/test.js'
'test/test.ts'
],
require: [
'sucrase/register'
]
};
// add coverage options when running 'npx c8 mocha'
if (process.env.NODE_V8_COVERAGE) {
Object.assign(module.exports, {
fullTrace: true,
require: [
'source-map-support/register'
]
});
module.exports.fullTrace = true;
module.exports.require.push('source-map-support/register');
}

@ -2,6 +2,73 @@
## Unreleased
* Fix setting reactive dependencies which don't appear in the template to `undefined` ([#5538](https://github.com/sveltejs/svelte/issues/5538))
* Support preprocessor sourcemaps during compilation ([#5584](https://github.com/sveltejs/svelte/pull/5584))
* Fix ordering of elements when using `{#if}` inside `{#key}` ([#5680](https://github.com/sveltejs/svelte/issues/5680))
* Add `hasContext` lifecycle function ([#5690](https://github.com/sveltejs/svelte/pull/5690))
* Fix missing `walk` types in `svelte/compiler` ([#5696](https://github.com/sveltejs/svelte/pull/5696))
## 3.29.7
* Include `./register` in exports map ([#5670](https://github.com/sveltejs/svelte/issues/5670))
## 3.29.6
* Include `./package.json` in export map ([#5659](https://github.com/sveltejs/svelte/issues/5659))
## 3.29.5
* Fix `$$props` and `$$restProps` when compiling to a custom element ([#5482](https://github.com/sveltejs/svelte/issues/5482))
* Include an export map in `package.json` ([#5556](https://github.com/sveltejs/svelte/issues/5556))
* Fix function calls in `<slot>` props that use contextual values ([#5565](https://github.com/sveltejs/svelte/issues/5565))
* Fix handling aborted transitions in `{:else}` blocks ([#5573](https://github.com/sveltejs/svelte/issues/5573))
* Add `Element` and `Node` to known globals ([#5586](https://github.com/sveltejs/svelte/issues/5586))
* Fix `$$slots` when compiling to custom elements ([#5594](https://github.com/sveltejs/svelte/issues/5594))
* Fix internal `import`s so that we're exposing a valid ES module ([#5617](https://github.com/sveltejs/svelte/issues/5617))
## 3.29.4
* Fix code generation error with `??` alongside logical operators ([#5558](https://github.com/sveltejs/svelte/issues/5558))
## 3.29.3
* Hopefully actually republish with proper UMD build for use in the REPL
## 3.29.2
* Republish with proper UMD build for use in the REPL
## 3.29.1
* Fix compiler hanging on `<slot slot="...">` ([#5475](https://github.com/sveltejs/svelte/issues/5475))
* Fix types on `get` function in `svelte/store` ([#5483](https://github.com/sveltejs/svelte/pull/5483))
* Add missing `end` field on ASTs for non-top-level `<style>` elements ([#5487](https://github.com/sveltejs/svelte/pull/5487))
* Fix `{#if}` inside `{#await}` with destructuring ([#5508](https://github.com/sveltejs/svelte/issues/5508))
* Fix types on lifecycle hooks ([#5529](https://github.com/sveltejs/svelte/pull/5529))
## 3.29.0
* Support `<slot slot="...">` ([#2079](https://github.com/sveltejs/svelte/issues/2079))
* Fix unmounting components with a bidirectional transition with a delay ([#4954](https://github.com/sveltejs/svelte/issues/4954))
* Add types to `get` function in `svelte/store` ([#5269](https://github.com/sveltejs/svelte/pull/5269))
* Add a warning when a component looks like it's trying to use another component without beginning with a capital letter ([#5302](https://github.com/sveltejs/svelte/pull/5302))
* Add `EventSource` to known globals ([#5463](https://github.com/sveltejs/svelte/issues/5463))
* Fix compiler exception with `~`/`+` combinators and `{...spread}` attributes ([#5465](https://github.com/sveltejs/svelte/issues/5465))
## 3.28.0
* Add `{#key}` block for keying arbitrary content on an expression ([#1469](https://github.com/sveltejs/svelte/issues/1469))
## 3.27.0
* Add `|nonpassive` event modifier, explicitly passing `passive: false` ([#2068](https://github.com/sveltejs/svelte/issues/2068))
* Scope CSS selectors with `~` and `+` combinators ([#3104](https://github.com/sveltejs/svelte/issues/3104))
* Fix keyed `{#each}` not reacting to key changing ([#5444](https://github.com/sveltejs/svelte/issues/5444))
* Fix destructuring into store values ([#5449](https://github.com/sveltejs/svelte/issues/5449))
* Fix erroneous `missing-declaration` warning with `use:obj.method` ([#5451](https://github.com/sveltejs/svelte/issues/5451))
## 3.26.0
* Support `use:obj.method` as actions ([#3935](https://github.com/sveltejs/svelte/issues/3935))
* Support `_` as numeric separator ([#5407](https://github.com/sveltejs/svelte/issues/5407))
* Fix assignments to properties on store values ([#5412](https://github.com/sveltejs/svelte/issues/5412))

@ -2,14 +2,15 @@
<a href="https://svelte.dev">
<img alt="Cybernetically enhanced web apps: Svelte" src="https://sveltejs.github.io/assets/banner.png">
</a>
<a href="https://www.npmjs.com/package/svelte">
<img src="https://img.shields.io/npm/v/svelte.svg" alt="npm version">
</a>
<a href="https://github.com/sveltejs/svelte/blob/master/LICENSE">
<img src="https://img.shields.io/npm/l/svelte.svg" alt="license">
</a>
<a href="https://svelte.dev/chat">
<img src="https://img.shields.io/discord/457912077277855764?label=chat&logo=discord" alt="Chat">
</a>
</p>

34
package-lock.json generated

@ -1,9 +1,19 @@
{
"name": "svelte",
"version": "3.25.1",
"version": "3.29.7",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@ampproject/remapping": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-0.3.0.tgz",
"integrity": "sha512-dqmASpaTCavldZqwdEpokgG4yOXmEiEGPP3ATTsBbdXXSKf6kx8jt2fPcKhodABdZlYe82OehR2oFK1y9gwZxw==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "1.0.0",
"sourcemap-codec": "1.4.8"
}
},
"@babel/code-frame": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
@ -36,6 +46,12 @@
"integrity": "sha512-KioOCsSvSvXx6xUNLiJz+P+VMb7NRcePjoefOr74Y5P6lEKsiOn35eZyZzgpK4XCNJdXTDR7+zykj0lwxRvZ2g==",
"dev": true
},
"@jridgewell/resolve-uri": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-1.0.0.tgz",
"integrity": "sha512-9oLAnygRMi8Q5QkYEU4XWK04B+nuoXoxjRvRxgjuChkLZFBja0YPSgdZ7dZtwhncLBcQe/I/E+fLuk5qxcYVJA==",
"dev": true
},
"@rollup/plugin-commonjs": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.0.0.tgz",
@ -144,8 +160,8 @@
}
},
"@sveltejs/eslint-config": {
"version": "github:sveltejs/eslint-config#848ce6464a9ae9c2f3a3095474701dfe9ab851df",
"from": "github:sveltejs/eslint-config#v5.0.0",
"version": "github:sveltejs/eslint-config#cca8177349dd5a02b19a5865afc4a7066921409a",
"from": "github:sveltejs/eslint-config#v5.6.0",
"dev": true
},
"@tootallnate/once": {
@ -804,9 +820,9 @@
}
},
"code-red": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/code-red/-/code-red-0.1.3.tgz",
"integrity": "sha512-3n9i1Jv0om4+2Aq7pCL8M5xRgc2wTXsblsYUxXJDpgBZ3wJP1zbcNzu4XgUqrG0rjc1to2yh+3n9rwHsJa7qSA==",
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/code-red/-/code-red-0.1.4.tgz",
"integrity": "sha512-NPiLFd1EfonYONCNXCH1oVirQMXk5oAOQ3FanRbjB4HZ09rTU/lZGKjVOOP0FKr4pccGVYE5I4CqtyU6ens2VA==",
"dev": true,
"requires": {
"acorn": "^7.3.1",
@ -3737,9 +3753,9 @@
}
},
"sourcemap-codec": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz",
"integrity": "sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==",
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
"spdx-correct": {

@ -1,6 +1,6 @@
{
"name": "svelte",
"version": "3.25.1",
"version": "3.29.7",
"description": "Cybernetically enhanced web apps",
"module": "index.mjs",
"main": "index",
@ -18,6 +18,44 @@
"svelte",
"README.md"
],
"exports": {
"./package.json": "./package.json",
".": {
"import": "./index.mjs",
"require": "./index.js"
},
"./compiler": {
"import": "./compiler.mjs",
"require": "./compiler.js"
},
"./animate": {
"import": "./animate/index.mjs",
"require": "./animate/index.js"
},
"./easing": {
"import": "./easing/index.mjs",
"require": "./easing/index.js"
},
"./internal": {
"import": "./internal/index.mjs",
"require": "./internal/index.js"
},
"./motion": {
"import": "./motion/index.mjs",
"require": "./motion/index.js"
},
"./register": {
"require": "./register.js"
},
"./store": {
"import": "./store/index.mjs",
"require": "./store/index.js"
},
"./transition": {
"import": "./transition/index.mjs",
"require": "./transition/index.js"
}
},
"engines": {
"node": ">= 8"
},
@ -56,6 +94,7 @@
},
"homepage": "https://github.com/sveltejs/svelte#README",
"devDependencies": {
"@ampproject/remapping": "^0.3.0",
"@rollup/plugin-commonjs": "^11.0.0",
"@rollup/plugin-json": "^4.0.1",
"@rollup/plugin-node-resolve": "^6.0.0",
@ -63,7 +102,7 @@
"@rollup/plugin-sucrase": "^3.0.0",
"@rollup/plugin-typescript": "^2.0.1",
"@rollup/plugin-virtual": "^2.0.0",
"@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.0.0",
"@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.6.0",
"@types/mocha": "^7.0.0",
"@types/node": "^8.10.53",
"@typescript-eslint/eslint-plugin": "^3.0.2",
@ -71,7 +110,7 @@
"acorn": "^7.4.0",
"agadoo": "^1.1.0",
"c8": "^5.0.1",
"code-red": "^0.1.3",
"code-red": "^0.1.4",
"codecov": "^3.5.0",
"css-tree": "1.0.0-alpha22",
"eslint": "^7.1.0",
@ -89,6 +128,7 @@
"rollup": "^1.27.14",
"source-map": "^0.7.3",
"source-map-support": "^0.5.13",
"sourcemap-codec": "^1.4.8",
"tiny-glob": "^0.2.6",
"tslib": "^1.10.0",
"typescript": "^3.5.3"

@ -20,7 +20,7 @@ const ts_plugin = is_publish
const external = id => id.startsWith('svelte/');
fs.writeFileSync(`./compiler.d.ts`, `export { compile, parse, preprocess, VERSION } from './types/compiler/index';`);
fs.writeFileSync(`./compiler.d.ts`, `export { compile, parse, preprocess, walk, VERSION } from './types/compiler/index';`);
export default [
/* runtime */
@ -30,12 +30,12 @@ export default [
{
file: `index.mjs`,
format: 'esm',
paths: id => id.startsWith('svelte/') && `${id.replace('svelte', '.')}`
paths: id => id.startsWith('svelte/') && `${id.replace('svelte', '.')}/index.mjs`
},
{
file: `index.js`,
format: 'cjs',
paths: id => id.startsWith('svelte/') && `${id.replace('svelte', '.')}`
paths: id => id.startsWith('svelte/') && `${id.replace('svelte', '.')}/index.js`
}
],
external,
@ -50,12 +50,12 @@ export default [
{
file: `${dir}/index.mjs`,
format: 'esm',
paths: id => id.startsWith('svelte/') && `${id.replace('svelte', '..')}`
paths: id => id.startsWith('svelte/') && `${id.replace('svelte', '..')}/index.mjs`
},
{
file: `${dir}/index.js`,
format: 'cjs',
paths: id => id.startsWith('svelte/') && `${id.replace('svelte', '..')}`
paths: id => id.startsWith('svelte/') && `${id.replace('svelte', '..')}/index.js`
}
],
external,

@ -30,7 +30,9 @@ To treat `*.svelte` files as HTML, open *__Edit → Config...__* and add the fol
## Vim/Neovim
To treat all `*.svelte` files as HTML, add the following line to your `init.vim`:
You can use the [coc-svelte extension](https://github.com/coc-extensions/coc-svelte) which utilises the official language-server.
As an alternative you can treat all `*.svelte` files as HTML. Add the following line to your `init.vim`:
```
au! BufNewFile,BufRead *.svelte set ft=html
@ -50,13 +52,7 @@ To set the filetype for a single file, use a [modeline](https://vim.fandom.com/w
## Visual Studio Code
To treat `*.svelte` files as HTML, add the following lines to your `settings.json` file:
```cson
"files.associations": {
"*.svelte": "html"
}
```
We recommend using the official [Svelte for VS Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## JetBrains WebStorm

@ -45,7 +45,7 @@ A full introduction to the command line is out of the scope of this guide, but h
Once installed, you'll have access to three new commands:
* `node my-file.js` — runs the JavaScript in `my-file.js`
* `npm [subcommand]` — [npm](https://www.npmjs.com/) is a way to install 'packages' that your application depends on, such as the [svelte](https://www.npmjs.com/) package
* `npm [subcommand]` — [npm](https://www.npmjs.com/) is a way to install 'packages' that your application depends on, such as the [svelte](https://www.npmjs.com/package/svelte) package
* `npx [subcommand]` — a convenient way to run programs available on npm without permanently installing them
@ -58,9 +58,7 @@ To write code, you need a good editor. The most popular choice is [Visual Studio
We're going to follow the instructions in part two of [The easiest way to get started with Svelte](/blog/the-easiest-way-to-get-started).
First, we'll use npx to run [degit](https://github.com/Rich-Harris/degit), a program for cloning project templates from [GitHub](https://github.com) and other code storage websites. You don't have to use a project template, but it means you have to do a lot less setup work.
(Eventually you'll probably have to learn [git](https://git-scm.com/), which most programmers use to manage their projects. But you don't need to worry about it just yet.)
First, we'll use npx to run [degit](https://github.com/Rich-Harris/degit), a program for cloning project templates from [GitHub](https://github.com) and other code storage websites. You don't have to use a project template, but it means you have to do a lot less setup work. You will need to have [Git](https://git-scm.com/) installed in order to use degit. (Eventually you'll probably have to learn [Git](https://git-scm.com/) itself, which most programmers use to manage their projects.)
On the command line, navigate to where you want to create a new project, then type the following lines (you can paste the whole lot, but you'll develop better muscle memory if you get into the habit of writing each line out one at a time then running it):

@ -0,0 +1,85 @@
---
title: What's new in Svelte: October 2020
description: New object methods, in-depth learning resources and tons of integration examples!
author: Daniel Sandoval
authorURL: https://desandoval.net
---
Welcome to the first edition of our "What's new in Svelte" series! We'll try to make this a monthly blog post in which you'll find out about new features, bug fixes, and a showcase of Svelte projects from around the community.
## New features
1. `use:obj.method` allows functions defined within objects to be used within actions ([Example](https://svelte.dev/repl/c305722adb4a4545b27b198ea8ff9bde?version=3.27.0), **3.26.0**, warning removed in **3.27.0**)
2. `_` is now supported as a "numerical separator", similar to a `.` or `,` ([Example](https://svelte.dev/repl/844c39e91d1248649fe54af839fab570?version=3.26.0), **3.26.0**)
3. `import.meta` now works in template expressions ([Example](https://svelte.dev/repl/9630de41957a4c80a4fce264360a6bc7?version=3.26.0), **3.26.0**)
4. CSS Selectors with `~` and `+` combinators are now supported ([Example](https://svelte.dev/repl/91ad9257d2d1430185a504a18cc60172?version=3.29.0), **3.27.0**, with a compiler fix in **3.29.0**)
5. The `{#key}` block is now available to key arbitrary content on an expression. Whever the expression changes, the contents inside the `{#key}` block will be destroyed and recreated. For an in-depth explanation and to find out how it's implemented, check out the [new blog post](https://lihautan.com/contributing-to-svelte-implement-key-block/) of Svelte Team member Tan Li Hau. ([More info](https://github.com/sveltejs/svelte/issues/1469), **3.29.0**)
6. Slots can now be forwarded through child components! This used to only be possible by adding extra wrapper `<div>`s ([More info](https://github.com/sveltejs/svelte/issues/2079), **3.29.0**)
7. When using TypeScript, you can now type the `createEventDispatcher` method:
```html
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher<{
/**
* you can also add docs
*/
checked: boolean; // Will translate to `CustomEvent<boolean>`
hello: string;
}>();
// ...
</script>
```
This will make sure that you can invoke dispatch only with the specified event names and its types. The Svelte for VS Code extension was also updated to deal with this new feature. It will provide strong typings for these events as well as autocompletion and hover information.
**New from Sapper!**
Sapper 0.28.9 just came out. The highlights from it include much better support for CSP nonces, asset preload support for exported pages, and error details are now available in the `$page` store on error pages.
In addition, Sapper's CSS handling has been rewritten over the course of recent releases in order to fix existing CSS handling bugs, refactor the CSS handling to occur entirely within a Rollup plugin, and remove the need internally to register CSS in the routing system. Congrats and thank you to the folks working on Sapper for all their solid work!
## Impactful bug fixes
- CSS compilation will no longer remove rules for the `open` attribute on `<details>` elements ([Example](https://svelte.dev/repl/ab4c0c177d1f4fab92f46eb8539cea9a?version=3.26.0), **3.26.0**)
- `prettier-plugin-svelte` will do a better job now at dealing with whitespaces, especially around inline elements. It will also preserve formatting inside `<pre>` tags and will no longer format languages which are not supported by Prettier, like SASS, Pug or Stylus.
## Coming up
- [Svelte Summit](https://sveltesummit.com/), Svelte's second global online conference, is taking place on October 18! Sign up for free to get reminders and talk updates!
For all the features and bugfixes see the CHANGELOG for [Svelte](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md) and [Sapper](https://github.com/sveltejs/sapper/blob/master/CHANGELOG.md).
---
## Svelte Showcase
- [This CustomMenu example](https://svelte.dev/repl/3a33725c3adb4f57b46b597f9dade0c1?version=3.25.0) demos how to replace the OS right-click menu
- [Github Tetris](https://svelte.dev/repl/cc1eaa7c66964fedb5e70e3ecbbaa0e1?version=3.25.1) lets you play a Tetris-like game in a git commit history
- [Who are my representatives?](https://whoaremyrepresentatives.us/) is a website built with Svelte to help US residents get more info on their congressional representatives
- [Pick Palette](https://github.com/bluwy/pick-palette) is a color palette manager made with Svelte!
#### In-depth learning:
- [Svelte 3 Up and Running](https://www.amazon.com/dp/B08D6T6BKS/ref=cm_sw_r_tw_dp_x_OQMtFb3GPQCB2) is a new book about building production-ready static web apps with Svelte 3
- [Sapper Tutorial (Crash Course)](https://www.youtube.com/playlist?list=PL4cUxeGkcC9gdr4Qhx83gBBcID-KMe-PQ) walks through the ins-and-outs of Sapper, the Svelte-powered application framework
- [Svelte Society Day France](https://france.sveltesociety.dev/) happened September 27th featuring a wide variety of topics all in French! You can find the full recording [here](https://www.youtube.com/watch?v=aS1TQ155JK4).
#### Plug-and-play components:
- [svelte-zoom](https://github.com/vaheqelyan/svelte-zoom) brings "nearly native" pan-and-zoom to images on desktop and mobile
- [svelte-materialify](https://github.com/TheComputerM/svelte-materialify) is a Material component library for Svelte with over 50 components
- [svelte-undoable](https://github.com/macfja/svelte-undoable) makes it easy to introduce undo and redo functionality using `bind:`
- [This Tilt component](https://svelte.dev/repl/7b23ad9d2693424482cd411b0378b55b?version=3.24.1) implements a common UX pattern where the hovered element tilts to follow the mouse
#### Lots of examples of how use JS tech came out this month:
- [Sapper with PostCSS and Tailwind](https://codechips.me/sapper-with-postcss-and-tailwind/)
- [PrismJS (Code block syntax highlighting)](https://github.com/phptuts/Svelte-PrismJS)
- [Filepond (Drag-and-drop file upload)](https://github.com/pqina/svelte-filepond)
- [Ionic (UI Components)](https://github.com/Tommertom/svelte-ionic-app)
- [Pell (WYSIWYG Editor)](https://github.com/Demonicious/svelte-pell/)
- [Leaflet (Mapping)](https://github.com/anoram/leaflet-svelte)
**Reminder**: There's a [Svelte integrations repo](https://github.com/sveltejs/integrations) that demonstrates ways to incorporate Svelte into your stack (and vice versa). If you've got questions on how to use a particular piece of tech with Svelte, you may find your answer there... and if you've gotten something to work with Svelte, consider contributing!
For more amazing Svelte projects, check out the [Svelte Society](https://sveltesociety.dev/), [Reddit](https://www.reddit.com/r/sveltejs/) and [Discord](https://discord.com/invite/yy75DKs)… and be sure to post your own!
## See you next month!
By the way, Svelte now has an [OpenCollective](https://opencollective.com/svelte)! All contributions and all expenses are published in our transparent public ledger. Learn who is donating, how much, where that money is going, submit expenses, get reimbursed and more!

@ -0,0 +1,46 @@
---
title: What's new in Svelte: November 2020
description: Slot forwarding fixes, SvelteKit for faster local development, and more from Svelte Summit
author: Daniel Sandoval
authorURL: https://desandoval.net
---
Welcome back to the "What's new in Svelte" series! This month, we're covering new features & bug fixes, last month's Svelte Summit and some stand-out sites and libraries...
## New features & impactful bug fixes
1. Destructuring Promises now works as expected by using the `{#await}` syntax
(**3.29.3**, [Example](https://svelte.dev/repl/3fd4e2cecfa14d629961478f1dac2445?version=3.29.3))
2. Slot forwarding (released in 3.29.0) should no longer hang during compilation (**3.29.3**, [Example](https://svelte.dev/repl/29959e70103f4868a6525c0734934936?version=3.29.3))
3. Better typings for the `get` function in `svelte/store` and on lifecycle hooks (**3.29.1**)
**What's going on in Sapper?**
Sapper got some new types in its `preload` function, which will make typing easier if you are using TypeScript. See the [Sapper docs](https://sapper.svelte.dev/docs#Typing_the_function) on how to use them. There also were fixes to `preload` links in exported sites. Route layouts got a few fixes too - including ensuring CSS is applied to nested route layouts. You can also better organize your files now that extensions with multiple dots are supported. (**0.28.10**)
For all the features and bugfixes see the CHANGELOGs for [Svelte](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md) and [Sapper](https://github.com/sveltejs/sapper/blob/master/CHANGELOG.md).
## [Svelte Summit](https://sveltesummit.com/) was Svelte-tacular!
- Rich Harris demoed the possible future of Svelte development in a talk titled "Futuristic Web Development". The not-yet-public project is called SvelteKit (name may change) and will bring a first-class developer experience and more flexibility for build outputs. If you want to get the full sneak-peek, [check out the video](https://www.youtube.com/watch?v=qSfdtmcZ4d0).
- 17 speakers made the best of the conference's virtual format... From floating heads to seamless demos, Svelte developers from every skill level will find something of interest in this year's [YouTube playlist](https://www.youtube.com/playlist?list=PL8bMgX1kyZThM1sbYCoWdTcpiYysJsSeu)
---
## Community Showcase
- [Svelte Lab](https://sveltelab.app/) showcases a variety of components, visualizations and interactions that can be achieved in Svelte. You can click into any component to see its source or edit it, using the site's built-in REPL
- [svelte-electron-boilerplate](https://github.com/hjalmar/svelte-electron-boilerplate) is a fast way to get up and running with a Svelte app built in the desktop javascript framework, Electron
- [React Hooks in Svelte](https://github.com/joshnuss/react-hooks-in-svelte) showcases examples of common React Hooks ported to Svelte.
- [gurlic](https://gurlic.com/) is a social network and internet experiment that is super snappy thanks to Svelte
- [Interference 2020](https://interference2020.org/) visualizes reported foreign interference in the 2020 U.S. elections. You can learn more about how it was built in [YYY's talk at Svelte Summit]()
- [jitsi-svelte](https://github.com/relm-us/jitsi-svelte) lets you easily create your own custom Jitsi client by providing out-of-the-box components built with Svelte
- [Ellx](https://ellx.io/) is part spreadsheet, part notebook and part IDE. It's super smooth thanks to Svelte 😎
- [This New Zealand news site](https://www.nzherald.co.nz/nz/election-2020-latest-results-party-vote-electorate-vote-and-full-data/5CFVO4ENKNQDE3SICRRNPU5GZM/) breaks down the results of the 2020 Parliamentary elections using Svelte
- [Budibase](https://github.com/Budibase/budibase) is a no-code app builder, powered by Svelte
- [Svelt-yjs](https://github.com/relm-us/svelt-yjs) combines the collaborative, local-first technology of Yjs with the power of Svelte to enable multiple users across the internet to stay in sync.
- [tabler-icons-svelte](https://github.com/benflap/tabler-icons-svelte) is a Svelte wrapper for over 850 free MIT-licensed high-quality SVG icons for you to use in your web projects.
## See you next month!
Got an idea for something to add to the Showcase? Want to get involved more with Svelte? We're always looking for maintainers, contributors and fanatics... Check out the [Svelte Society](https://sveltesociety.dev/), [Reddit](https://www.reddit.com/r/sveltejs/) and [Discord](https://discord.com/invite/yy75DKs) to get involved!

@ -0,0 +1,103 @@
---
title: What's the deal with SvelteKit?
description: We're rethinking how to build Svelte apps. Here's what you need to know
author: Rich Harris
authorURL: https://twitter.com/rich_harris
---
<aside><p>If you <em>didn't</em> attend Svelte Summit, you can catch up on the <a href="https://www.youtube.com/c/SvelteSociety/videos">Svelte Society YouTube page</a></p></aside>
If you attended [Svelte Summit](https://sveltesummit.com/) last month you may have seen my talk, Futuristic Web Development, in which I finally tackled one of the most frequently asked questions about Svelte: when will Sapper reach version 1.0?
The answer: never.
This was slightly tongue-in-cheek — as the talk explains, it's really more of a rewrite of Sapper coupled with a rebrand — but it raised a lot of new questions from the community, and it's time we offered a bit more clarity on what you can expect from Sapper's successor, SvelteKit.
<div class="max">
<figure style="max-width: 960px; margin: 0 auto">
<div style="height: 0; padding: 0 0 57.1% 0; position: relative; margin: 0 auto;">
<iframe style="position: absolute; width: 100%; height: 100%; left: 0; top: 0; margin: 0;" src="https://www.youtube-nocookie.com/embed/qSfdtmcZ4d0" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
<figcaption>'Futuristic Web Development' from <a href="https://sveltesummit.com/">Svelte Summit</a></figcaption>
</figure>
</div>
## What's Sapper?
[Sapper](https://sapper.svelte.dev) is an *app framework* (or 'metaframework') built on top of Svelte (which is a *component* framework). Its job is to make it easy to build Svelte apps with all the modern best practices like server-side rendering (SSR) and code-splitting, and to provide a project structure that makes development productive and fun. It uses *filesystem-based routing* (as popularised by [Next](https://nextjs.org/) and adopted by many other frameworks, albeit with some enhancements) — your project's file structure mirrors the structure of the app itself.
While the Svelte homepage and documentation encourages you to [degit](https://github.com/Rich-Harris/degit) the [sveltejs/template](https://github.com/sveltejs/template) repo to start building an app, Sapper has long been our recommended way to build apps; this very blog post is (at the time of writing!) rendered with Sapper.
## Why are we migrating to something new?
Firstly, the distinction between [sveltejs/template](https://github.com/sveltejs/template) and [sveltejs/sapper-template](https://github.com/sveltejs/sapper-template) is confusing, particularly to newcomers to Svelte. Having a single recommended way to start building apps with Svelte will bring enormous benefits: we simplify onboarding, reduce the maintenance and support burden, and can potentially begin to explore the new possibilities that are unlocked by having a predictable project structure. (This last part is deliberately vague because it will take time to fully understand what those possibilities are.)
Aside from all that, we've been tempted by the thought of rewriting Sapper for a while. This is partly because the codebase has become a little unkempt over the years ([Sapper started in 2017](/blog/sapper-towards-the-ideal-web-app-framework)), but mostly because the web has changed a lot recently, and it's time to rethink some of our foundational assumptions.
## How is this new thing different?
The first of those foundational assumptions is that you need to use a module bundler like [webpack](https://webpack.js.org/) or [Rollup](http://rollupjs.org/) to build apps. These tools trace the dependency graph of your application, analysing and transforming code along the way (turning Svelte components to JS modules, for example), in order to create bundles of code that can run anywhere. As the original creator of Rollup, I can attest that it is a surprisingly complex problem with fiendish edge cases.
You certainly needed a bundler several years ago, because browsers didn't natively support the `import` keyword, but it's much less true today. Right now, we're seeing the rise of the *unbundled development* workflow, which is radically simpler: instead of eagerly bundling your app, a dev server can serve modules (converted to JavaScript, if necessary) *on-demand*, meaning startup is essentially instantaneous however large your app becomes.
[Snowpack](https://www.snowpack.dev/) is at the vanguard of this movement, and it's what powers SvelteKit. It's astonishingly fast, and has a beautiful development experience (hot module reloading, error overlays and so on), and we've been working closely with the Snowpack team on features like SSR. The hot module reloading is particularly revelatory if you're used to using Sapper with Rollup (which has never had first-class HMR support owing to its architecture, which prioritises the most efficient output).
That's not to say we're abandoning bundlers altogether. It's still essential to optimise your app for production, and SvelteKit uses Rollup to make your apps as fast and lean as they possibly can be (which includes things like extracting styles into static `.css` files).
The other foundational assumption is that a server-rendered app needs, well, a server. Sapper effectively has two modes — `sapper build`, which creates a standalone app that has to run on a Node server, and `sapper export` which bakes your app out as a collection of static files suitable for hosting on services like GitHub Pages.
Static files can go pretty much anywhere, but running a Node server (and monitoring/scaling it etc) is less straightforward. Nowadays we're witnessing a shift towards *serverless platforms*, in which you as the app author don't need to think about the server your code is running on, with all the attendant complexity. You can get Sapper apps running on serverless platforms, thanks to things like [vercel-sapper](https://github.com/thgh/vercel-sapper), but it's certainly not what you'd call idiomatic.
<aside><p>It'll still be possible to create both Node apps and fully pre-rendered (aka exported) sites</a></p></aside>
SvelteKit fully embraces the serverless paradigm, and will launch with support for all the major serverless providers, with an 'adapter' API for targeting any platforms that we don't officially cater to. In addition, we'll be able to do partial pre-rendering, which means that static pages can be generated at build time but dynamic ones get rendered on-demand.
## When can I start using it?
If you're feeling brave, you can start right now:
```bash
npm init svelte@next
```
This will scaffold a new project and install the `@sveltejs/kit` CLI, which provides the tools for developing and building an app.
We don't recommend it though! There are no docs, and we won't be able to offer any form of support. It's also likely to break often.
The work is being done in a private monorepo while we're still in exploration mode. Our plan is to get a public beta ready and announce it here once we've closed a few issues — the repo itself will remain private at that time, but we'll create a place to collect feedback from the YOLO crowd. After that, we'll work towards a 1.0 release which will involve opening the repo up.
I'm not going to make any firm promises about timings, because I don't like to break promises. But I *think* we're talking about weeks rather than months.
## What if I don't want to use SvelteKit?
You won't have to — it will always be possible to use Svelte as a standalone package or via a bundler integration like [rollup-plugin-svelte](https://github.com/sveltejs/rollup-plugin-svelte). We think it's essential that you can bend Svelte to fit your workflow, however esoteric, and use third-party app frameworks like [Elder.js](https://github.com/Elderjs/elderjs), [Routify](https://routify.dev/), [Plenti](https://plenti.co/), [Crown](https://crownframework.com/), [JungleJS](https://www.junglejs.org/) and others.
## TypeScript?
Don't worry, we won't launch without full TypeScript support.
## How can I migrate my existing Sapper apps?
For the most part, it should be relatively straightforward to migrate a Sapper codebase.
There are some unavoidable changes (being able to run on serverless platforms means we need to replace custom `server.js` files and `(req, res) => {...}` functions with more portable equivalents), and we're taking the opportunity to fix a few design flaws, but on the whole a SvelteKit app will feel very familiar to Sapper users.
Detailed migration guides will accompany the 1.0 launch.
## How can I contribute?
Keep your eyes peeled for announcements about when we'll launch the public beta and open up the repo. (Also, blog post TODO but I would be remiss if I didn't mention that we now have an [OpenCollective](https://opencollective.com/svelte) where you can contribute financially to the project if it's been valuable to you. Many, many thanks to those of you who already have.)
## Where can I learn more?
Follow [@sveltejs](https://twitter.com/sveltejs) and [@SvelteSociety](https://twitter.com/SvelteSociety) on Twitter, and visit [svelte.dev/chat](https://svelte.dev/chat). You should also subscribe to [Svelte Radio](https://www.svelteradio.com/), where Kevin and his co-hosts will grill me about this project on an upcoming episode (and between now and next week when we record it, [reply to this Twitter thread](https://twitter.com/Rich_Harris/status/1323376048571121665) with your additional questions).

@ -300,6 +300,9 @@ An each block can also have an `{:else}` clause, which is rendered if the list i
```sv
{#await expression then name}...{/await}
```
```sv
{#await expression catch name}...{/await}
```
---
@ -342,6 +345,43 @@ If you don't care about the pending state, you can also omit the initial block.
{/await}
```
---
If conversely you only want to show the error state, you can omit the `then` block.
```sv
{#await promise catch error}
<p>The error is {error}</p>
{/await}
```
### {#key ...}
```sv
{#key expression}...{/key}
```
Key blocks destroy and recreate their contents when the value of an expression changes.
---
This is useful if you want an element to play its transition whenever a value changes.
```sv
{#key value}
<div transition:fade>{value}</div>
{/key}
```
---
When used around components, this will cause them to be reinstantiated and reinitialised.
```sv
{#key value}
<Component />
{/key}
```
### {@html ...}
@ -471,6 +511,7 @@ The following modifiers are available:
* `preventDefault` — calls `event.preventDefault()` before running the handler
* `stopPropagation` — calls `event.stopPropagation()`, preventing the event reaching the next element
* `passive` — improves scrolling performance on touch/wheel events (Svelte will add it automatically where it's safe to do so)
* `nonpassive` — explicitly set `passive: false`
* `capture` — fires the handler during the *capture* phase instead of the *bubbling* phase
* `once` — remove the handler after the first time it runs
* `self` — only trigger handler if event.target is the element itself
@ -1289,6 +1330,31 @@ Named slots allow consumers to target specific areas. They can also have fallbac
</div>
```
#### [`$$slots`](slots_object)
---
`$$slots` is an object whose keys are the names of the slots passed into the component by the parent. If the parent does not pass in a slot with a particular name, that name will not be a present in `$$slots`. This allows components to render a slot (and other elements, like wrappers for styling) only if the parent provides it.
Note that explicitly passing in an empty named slot will add that slot's name to `$$slots`. For example, if a parent passes `<div slot="title" />` to a child component, `$$slots.title` will be truthy within the child.
```sv
<!-- App.svelte -->
<Card>
<h1 slot="title">Blog Post Title</h1>
</Card>
<!-- Card.svelte -->
<div>
<slot name="title"></slot>
{#if $$slots.description}
<!-- This slot and the <hr> before it will not render. -->
<hr>
<slot name="description"></slot>
{/if}
</div>
```
#### [`<slot let:`*name*`={`*value*`}>`](slot_let)
---

@ -102,7 +102,7 @@ onDestroy(callback: () => void)
---
Schedules a callback to run once the component is unmounted.
Schedules a callback to run immediately before the component is unmounted.
Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the only one that runs inside a server-side component.
@ -178,6 +178,26 @@ Retrieves the context that belongs to the closest parent component with the spec
</script>
```
#### `hasContext`
```js
hasContext: boolean = hasContext(key: any)
```
---
Checks whether a given `key` has been set in the context of a parent component. Must be called during component initialisation.
```sv
<script>
import { hasContext } from 'svelte';
if (hasContext('answer')) {
// do something
}
</script>
```
#### `createEventDispatcher`
```js
@ -547,6 +567,7 @@ Animates the opacity of an element from 0 to the current opacity for `in` transi
* `delay` (`number`, default 0) — milliseconds before starting
* `duration` (`number`, default 400) — milliseconds the transition lasts
* `easing` (`function`, default `linear`) — an [easing function](docs#svelte_easing)
You can see the `fade` transition in action in the [transition tutorial](tutorial/transition).
@ -776,7 +797,7 @@ The `flip` function calculates the start and end position of an element and anim
* `delay` (`number`, default 0) — milliseconds before starting
* `duration` (`number` | `function`, default `d => Math.sqrt(d) * 120`) — see below
* `easing` (`function`, default [`cubicOut`](docs#cubicOut)) — an [easing function](docs#svelte_easing)
* `easing` (`function`, default `cubicOut`) — an [easing function](docs#svelte_easing)
`duration` can be be provided as either:

@ -115,8 +115,9 @@
on:mousedown={handleMousedown}
bind:currentTime={time}
bind:duration
bind:paused
></video>
bind:paused>
<track kind="captions"/>
</video>
<div class="controls" style="opacity: {duration && showControls ? 1 : 0}">
<progress value="{(time / duration) || 0}"/>

@ -0,0 +1,13 @@
<script>
import Profile from "./Profile.svelte";
</script>
<Profile>
<span slot="name">Bob</span>
<span slot="email">bob@email.com</span>
</Profile>
<Profile>
<span slot="name">Alice</span>
<span slot="phone">12345678</span>
</Profile>

@ -0,0 +1,24 @@
<style>
section {
width: 200px;
display: grid;
grid-template-columns: 1fr 1fr;
padding: 16px;
box-shadow: 2px 2px 4px #dedede;
border: 1px solid #888;
margin-bottom: 16px;
}
</style>
<section>
<div>Name</div>
<slot name="name" />
{#if $$slots.email}
<div>Email</div>
<slot name="email" />
{/if}
{#if $$slots.phone}
<div>Phone</div>
<slot name="phone" />
{/if}
</section>

@ -9,6 +9,10 @@ Rich Harris, the creator of Svelte, taught a course:
There are also a number of third-party courses:
- [Egghead](https://egghead.io/browse/frameworks/svelte)
- [Udemy](https://www.udemy.com/courses/search/?q=sveltejs+svelte)
- [Udemy](https://www.udemy.com/courses/search/?q=sveltejs+svelte) (Note: Udemy frequently has discounts over 90%)
- [Pluralsight](https://www.pluralsight.com/search?q=svelte)
Note that Udemy very frequently has discounts over 90%.
Finally, there are also YouTube channels and playlists that teach Svelte:
- [Svelte Master](https://www.youtube.com/channel/UCg6SQd5jnWo5Y70rZD9SQFA)
- [Svelte Tutorial for Beginners](https://www.youtube.com/watch?v=zojEMeQGGHs&list=PL4cUxeGkcC9hlbrVO_2QFVqVPhlZmz7tO) by The Net Ninja

@ -13,7 +13,7 @@ First, you'll need to integrate Svelte with a build tool. There are officially m
Don't worry if you're relatively new to web development and haven't used these tools before. We've prepared a simple step-by-step guide, [Svelte for new developers](blog/svelte-for-new-developers), which walks you through the process.
You'll also want to configure your text editor to treat `.svelte` files the same as `.html` for the sake of syntax highlighting. [Read this guide to learn how](blog/setting-up-your-editor).
You'll also want to configure your text editor. If you're using VS Code, install the [Svelte extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), otherwise follow [this guide](blog/setting-up-your-editor) to configure your text editor to treat `.svelte` files the same as `.html` for the sake of syntax highlighting.
Then, once you've got your project set up, using Svelte components is easy. The compiler turns each component into a regular JavaScript class — just import it and instantiate with `new`:

@ -1,6 +1,4 @@
<script>
let promise = getRandomNumber();
async function getRandomNumber() {
const res = await fetch(`tutorial/random-number`);
const text = await res.text();
@ -12,6 +10,8 @@
}
}
let promise = getRandomNumber();
function handleClick() {
promise = getRandomNumber();
}
@ -22,4 +22,4 @@
</button>
<!-- replace this element -->
<p>{promise}</p>
<p>{promise}</p>

@ -1,6 +1,4 @@
<script>
let promise = getRandomNumber();
async function getRandomNumber() {
const res = await fetch(`tutorial/random-number`);
const text = await res.text();
@ -11,6 +9,8 @@
throw new Error(text);
}
}
let promise = getRandomNumber();
function handleClick() {
promise = getRandomNumber();
@ -27,4 +27,4 @@
<p>The number is {number}</p>
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
{/await}

@ -21,6 +21,7 @@ The full list of modifiers:
* `preventDefault` — calls `event.preventDefault()` before running the handler. Useful for client-side form handling, for example.
* `stopPropagation` — calls `event.stopPropagation()`, preventing the event reaching the next element
* `passive` — improves scrolling performance on touch/wheel events (Svelte will add it automatically where it's safe to do so)
* `nonpassive` — explicitly set `passive: false`
* `capture` — fires the handler during the *capture* phase instead of the *bubbling* phase ([MDN docs](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_bubbling_and_capture))
* `once` — remove the handler after the first time it runs
* `self` — only trigger handler if event.target is the element itself

@ -112,8 +112,9 @@
poster="https://sveltejs.github.io/assets/caminandes-llamigos.jpg"
src="https://sveltejs.github.io/assets/caminandes-llamigos.mp4"
on:mousemove={handleMousemove}
on:mousedown={handleMousedown}
></video>
on:mousedown={handleMousedown}>
<track kind="captions">
</video>
<div class="controls" style="opacity: {duration && showControls ? 1 : 0}">
<progress value="{(time / duration) || 0}"/>

@ -115,8 +115,9 @@
on:mousedown={handleMousedown}
bind:currentTime={time}
bind:duration
bind:paused
></video>
bind:paused>
<track kind="captions">
</video>
<div class="controls" style="opacity: {duration && showControls ? 1 : 0}">
<progress value="{(time / duration) || 0}"/>

@ -5,9 +5,9 @@
onMount(() => {
const ctx = canvas.getContext('2d');
let frame;
let frame = requestAnimationFrame(loop);
(function loop() {
function loop(t) {
frame = requestAnimationFrame(loop);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
@ -17,8 +17,6 @@
const x = i % canvas.width;
const y = i / canvas.height >>> 0;
const t = window.performance.now();
const r = 64 + (128 * x / canvas.width) + (64 * Math.sin(t / 1000));
const g = 64 + (128 * y / canvas.height) + (64 * Math.cos(t / 1000));
const b = 128;
@ -30,7 +28,7 @@
}
ctx.putImageData(imageData, 0, 0);
}());
}
return () => {
cancelAnimationFrame(frame);

@ -5,9 +5,9 @@
onMount(() => {
const ctx = canvas.getContext('2d');
let frame;
let frame = requestAnimationFrame(loop);
(function loop() {
function loop(t) {
frame = requestAnimationFrame(loop);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
@ -17,8 +17,6 @@
const x = i % canvas.width;
const y = i / canvas.height >>> 0;
const t = window.performance.now();
const r = 64 + (128 * x / canvas.width) + (64 * Math.sin(t / 1000));
const g = 64 + (128 * y / canvas.height) + (64 * Math.cos(t / 1000));
const b = 128;
@ -30,7 +28,7 @@
}
ctx.putImageData(imageData, 0, 0);
}());
}
return () => {
cancelAnimationFrame(frame);

@ -0,0 +1,57 @@
<script>
import Project from './Project.svelte'
import Comment from './Comment.svelte'
</script>
<style>
h1 {
font-weight: 300;
margin: 0 1rem;
}
ul {
list-style: none;
padding: 0;
margin: 0.5rem;
display: flex;
}
@media (max-width: 600px) {
ul {
flex-direction: column;
}
}
li {
padding: 0.5rem;
flex: 1 1 50%;
min-width: 200px;
}
</style>
<h1>
Projects
</h1>
<ul>
<li>
<Project
title="Add Typescript support"
tasksCompleted={25}
totalTasks={57}
>
<div slot="comments">
<Comment name="Ecma Script" postedAt={new Date('2020-08-17T14:12:23')}>
<p>Those interface tests are now passing.</p>
</Comment>
</div>
</Project>
</li>
<li>
<Project
title="Update documentation"
tasksCompleted={18}
totalTasks={21}
/>
</li>
</ul>

@ -0,0 +1,56 @@
<script>
export let name;
export let postedAt;
$: avatar = `https://ui-avatars.com/api/?name=${name.replace(/ /g, '+')}&rounded=true&background=ff3e00&color=fff&bold=true`;
</script>
<style>
article {
background-color: #fff;
border: 1px #ccc solid;
border-radius: 4px;
padding: 1rem;
}
.header {
align-items: center;
display: flex;
}
.details {
flex: 1 1 auto;
margin-left: 0.5rem
}
h4 {
margin: 0;
}
time {
color: #777;
font-size: 0.75rem;
text-decoration: underline;
}
.body {
margin-top: 0.5rem;
}
.body :global(p) {
margin: 0;
}
</style>
<article>
<div class="header">
<img src={avatar} alt="" height="32" width="32">
<div class="details">
<h4>{name}</h4>
<time datetime={postedAt.toISOString()}>{postedAt.toLocaleDateString()}</time>
</div>
</div>
<div class="body">
<slot></slot>
</div>
</article>

@ -0,0 +1,62 @@
<script>
export let title;
export let tasksCompleted = 0;
export let totalTasks = 0;
</script>
<style>
article {
border: 1px #ccc solid;
border-radius: 4px;
position: relative;
}
article > div {
padding: 1.25rem;
}
article.has-discussion::after {
content: '';
background-color: #ff3e00;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
height: 20px;
position: absolute;
right: -10px;
top: -10px;
width: 20px;
}
h2,
h3 {
margin: 0 0 0.5rem;
}
h3 {
font-size: 0.875rem;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
}
p {
color: #777;
margin: 0;
}
.discussion {
background-color: #eee;
border-top: 1px #ccc solid;
}
</style>
<article class:has-discussion={true}>
<div>
<h2>{title}</h2>
<p>{tasksCompleted}/{totalTasks} tasks completed</p>
</div>
<div class="discussion">
<h3>Comments</h3>
<slot name="comments"></slot>
</div>
</article>

@ -0,0 +1,57 @@
<script>
import Project from './Project.svelte'
import Comment from './Comment.svelte'
</script>
<style>
h1 {
font-weight: 300;
margin: 0 1rem;
}
ul {
list-style: none;
padding: 0;
margin: 0.5rem;
display: flex;
}
@media (max-width: 600px) {
ul {
flex-direction: column;
}
}
li {
padding: 0.5rem;
flex: 1 1 50%;
min-width: 200px;
}
</style>
<h1>
Projects
</h1>
<ul>
<li>
<Project
title="Add Typescript support"
tasksCompleted={25}
totalTasks={57}
>
<div slot="comments">
<Comment name="Ecma Script" postedAt={new Date('2020-08-17T14:12:23')}>
<p>Those interface tests are now passing.</p>
</Comment>
</div>
</Project>
</li>
<li>
<Project
title="Update documentation"
tasksCompleted={18}
totalTasks={21}
/>
</li>
</ul>

@ -0,0 +1,56 @@
<script>
export let name;
export let postedAt;
$: avatar = `https://ui-avatars.com/api/?name=${name.replace(/ /g, '+')}&rounded=true&background=ff3e00&color=fff&bold=true`;
</script>
<style>
article {
background-color: #fff;
border: 1px #ccc solid;
border-radius: 4px;
padding: 1rem;
}
.header {
align-items: center;
display: flex;
}
.details {
flex: 1 1 auto;
margin-left: 0.5rem
}
h4 {
margin: 0;
}
time {
color: #777;
font-size: 0.75rem;
text-decoration: underline;
}
.body {
margin-top: 0.5rem;
}
.body :global(p) {
margin: 0;
}
</style>
<article>
<div class="header">
<img src={avatar} alt="" height="32" width="32">
<div class="details">
<h4>{name}</h4>
<time datetime={postedAt.toISOString()}>{postedAt.toLocaleDateString()}</time>
</div>
</div>
<div class="body">
<slot></slot>
</div>
</article>

@ -0,0 +1,64 @@
<script>
export let title;
export let tasksCompleted = 0;
export let totalTasks = 0;
</script>
<style>
article {
border: 1px #ccc solid;
border-radius: 4px;
position: relative;
}
article > div {
padding: 1.25rem;
}
article.has-discussion::after {
content: '';
background-color: #ff3e00;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
height: 20px;
position: absolute;
right: -10px;
top: -10px;
width: 20px;
}
h2,
h3 {
margin: 0 0 0.5rem;
}
h3 {
font-size: 0.875rem;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
}
p {
color: #777;
margin: 0;
}
.discussion {
background-color: #eee;
border-top: 1px #ccc solid;
}
</style>
<article class:has-discussion={$$slots.comments}>
<div>
<h2>{title}</h2>
<p>{tasksCompleted}/{totalTasks} tasks completed</p>
</div>
{#if $$slots.comments}
<div class="discussion">
<h3>Comments</h3>
<slot name="comments"></slot>
</div>
{/if}
</article>

@ -0,0 +1,28 @@
---
title: Checking for slot content
---
In some cases, you may want to control parts of your component based on whether the parent passes in content for a certain slot. Perhaps you have a wrapper around that slot, and you don't want to render it if the slot is empty. Or perhaps you'd like to apply a class only if the slot is present. You can do this by checking the properties of the special `$$slots` variable.
`$$slots` is an object whose keys are the names of the slots passed in by the parent component. If the parent leaves a slot empty, then `$$slots` will not have an entry for that slot.
Notice that both instances of `<Project>` in this example render a container for comments and a notification dot, even though only one has comments. We want to use `$$slots` to make sure we only render these elements when the parent `<App>` passes in content for the `comments` slot.
In `Project.svelte`, update the `class:has-discussion` directive on the `<article>`:
```html
<article class:has-discussion={$$slots.comments}>
```
Next, wrap the `comments` slot and its wrapping `<div>` in an `if` block that checks `$$slots`:
```html
{#if $$slots.comments}
<div class="discussion">
<h3>Comments</h3>
<slot name="comments"></slot>
</div>
{/if}
```
Now the comments container and the notification dot won't render when `<App>` leaves the `comments` slot empty.

@ -172,17 +172,4 @@
margin: 2em auto;
}
}
/* @media (min-width: 1460px) {
.post :global(iframe) {
width: 1360px;
margin: 2em -280px;
}
}
@media (min-height: 800px) {
.post :global(iframe) {
height: 640px;
}
} */
</style>

@ -7,12 +7,12 @@
/* colors --------------------------------- */
pre[class*='language-'] {
--background: var(--back-light);
--base: hsl(45, 7%, 45%);
--comment: hsl(210, 25%, 60%);
--keyword: hsl(204, 58%, 45%);
--function: hsl(19, 67%, 45%);
--string: hsl(41, 37%, 45%);
--number: hsl(102, 27%, 50%);
--base: #545454;
--comment: #696969;
--keyword: #007f8a;
--function: #bb5525;
--string: #856e3d;
--number: #008000;
--tags: var(--function);
--important: var(--string);
}

@ -29,6 +29,9 @@ import add_to_set from './utils/add_to_set';
import check_graph_for_cycles from './utils/check_graph_for_cycles';
import { print, x, b } from 'code-red';
import { is_reserved_keyword } from './utils/reserved_keywords';
import { apply_preprocessor_sourcemap } from '../utils/string_with_sourcemap';
import Element from './nodes/Element';
import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types';
interface ComponentOptions {
namespace?: string;
@ -85,6 +88,7 @@ export default class Component {
file: string;
locate: (c: number) => { line: number; column: number };
elements: Element[] = [];
stylesheet: Stylesheet;
aliases: Map<string, Identifier> = new Map();
@ -155,7 +159,7 @@ export default class Component {
) || { start: 0, end: 0 };
this.warn(svelteOptions, {
code: 'custom-element-no-tag',
message: `No custom element 'tag' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag="my-thing"/>. To hide this warning, use <svelte:options tag={null}/>`
message: 'No custom element \'tag\' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag="my-thing"/>. To hide this warning, use <svelte:options tag={null}/>'
});
}
this.tag = this.component_options.tag || compile_options.tag;
@ -171,8 +175,8 @@ export default class Component {
this.walk_instance_js_post_template();
this.elements.forEach(element => this.stylesheet.apply(element));
if (!compile_options.customElement) this.stylesheet.reify();
this.stylesheet.warn_on_unused_selectors(this);
}
@ -221,6 +225,10 @@ export default class Component {
return this.aliases.get(name);
}
apply_stylesheet(element: Element) {
this.elements.push(element);
}
global(name: string) {
const alias = this.alias(name);
this.globals.set(name, alias);
@ -235,7 +243,7 @@ export default class Component {
const { compile_options, name } = this;
const { format = 'esm' } = compile_options;
const banner = `${this.file ? `${this.file} ` : ``}generated by Svelte v${'__VERSION__'}`;
const banner = `${this.file ? `${this.file} ` : ''}generated by Svelte v${'__VERSION__'}`;
const program: any = { type: 'Program', body: result.js };
@ -261,17 +269,13 @@ export default class Component {
this.helpers.set(name, alias);
node.name = alias.name;
}
}
else if (node.name[0] !== '#' && !is_valid(node.name)) {
} else if (node.name[0] !== '#' && !is_valid(node.name)) {
// this hack allows x`foo.${bar}` where bar could be invalid
const literal: Literal = { type: 'Literal', value: node.name };
if (parent.type === 'Property' && key === 'key') {
parent.key = literal;
}
else if (parent.type === 'MemberExpression' && key === 'property') {
} else if (parent.type === 'MemberExpression' && key === 'property') {
parent.property = literal;
parent.computed = true;
}
@ -324,6 +328,8 @@ export default class Component {
js.map.sourcesContent = [
this.source
];
js.map = apply_preprocessor_sourcemap(this.file, js.map, compile_options.sourcemap as (string | RawSourceMap | DecodedSourceMap));
}
return {
@ -452,16 +458,16 @@ export default class Component {
extract_exports(node) {
if (node.type === 'ExportDefaultDeclaration') {
this.error(node, {
code: `default-export`,
message: `A component cannot have a default export`
code: 'default-export',
message: 'A component cannot have a default export'
});
}
if (node.type === 'ExportNamedDeclaration') {
if (node.source) {
this.error(node, {
code: `not-implemented`,
message: `A component currently cannot have an export ... from`
code: 'not-implemented',
message: 'A component currently cannot have an export ... from'
});
}
if (node.declaration) {
@ -472,7 +478,7 @@ export default class Component {
variable.export_name = name;
if (variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) {
this.warn(declarator, {
code: `unused-export-let`,
code: 'unused-export-let',
message: `${this.name.name} has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``
});
}
@ -495,7 +501,7 @@ export default class Component {
if (variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) {
this.warn(specifier, {
code: `unused-export-let`,
code: 'unused-export-let',
message: `${this.name.name} has unused export property '${specifier.exported.name}'. If it is for external reference only, please consider using \`export const ${specifier.exported.name}\``
});
}
@ -515,8 +521,7 @@ export default class Component {
if (this.hoistable_nodes.has(node)) return false;
if (this.reactive_declaration_nodes.has(node)) return false;
if (node.type === 'ImportDeclaration') return false;
if (node.type === 'ExportDeclaration' && node.specifiers.length > 0)
return false;
if (node.type === 'ExportDeclaration' && node.specifiers.length > 0) return false;
return true;
});
}
@ -544,7 +549,7 @@ export default class Component {
if (name[0] === '$') {
this.error(node as any, {
code: 'illegal-declaration',
message: `The $ prefix is reserved, and cannot be used for variable and import names`
message: 'The $ prefix is reserved, and cannot be used for variable and import names'
});
}
@ -562,7 +567,7 @@ export default class Component {
if (name[0] === '$') {
this.error(node as any, {
code: 'illegal-subscription',
message: `Cannot reference store value inside <script context="module">`
message: 'Cannot reference store value inside <script context="module">'
});
} else {
this.add_var({
@ -623,16 +628,18 @@ export default class Component {
if (name[0] === '$') {
this.error(node as any, {
code: 'illegal-declaration',
message: `The $ prefix is reserved, and cannot be used for variable and import names`
message: 'The $ prefix is reserved, and cannot be used for variable and import names'
});
}
const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let');
const imported = node.type.startsWith('Import');
this.add_var({
name,
initialised: instance_scope.initialised_declarations.has(name),
writable
writable,
imported
});
this.node_for_declaration.set(name, node);
@ -859,8 +866,8 @@ export default class Component {
if (name[1] !== '$' && scope.has(name.slice(1)) && scope.find_owner(name.slice(1)) !== this.instance_scope) {
this.error(node, {
code: `contextual-store`,
message: `Stores must be declared at the top level of the component (this may change in a future version of Svelte)`
code: 'contextual-store',
message: 'Stores must be declared at the top level of the component (this may change in a future version of Svelte)'
});
}
}
@ -927,7 +934,7 @@ export default class Component {
// TODO is this still true post-#3539?
component.error(declarator as any, {
code: 'destructured-prop',
message: `Cannot declare props in destructured declaration`
message: 'Cannot declare props in destructured declaration'
});
}
@ -1030,8 +1037,9 @@ export default class Component {
this.vars.find(
variable => variable.name === name && variable.module
)
)
) {
return false;
}
return true;
});
@ -1280,8 +1288,9 @@ export default class Component {
declaration.dependencies.forEach(name => {
if (declaration.assignees.has(name)) return;
const earlier_declarations = lookup.get(name);
if (earlier_declarations)
if (earlier_declarations) {
earlier_declarations.forEach(add_declaration);
}
});
this.reactive_declarations.push(declaration);
@ -1311,8 +1320,9 @@ export default class Component {
if (globals.has(name) && node.type !== 'InlineComponent') return;
let message = `'${name}' is not defined`;
if (!this.ast.instance)
if (!this.ast.instance) {
message += `. Consider adding a <script> block with 'export let ${name}' to declare a prop`;
}
this.warn(node, {
code: 'missing-declaration',
@ -1371,23 +1381,24 @@ function process_component_options(component: Component, nodes) {
switch (name) {
case 'tag': {
const code = 'invalid-tag-attribute';
const message = `'tag' must be a string literal`;
const message = "'tag' must be a string literal";
const tag = get_value(attribute, code, message);
if (typeof tag !== 'string' && tag !== null)
if (typeof tag !== 'string' && tag !== null) {
component.error(attribute, { code, message });
}
if (tag && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
component.error(attribute, {
code: `invalid-tag-property`,
message: `tag name must be two or more words joined by the '-' character`
code: 'invalid-tag-property',
message: "tag name must be two or more words joined by the '-' character"
});
}
if (tag && !component.compile_options.customElement) {
component.warn(attribute, {
code: 'missing-custom-element-compile-options',
message: `The 'tag' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?`
message: "The 'tag' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?"
});
}
@ -1397,22 +1408,23 @@ function process_component_options(component: Component, nodes) {
case 'namespace': {
const code = 'invalid-namespace-attribute';
const message = `The 'namespace' attribute must be a string literal representing a valid namespace`;
const message = "The 'namespace' attribute must be a string literal representing a valid namespace";
const ns = get_value(attribute, code, message);
if (typeof ns !== 'string')
if (typeof ns !== 'string') {
component.error(attribute, { code, message });
}
if (valid_namespaces.indexOf(ns) === -1) {
const match = fuzzymatch(ns, valid_namespaces);
if (match) {
component.error(attribute, {
code: `invalid-namespace-property`,
code: 'invalid-namespace-property',
message: `Invalid namespace '${ns}' (did you mean '${match}'?)`
});
} else {
component.error(attribute, {
code: `invalid-namespace-property`,
code: 'invalid-namespace-property',
message: `Invalid namespace '${ns}'`
});
}
@ -1429,8 +1441,9 @@ function process_component_options(component: Component, nodes) {
const message = `${name} attribute must be true or false`;
const value = get_value(attribute, code, message);
if (typeof value !== 'boolean')
if (typeof value !== 'boolean') {
component.error(attribute, { code, message });
}
component_options[name] = value;
break;
@ -1438,14 +1451,14 @@ function process_component_options(component: Component, nodes) {
default:
component.error(attribute, {
code: `invalid-options-attribute`,
message: `<svelte:options> unknown attribute`
code: 'invalid-options-attribute',
message: '<svelte:options> unknown attribute'
});
}
} else {
component.error(attribute, {
code: `invalid-options-attribute`,
message: `<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes`
code: 'invalid-options-attribute',
message: "<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes"
});
}
});

@ -4,12 +4,20 @@ import { gather_possible_values, UNKNOWN } from './gather_possible_values';
import { CssNode } from './interfaces';
import Component from '../Component';
import Element from '../nodes/Element';
import { INode } from '../nodes/interfaces';
import EachBlock from '../nodes/EachBlock';
import IfBlock from '../nodes/IfBlock';
import AwaitBlock from '../nodes/AwaitBlock';
enum BlockAppliesToNode {
NotPossible,
Possible,
UnknownSelectorType
}
enum NodeExist {
Probably = 1,
Definitely = 2,
}
const whitelist_attribute_selector = new Map([
['details', new Set(['open'])]
@ -39,10 +47,10 @@ export default class Selector {
this.used = this.local_blocks.length === 0;
}
apply(node: Element, stack: Element[]) {
apply(node: Element) {
const to_encapsulate: any[] = [];
apply_selector(this.local_blocks.slice(), node, stack.slice(), to_encapsulate);
apply_selector(this.local_blocks.slice(), node, to_encapsulate);
if (to_encapsulate.length > 0) {
to_encapsulate.forEach(({ node, block }) => {
@ -110,8 +118,8 @@ export default class Selector {
const selector = block.selectors[i];
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
component.error(selector, {
code: `css-invalid-global`,
message: `:global(...) must be the first element in a compound selector`
code: 'css-invalid-global',
message: ':global(...) must be the first element in a compound selector'
});
}
}
@ -131,8 +139,8 @@ export default class Selector {
for (let i = start; i < end; i += 1) {
if (this.blocks[i].global) {
component.error(this.blocks[i].selectors[0], {
code: `css-invalid-global`,
message: `:global(...) can be at the start or end of a selector sequence, but not in the middle`
code: 'css-invalid-global',
message: ':global(...) can be at the start or end of a selector sequence, but not in the middle'
});
}
}
@ -149,7 +157,7 @@ export default class Selector {
}
}
function apply_selector(blocks: Block[], node: Element, stack: Element[], to_encapsulate: any[]): boolean {
function apply_selector(blocks: Block[], node: Element, to_encapsulate: any[]): boolean {
const block = blocks.pop();
if (!block) return false;
@ -162,7 +170,7 @@ function apply_selector(blocks: Block[], node: Element, stack: Element[], to_enc
return false;
case BlockAppliesToNode.UnknownSelectorType:
// bail. TODO figure out what these could be
// bail. TODO figure out what these could be
to_encapsulate.push({ node, block });
return true;
}
@ -174,9 +182,10 @@ function apply_selector(blocks: Block[], node: Element, stack: Element[], to_enc
continue;
}
for (const stack_node of stack) {
if (block_might_apply_to_node(ancestor_block, stack_node) !== BlockAppliesToNode.NotPossible) {
to_encapsulate.push({ node: stack_node, block: ancestor_block });
let parent = node;
while (parent = get_element_parent(parent)) {
if (block_might_apply_to_node(ancestor_block, parent) !== BlockAppliesToNode.NotPossible) {
to_encapsulate.push({ node: parent, block: ancestor_block });
}
}
@ -193,12 +202,22 @@ function apply_selector(blocks: Block[], node: Element, stack: Element[], to_enc
return false;
} else if (block.combinator.name === '>') {
if (apply_selector(blocks, stack.pop(), stack, to_encapsulate)) {
if (apply_selector(blocks, get_element_parent(node), to_encapsulate)) {
to_encapsulate.push({ node, block });
return true;
}
return false;
} else if (block.combinator.name === '+' || block.combinator.name === '~') {
const siblings = get_possible_element_siblings(node, block.combinator.name === '+');
let has_match = false;
for (const possible_sibling of siblings.keys()) {
if (apply_selector(blocks.slice(), possible_sibling, to_encapsulate)) {
to_encapsulate.push({ node, block });
has_match = true;
}
}
return has_match;
}
// TODO other combinators
@ -229,25 +248,17 @@ function block_might_apply_to_node(block: Block, node: Element): BlockAppliesToN
if (selector.type === 'ClassSelector') {
if (!attribute_matches(node, 'class', name, '~=', false) && !node.classes.some(c => c.name === name)) return BlockAppliesToNode.NotPossible;
}
else if (selector.type === 'IdSelector') {
} else if (selector.type === 'IdSelector') {
if (!attribute_matches(node, 'id', name, '=', false)) return BlockAppliesToNode.NotPossible;
}
else if (selector.type === 'AttributeSelector') {
} else if (selector.type === 'AttributeSelector') {
if (
!(whitelist_attribute_selector.has(node.name.toLowerCase()) && whitelist_attribute_selector.get(node.name.toLowerCase()).has(selector.name.name.toLowerCase())) &&
!attribute_matches(node, selector.name.name, selector.value && unquote(selector.value), selector.matcher, selector.flags)) {
return BlockAppliesToNode.NotPossible;
}
}
else if (selector.type === 'TypeSelector') {
} else if (selector.type === 'TypeSelector') {
if (node.name.toLowerCase() !== name.toLowerCase() && name !== '*') return BlockAppliesToNode.NotPossible;
}
else {
} else {
return BlockAppliesToNode.UnknownSelectorType;
}
}
@ -267,7 +278,7 @@ function test_attribute(operator, expected_value, case_insensitive, value) {
case '^=': return value.startsWith(expected_value);
case '$=': return value.endsWith(expected_value);
case '*=': return value.includes(expected_value);
default: throw new Error(`this shouldn't happen`);
default: throw new Error("this shouldn't happen");
}
}
@ -376,6 +387,158 @@ function unquote(value: CssNode) {
return str;
}
function get_element_parent(node: Element): Element | null {
let parent: INode = node;
while ((parent = parent.parent) && parent.type !== 'Element');
return parent as Element | null;
}
function get_possible_element_siblings(node: INode, adjacent_only: boolean): Map<Element, NodeExist> {
const result: Map<Element, NodeExist> = new Map();
let prev: INode = node;
while (prev = prev.prev) {
if (prev.type === 'Element') {
if (!prev.attributes.find(attr => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot')) {
result.set(prev, NodeExist.Definitely);
}
if (adjacent_only) {
break;
}
} else if (prev.type === 'EachBlock' || prev.type === 'IfBlock' || prev.type === 'AwaitBlock') {
const possible_last_child = get_possible_last_child(prev, adjacent_only);
add_to_map(possible_last_child, result);
if (adjacent_only && has_definite_elements(possible_last_child)) {
return result;
}
}
}
if (!prev || !adjacent_only) {
let parent: INode = node;
let skip_each_for_last_child = node.type === 'ElseBlock';
while ((parent = parent.parent) && (parent.type === 'EachBlock' || parent.type === 'IfBlock' || parent.type === 'ElseBlock' || parent.type === 'AwaitBlock')) {
const possible_siblings = get_possible_element_siblings(parent, adjacent_only);
add_to_map(possible_siblings, result);
if (parent.type === 'EachBlock') {
// first child of each block can select the last child of each block as previous sibling
if (skip_each_for_last_child) {
skip_each_for_last_child = false;
} else {
add_to_map(get_possible_last_child(parent, adjacent_only), result);
}
} else if (parent.type === 'ElseBlock') {
skip_each_for_last_child = true;
parent = parent.parent;
}
if (adjacent_only && has_definite_elements(possible_siblings)) {
break;
}
}
}
return result;
}
function get_possible_last_child(block: EachBlock | IfBlock | AwaitBlock, adjacent_only: boolean): Map<Element, NodeExist> {
const result: Map<Element, NodeExist> = new Map();
if (block.type === 'EachBlock') {
const each_result: Map<Element, NodeExist> = loop_child(block.children, adjacent_only);
const else_result: Map<Element, NodeExist> = block.else ? loop_child(block.else.children, adjacent_only) : new Map();
const not_exhaustive = !has_definite_elements(else_result);
if (not_exhaustive) {
mark_as_probably(each_result);
mark_as_probably(else_result);
}
add_to_map(each_result, result);
add_to_map(else_result, result);
} else if (block.type === 'IfBlock') {
const if_result: Map<Element, NodeExist> = loop_child(block.children, adjacent_only);
const else_result: Map<Element, NodeExist> = block.else ? loop_child(block.else.children, adjacent_only) : new Map();
const not_exhaustive = !has_definite_elements(if_result) || !has_definite_elements(else_result);
if (not_exhaustive) {
mark_as_probably(if_result);
mark_as_probably(else_result);
}
add_to_map(if_result, result);
add_to_map(else_result, result);
} else if (block.type === 'AwaitBlock') {
const pending_result: Map<Element, NodeExist> = block.pending ? loop_child(block.pending.children, adjacent_only) : new Map();
const then_result: Map<Element, NodeExist> = block.then ? loop_child(block.then.children, adjacent_only) : new Map();
const catch_result: Map<Element, NodeExist> = block.catch ? loop_child(block.catch.children, adjacent_only) : new Map();
const not_exhaustive = !has_definite_elements(pending_result) || !has_definite_elements(then_result) || !has_definite_elements(catch_result);
if (not_exhaustive) {
mark_as_probably(pending_result);
mark_as_probably(then_result);
mark_as_probably(catch_result);
}
add_to_map(pending_result, result);
add_to_map(then_result, result);
add_to_map(catch_result, result);
}
return result;
}
function has_definite_elements(result: Map<Element, NodeExist>): boolean {
if (result.size === 0) return false;
for (const exist of result.values()) {
if (exist === NodeExist.Definitely) {
return true;
}
}
return false;
}
function add_to_map(from: Map<Element, NodeExist>, to: Map<Element, NodeExist>) {
from.forEach((exist, element) => {
to.set(element, higher_existance(exist, to.get(element)));
});
}
function higher_existance(exist1: NodeExist | null, exist2: NodeExist | null): NodeExist {
if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
return exist1 > exist2 ? exist1 : exist2;
}
function mark_as_probably(result: Map<Element, NodeExist>) {
for (const key of result.keys()) {
result.set(key, NodeExist.Probably);
}
}
function loop_child(children: INode[], adjacent_only: boolean) {
const result: Map<Element, NodeExist> = new Map();
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (child.type === 'Element') {
result.set(child, NodeExist.Definitely);
if (adjacent_only) {
break;
}
} else if (child.type === 'EachBlock' || child.type === 'IfBlock' || child.type === 'AwaitBlock') {
const child_result = get_possible_last_child(child, adjacent_only);
add_to_map(child_result, result);
if (adjacent_only && has_definite_elements(child_result)) {
break;
}
}
}
return result;
}
class Block {
global: boolean;
combinator: CssNode;

@ -2,10 +2,10 @@ import MagicString from 'magic-string';
import { walk } from 'estree-walker';
import Selector from './Selector';
import Element from '../nodes/Element';
import { Ast, TemplateNode } from '../../interfaces';
import { Ast } from '../../interfaces';
import Component from '../Component';
import { CssNode } from './interfaces';
import hash from "../utils/hash";
import hash from '../utils/hash';
function remove_css_prefix(name: string): string {
return name.replace(/^-((webkit)|(moz)|(o)|(ms))-/, '');
@ -51,8 +51,8 @@ class Rule {
this.declarations = node.block.children.map((node: CssNode) => new Declaration(node));
}
apply(node: Element, stack: Element[]) {
this.selectors.forEach(selector => selector.apply(node, stack)); // TODO move the logic in here?
apply(node: Element) {
this.selectors.forEach(selector => selector.apply(node)); // TODO move the logic in here?
}
is_used(dev: boolean) {
@ -162,14 +162,12 @@ class Atrule {
this.declarations = [];
}
apply(node: Element, stack: Element[]) {
apply(node: Element) {
if (this.node.name === 'media' || this.node.name === 'supports') {
this.children.forEach(child => {
child.apply(node, stack);
child.apply(node);
});
}
else if (is_keyframes_node(this.node)) {
} else if (is_keyframes_node(this.node)) {
this.children.forEach((rule: Rule) => {
rule.selectors.forEach(selector => {
selector.used = true;
@ -364,15 +362,9 @@ export default class Stylesheet {
apply(node: Element) {
if (!this.has_styles) return;
const stack: Element[] = [];
let parent: TemplateNode = node;
while (parent = parent.parent) {
if (parent.type === 'Element') stack.unshift(parent as Element);
}
for (let i = 0; i < this.children.length; i += 1) {
const child = this.children[i];
child.apply(node, stack);
child.apply(node);
}
}
@ -434,7 +426,7 @@ export default class Stylesheet {
this.children.forEach(child => {
child.warn_on_unused_selector((selector: Selector) => {
component.warn(selector.node, {
code: `css-unused-selector`,
code: 'css-unused-selector',
message: `Unused CSS selector "${this.source.slice(selector.node.start, selector.node.end)}"`
});
});

@ -5,14 +5,10 @@ export const UNKNOWN = {};
export function gather_possible_values(node: Node, set: Set<string|{}>) {
if (node.type === 'Literal') {
set.add(node.value);
}
else if (node.type === 'ConditionalExpression') {
} else if (node.type === 'ConditionalExpression') {
gather_possible_values(node.consequent, set);
gather_possible_values(node.alternate, set);
}
else {
} else {
set.add(UNKNOWN);
}
}
}

@ -3,4 +3,4 @@ export interface CssNode {
start: number;
end: number;
[prop_name: string]: any;
}
}

@ -1,4 +1,3 @@
import { assign } from '../../runtime/internal/utils';
import Stats from '../Stats';
import parse from '../parse/index';
import render_dom from './render_dom/index';
@ -12,6 +11,7 @@ const valid_options = [
'format',
'name',
'filename',
'sourcemap',
'generate',
'outputFilename',
'cssOutputFilename',
@ -47,9 +47,9 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
}
if (name && /^[a-z]/.test(name)) {
const message = `options.name should be capitalised`;
const message = 'options.name should be capitalised';
warnings.push({
code: `options-lowercase-name`,
code: 'options-lowercase-name',
message,
filename,
toString: () => message
@ -59,7 +59,7 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
if (loopGuardTimeout && !dev) {
const message = 'options.loopGuardTimeout is for options.dev = true only';
warnings.push({
code: `options-loop-guard-timeout`,
code: 'options-loop-guard-timeout',
message,
filename,
toString: () => message
@ -68,7 +68,7 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
}
export default function compile(source: string, options: CompileOptions = {}) {
options = assign({ generate: 'dom', dev: false }, options);
options = Object.assign({ generate: 'dom', dev: false }, options);
const stats = new Stats();
const warnings = [];

@ -11,10 +11,11 @@ export default class Action extends Node {
constructor(component: Component, parent, scope, info) {
super(component, parent, scope, info);
component.warn_if_undefined(info.name, info, scope);
const object = info.name.split('.')[0];
component.warn_if_undefined(object, info, scope);
this.name = info.name;
component.add_reference(info.name.split('.')[0]);
component.add_reference(object);
this.expression = info.expression
? new Expression(component, this, scope, info.expression)
@ -22,4 +23,4 @@ export default class Action extends Node {
this.uses_context = this.expression && this.expression.uses_context;
}
}
}

@ -17,8 +17,8 @@ export default class Animation extends Node {
if (parent.animation) {
component.error(this, {
code: `duplicate-animation`,
message: `An element can only have one 'animate' directive`
code: 'duplicate-animation',
message: "An element can only have one 'animate' directive"
});
}
@ -26,8 +26,8 @@ export default class Animation extends Node {
if (!block || block.type !== 'EachBlock' || !block.key) {
// TODO can we relax the 'immediate child' rule?
component.error(this, {
code: `invalid-animation`,
message: `An element that use the animate directive must be the immediate child of a keyed each block`
code: 'invalid-animation',
message: 'An element that uses the animate directive must be the immediate child of a keyed each block'
});
}
@ -37,4 +37,4 @@ export default class Animation extends Node {
? new Expression(component, this, scope, info.expression, true)
: null;
}
}
}

@ -38,9 +38,7 @@ export default class Attribute extends Node {
this.chunks = null;
this.is_static = false;
}
else {
} else {
this.name = info.name;
this.is_true = info.value === true;
this.is_static = true;

@ -3,7 +3,7 @@ import get_object from '../utils/get_object';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import {dimensions} from "../../utils/patterns";
import {dimensions} from '../../utils/patterns';
import { Node as ESTreeNode } from 'estree';
// TODO this should live in a specific binding
@ -67,17 +67,21 @@ export default class Binding extends Node {
} else {
const variable = component.var_lookup.get(name);
if (!variable || variable.global) component.error(this.expression.node, {
code: 'binding-undeclared',
message: `${name} is not declared`
});
if (!variable || variable.global) {
component.error(this.expression.node, {
code: 'binding-undeclared',
message: `${name} is not declared`
});
}
variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
if (info.expression.type === 'Identifier' && !variable.writable) component.error(this.expression.node, {
code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable'
});
if (info.expression.type === 'Identifier' && !variable.writable) {
component.error(this.expression.node, {
code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable'
});
}
}
const type = parent.get_static_attribute_value('type');

@ -13,9 +13,7 @@ export default class Body extends Node {
info.attributes.forEach(node => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(component, this, scope, node));
}
else {
} else {
// TODO there shouldn't be anything else here...
}
});

@ -15,4 +15,4 @@ export default class Class extends Node {
? new Expression(component, this, scope, info.expression)
: null;
}
}
}

@ -14,4 +14,4 @@ export default class Comment extends Node {
const match = pattern.exec(this.data);
this.ignores = match ? match[1].split(/[^\S]/).map(x => x.trim()).filter(Boolean) : [];
}
}
}

@ -60,8 +60,8 @@ export default class EachBlock extends AbstractBlock {
if (this.children.length !== 1) {
const child = this.children.find(child => !!(child as Element).animation);
component.error((child as Element).animation, {
code: `invalid-animation`,
message: `An element that use the animate directive must be the sole child of a keyed each block`
code: 'invalid-animation',
message: 'An element that uses the animate directive must be the sole child of a keyed each block'
});
}
}

@ -16,6 +16,7 @@ import list from '../../utils/list';
import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import Component from '../Component';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|svg|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;
@ -62,14 +63,14 @@ const a11y_no_onchange = new Set([
]);
const a11y_labelable = new Set([
"button",
"input",
"keygen",
"meter",
"output",
"progress",
"select",
"textarea"
'button',
'input',
'keygen',
'meter',
'output',
'progress',
'select',
'textarea'
]);
const invisible_elements = new Set(['meta', 'html', 'script', 'style']);
@ -80,6 +81,7 @@ const valid_modifiers = new Set([
'capture',
'once',
'passive',
'nonpassive',
'self'
]);
@ -123,7 +125,7 @@ export default class Element extends Node {
namespace: string;
needs_manual_style_scoping: boolean;
constructor(component, parent, scope, info: any) {
constructor(component: Component, parent, scope, info: any) {
super(component, parent, scope, info);
this.name = info.name;
@ -134,8 +136,8 @@ export default class Element extends Node {
const value_attribute = info.attributes.find(node => node.name === 'value');
if (value_attribute) {
component.error(value_attribute, {
code: `textarea-duplicate-value`,
message: `A <textarea> can have either a value attribute or (equivalently) child content, but not both`
code: 'textarea-duplicate-value',
message: 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
});
}
@ -235,14 +237,21 @@ export default class Element extends Node {
this.validate();
component.stylesheet.apply(this);
component.apply_stylesheet(this);
}
validate() {
if (this.component.var_lookup.has(this.name) && this.component.var_lookup.get(this.name).imported) {
this.component.warn(this, {
code: 'component-name-lowercase',
message: `<${this.name}> will be treated as an HTML element unless it begins with a capital letter`
});
}
if (a11y_distracting_elements.has(this.name)) {
// no-distracting-elements
this.component.warn(this, {
code: `a11y-distracting-elements`,
code: 'a11y-distracting-elements',
message: `A11y: Avoid <${this.name}> elements`
});
}
@ -264,8 +273,8 @@ export default class Element extends Node {
if (!is_figure_parent) {
this.component.warn(this, {
code: `a11y-structure`,
message: `A11y: <figcaption> must be an immediate child of <figure>`
code: 'a11y-structure',
message: 'A11y: <figcaption> must be an immediate child of <figure>'
});
}
}
@ -281,8 +290,8 @@ export default class Element extends Node {
if (index !== -1 && (index !== 0 && index !== children.length - 1)) {
this.component.warn(children[index], {
code: `a11y-structure`,
message: `A11y: <figcaption> must be first or last child of <figure>`
code: 'a11y-structure',
message: 'A11y: <figcaption> must be first or last child of <figure>'
});
}
}
@ -309,7 +318,7 @@ export default class Element extends Node {
if (invisible_elements.has(this.name)) {
// aria-unsupported-elements
component.warn(attribute, {
code: `a11y-aria-attributes`,
code: 'a11y-aria-attributes',
message: `A11y: <${this.name}> should not have aria-* attributes`
});
}
@ -321,14 +330,14 @@ export default class Element extends Node {
if (match) message += ` (did you mean '${match}'?)`;
component.warn(attribute, {
code: `a11y-unknown-aria-attribute`,
code: 'a11y-unknown-aria-attribute',
message
});
}
if (name === 'aria-hidden' && /^h[1-6]$/.test(this.name)) {
component.warn(attribute, {
code: `a11y-hidden`,
code: 'a11y-hidden',
message: `A11y: <${this.name}> element should not be hidden`
});
}
@ -339,7 +348,7 @@ export default class Element extends Node {
if (invisible_elements.has(this.name)) {
// aria-unsupported-elements
component.warn(attribute, {
code: `a11y-misplaced-role`,
code: 'a11y-misplaced-role',
message: `A11y: <${this.name}> should not have role attribute`
});
}
@ -353,7 +362,7 @@ export default class Element extends Node {
if (match) message += ` (did you mean '${match}'?)`;
component.warn(attribute, {
code: `a11y-unknown-role`,
code: 'a11y-unknown-role',
message
});
}
@ -362,24 +371,24 @@ export default class Element extends Node {
// no-access-key
if (name === 'accesskey') {
component.warn(attribute, {
code: `a11y-accesskey`,
message: `A11y: Avoid using accesskey`
code: 'a11y-accesskey',
message: 'A11y: Avoid using accesskey'
});
}
// no-autofocus
if (name === 'autofocus') {
component.warn(attribute, {
code: `a11y-autofocus`,
message: `A11y: Avoid using autofocus`
code: 'a11y-autofocus',
message: 'A11y: Avoid using autofocus'
});
}
// scope
if (name === 'scope' && this.name !== 'th') {
component.warn(attribute, {
code: `a11y-misplaced-scope`,
message: `A11y: The scope attribute should only be used with <th> elements`
code: 'a11y-misplaced-scope',
message: 'A11y: The scope attribute should only be used with <th> elements'
});
}
@ -389,8 +398,8 @@ export default class Element extends Node {
// @ts-ignore todo is tabindex=true correct case?
if (!isNaN(value) && +value > 0) {
component.warn(attribute, {
code: `a11y-positive-tabindex`,
message: `A11y: avoid tabindex values above zero`
code: 'a11y-positive-tabindex',
message: 'A11y: avoid tabindex values above zero'
});
}
}
@ -398,7 +407,7 @@ export default class Element extends Node {
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
component.error(attribute, {
code: `illegal-attribute`,
code: 'illegal-attribute',
message: `'${name}' is not a valid attribute name`
});
}
@ -406,14 +415,14 @@ export default class Element extends Node {
if (name === 'slot') {
if (!attribute.is_static) {
component.error(attribute, {
code: `invalid-slot-attribute`,
message: `slot attribute cannot have a dynamic value`
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
});
}
if (component.slot_outlets.has(name)) {
component.error(attribute, {
code: `duplicate-slot-attribute`,
code: 'duplicate-slot-attribute',
message: `Duplicate '${name}' slot`
});
@ -422,8 +431,8 @@ export default class Element extends Node {
if (!(parent.type === 'InlineComponent' || within_custom_element(parent))) {
component.error(attribute, {
code: `invalid-slotted-content`,
message: `Element with a slot='...' attribute must be a child of a component or a descendant of a custom element`
code: 'invalid-slotted-content',
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
});
}
}
@ -431,7 +440,7 @@ export default class Element extends Node {
if (name === 'is') {
component.warn(attribute, {
code: 'avoid-is',
message: `The 'is' attribute is not supported cross-browser and should be avoided`
message: 'The \'is\' attribute is not supported cross-browser and should be avoided'
});
}
@ -462,7 +471,7 @@ export default class Element extends Node {
if (href_value === '' || href_value === '#' || /^\W*javascript:/i.test(href_value)) {
component.warn(href_attribute, {
code: `a11y-invalid-attribute`,
code: 'a11y-invalid-attribute',
message: `A11y: '${href_value}' is not a valid ${href_attribute.name} attribute`
});
}
@ -472,8 +481,8 @@ export default class Element extends Node {
if (!id_attribute_valid && !name_attribute_valid) {
component.warn(this, {
code: `a11y-missing-attribute`,
message: `A11y: <a> element should have an href attribute`
code: 'a11y-missing-attribute',
message: 'A11y: <a> element should have an href attribute'
});
}
}
@ -511,8 +520,8 @@ export default class Element extends Node {
if (/\b(image|picture|photo)\b/i.test(alt_value)) {
component.warn(this, {
code: `a11y-img-redundant-alt`,
message: `A11y: Screenreaders already announce <img> elements as an image.`
code: 'a11y-img-redundant-alt',
message: 'A11y: Screenreaders already announce <img> elements as an image.'
});
}
}
@ -522,8 +531,8 @@ export default class Element extends Node {
const has_input_child = this.children.some(i => (i instanceof Element && a11y_labelable.has(i.name) ));
if (!attribute_map.has('for') && !has_input_child) {
component.warn(this, {
code: `a11y-label-has-associated-control`,
message: `A11y: A form label must be associated with a control.`
code: 'a11y-label-has-associated-control',
message: 'A11y: A form label must be associated with a control.'
});
}
}
@ -541,8 +550,8 @@ export default class Element extends Node {
if (!has_caption) {
component.warn(this, {
code: `a11y-media-has-caption`,
message: `A11y: Media elements must have a <track kind="captions">`
code: 'a11y-media-has-caption',
message: 'A11y: Media elements must have a <track kind="captions">'
});
}
}
@ -550,8 +559,8 @@ export default class Element extends Node {
if (a11y_no_onchange.has(this.name)) {
if (handlers_map.has('change') && !handlers_map.has('blur')) {
component.warn(this, {
code: `a11y-no-onchange`,
message: `A11y: on:blur must be used instead of on:change, unless absolutely necessary and it causes no negative consequences for keyboard only or screen reader users.`
code: 'a11y-no-onchange',
message: 'A11y: on:blur must be used instead of on:change, unless absolutely necessary and it causes no negative consequences for keyboard only or screen reader users.'
});
}
}
@ -569,8 +578,8 @@ export default class Element extends Node {
if (!attribute.is_static) {
component.error(attribute, {
code: `invalid-type`,
message: `'type' attribute cannot be dynamic if input uses two-way binding`
code: 'invalid-type',
message: '\'type\' attribute cannot be dynamic if input uses two-way binding'
});
}
@ -578,8 +587,8 @@ export default class Element extends Node {
if (value === true) {
component.error(attribute, {
code: `missing-type`,
message: `'type' attribute must be specified`
code: 'missing-type',
message: '\'type\' attribute must be specified'
});
}
@ -596,7 +605,7 @@ export default class Element extends Node {
this.name !== 'select'
) {
component.error(binding, {
code: `invalid-binding`,
code: 'invalid-binding',
message: `'value' is not a valid binding on <${this.name}> elements`
});
}
@ -608,8 +617,8 @@ export default class Element extends Node {
if (attribute && !attribute.is_static) {
component.error(attribute, {
code: `dynamic-multiple-attribute`,
message: `'multiple' attribute cannot be dynamic if select uses two-way binding`
code: 'dynamic-multiple-attribute',
message: '\'multiple\' attribute cannot be dynamic if select uses two-way binding'
});
}
} else {
@ -618,7 +627,7 @@ export default class Element extends Node {
} else if (name === 'checked' || name === 'indeterminate') {
if (this.name !== 'input') {
component.error(binding, {
code: `invalid-binding`,
code: 'invalid-binding',
message: `'${name}' is not a valid binding on <${this.name}> elements`
});
}
@ -627,13 +636,13 @@ export default class Element extends Node {
if (type !== 'checkbox') {
let message = `'${name}' binding can only be used with <input type="checkbox">`;
if (type === 'radio') message += ` — for <input type="radio">, use 'group' binding`;
component.error(binding, { code: `invalid-binding`, message });
if (type === 'radio') message += ' — for <input type="radio">, use \'group\' binding';
component.error(binding, { code: 'invalid-binding', message });
}
} else if (name === 'group') {
if (this.name !== 'input') {
component.error(binding, {
code: `invalid-binding`,
code: 'invalid-binding',
message: `'group' is not a valid binding on <${this.name}> elements`
});
}
@ -642,14 +651,14 @@ export default class Element extends Node {
if (type !== 'checkbox' && type !== 'radio') {
component.error(binding, {
code: `invalid-binding`,
message: `'group' binding can only be used with <input type="checkbox"> or <input type="radio">`
code: 'invalid-binding',
message: '\'group\' binding can only be used with <input type="checkbox"> or <input type="radio">'
});
}
} else if (name === 'files') {
if (this.name !== 'input') {
component.error(binding, {
code: `invalid-binding`,
code: 'invalid-binding',
message: `'files' is not a valid binding on <${this.name}> elements`
});
}
@ -658,15 +667,15 @@ export default class Element extends Node {
if (type !== 'file') {
component.error(binding, {
code: `invalid-binding`,
message: `'files' binding can only be used with <input type="file">`
code: 'invalid-binding',
message: '\'files\' binding can only be used with <input type="file">'
});
}
} else if (name === 'open') {
if (this.name !== 'details') {
component.error(binding, {
code: `invalid-binding`,
code: 'invalid-binding',
message: `'${name}' binding can only be used with <details>`
});
}
@ -685,7 +694,7 @@ export default class Element extends Node {
) {
if (this.name !== 'audio' && this.name !== 'video') {
component.error(binding, {
code: `invalid-binding`,
code: 'invalid-binding',
message: `'${name}' binding can only be used with <audio> or <video>`
});
}
@ -695,7 +704,7 @@ export default class Element extends Node {
) {
if (this.name !== 'video') {
component.error(binding, {
code: `invalid-binding`,
code: 'invalid-binding',
message: `'${name}' binding can only be used with <video>`
});
}
@ -726,18 +735,18 @@ export default class Element extends Node {
if (!contenteditable) {
component.error(binding, {
code: `missing-contenteditable-attribute`,
message: `'contenteditable' attribute is required for textContent and innerHTML two-way bindings`
code: 'missing-contenteditable-attribute',
message: '\'contenteditable\' attribute is required for textContent and innerHTML two-way bindings'
});
} else if (contenteditable && !contenteditable.is_static) {
component.error(contenteditable, {
code: `dynamic-contenteditable-attribute`,
message: `'contenteditable' attribute cannot be dynamic if element uses two-way binding`
code: 'dynamic-contenteditable-attribute',
message: '\'contenteditable\' attribute cannot be dynamic if element uses two-way binding'
});
}
} else if (name !== 'this') {
component.error(binding, {
code: `invalid-binding`,
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding`
});
}
@ -753,7 +762,7 @@ export default class Element extends Node {
if (this.children.length === 0) {
this.component.warn(this, {
code: `a11y-missing-content`,
code: 'a11y-missing-content',
message: `A11y: <${this.name}> element should have child content`
});
}
@ -766,7 +775,14 @@ export default class Element extends Node {
if (handler.modifiers.has('passive') && handler.modifiers.has('preventDefault')) {
component.error(handler, {
code: 'invalid-event-modifier',
message: `The 'passive' and 'preventDefault' modifiers cannot be used together`
message: 'The \'passive\' and \'preventDefault\' modifiers cannot be used together'
});
}
if (handler.modifiers.has('passive') && handler.modifiers.has('nonpassive')) {
component.error(handler, {
code: 'invalid-event-modifier',
message: 'The \'passive\' and \'nonpassive\' modifiers cannot be used together'
});
}
@ -783,13 +799,13 @@ export default class Element extends Node {
if (handler.can_make_passive) {
component.warn(handler, {
code: 'redundant-event-modifier',
message: `Touch event handlers that don't use the 'event' object are passive by default`
message: 'Touch event handlers that don\'t use the \'event\' object are passive by default'
});
}
} else {
component.warn(handler, {
code: 'redundant-event-modifier',
message: `The passive modifier only works with wheel and touch events`
message: 'The passive modifier only works with wheel and touch events'
});
}
}
@ -804,7 +820,7 @@ export default class Element extends Node {
}
});
if (passive_events.has(handler.name) && handler.can_make_passive && !handler.modifiers.has('preventDefault')) {
if (passive_events.has(handler.name) && handler.can_make_passive && !handler.modifiers.has('preventDefault') && !handler.modifiers.has('nonpassive')) {
// touch/wheel events should be passive by default
handler.modifiers.add('passive');
}
@ -860,7 +876,7 @@ function should_have_attribute(
attributes[0];
node.component.warn(node, {
code: `a11y-missing-attribute`,
code: 'a11y-missing-attribute',
message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
});
}

@ -12,8 +12,8 @@ export default class Head extends Node {
if (info.attributes.length) {
component.error(info.attributes[0], {
code: `invalid-attribute`,
message: `<svelte:head> should not have any attributes or directives`
code: 'invalid-attribute',
message: '<svelte:head> should not have any attributes or directives'
});
}

@ -40,15 +40,15 @@ export default class InlineComponent extends Node {
switch (node.type) {
case 'Action':
component.error(node, {
code: `invalid-action`,
message: `Actions can only be applied to DOM elements, not components`
code: 'invalid-action',
message: 'Actions can only be applied to DOM elements, not components'
});
case 'Attribute':
if (node.name === 'slot') {
component.error(node, {
code: `invalid-prop`,
message: `'slot' is reserved for future use in named slots`
code: 'invalid-prop',
message: "'slot' is reserved for future use in named slots"
});
}
// fallthrough
@ -62,8 +62,8 @@ export default class InlineComponent extends Node {
case 'Class':
component.error(node, {
code: `invalid-class`,
message: `Classes can only be applied to DOM elements, not components`
code: 'invalid-class',
message: 'Classes can only be applied to DOM elements, not components'
});
case 'EventHandler':
@ -76,8 +76,8 @@ export default class InlineComponent extends Node {
case 'Transition':
component.error(node, {
code: `invalid-transition`,
message: `Transitions can only be applied to DOM elements, not components`
code: 'invalid-transition',
message: 'Transitions can only be applied to DOM elements, not components'
});
default:
@ -105,7 +105,7 @@ export default class InlineComponent extends Node {
if (modifier !== 'once') {
component.error(handler, {
code: 'invalid-event-modifier',
message: `Event modifiers other than 'once' can only be used on DOM elements`
message: "Event modifiers other than 'once' can only be used on DOM elements"
});
}
});

@ -0,0 +1,19 @@
import Expression from './shared/Expression';
import map_children from './shared/map_children';
import AbstractBlock from './shared/AbstractBlock';
export default class KeyBlock extends AbstractBlock {
type: 'KeyBlock';
expression: Expression;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.expression = new Expression(component, this, scope, info.expression);
this.children = map_children(component, this, scope, info.children);
this.warn_if_empty_block();
}
}

@ -26,7 +26,7 @@ export default class Let extends Node {
if (!applicable.has(node.type)) {
component.error(node as any, {
code: 'invalid-let',
message: `let directive value must be an identifier or an object/array pattern`
message: 'let directive value must be an identifier or an object/array pattern'
});
}

@ -17,24 +17,24 @@ export default class Slot extends Element {
info.attributes.forEach(attr => {
if (attr.type !== 'Attribute') {
component.error(attr, {
code: `invalid-slot-directive`,
message: `<slot> cannot have directives`
code: 'invalid-slot-directive',
message: '<slot> cannot have directives'
});
}
if (attr.name === 'name') {
if (attr.value.length !== 1 || attr.value[0].type !== 'Text') {
component.error(attr, {
code: `dynamic-slot-name`,
message: `<slot> name cannot be dynamic`
code: 'dynamic-slot-name',
message: '<slot> name cannot be dynamic'
});
}
this.slot_name = attr.value[0].data;
if (this.slot_name === 'default') {
component.error(attr, {
code: `invalid-slot-name`,
message: `default is a reserved word — it cannot be used as a slot name`
code: 'invalid-slot-name',
message: 'default is a reserved word — it cannot be used as a slot name'
});
}
}

@ -37,7 +37,7 @@ export default class Text extends Node {
// svg namespace exclusions
if (/svg$/.test(parent_element.namespace)) {
if (this.prev && this.prev.type === "Element" && this.prev.name === "tspan") return false;
if (this.prev && this.prev.type === 'Element' && this.prev.name === 'tspan') return false;
}
return parent_element.namespace || elements_without_text.has(parent_element.name);

@ -13,8 +13,8 @@ export default class Title extends Node {
if (info.attributes.length > 0) {
component.error(info.attributes[0], {
code: `illegal-attribute`,
message: `<title> cannot have attributes`
code: 'illegal-attribute',
message: '<title> cannot have attributes'
});
}
@ -22,7 +22,7 @@ export default class Title extends Node {
if (child.type !== 'Text' && child.type !== 'MustacheTag') {
component.error(child, {
code: 'illegal-structure',
message: `<title> can only contain text and {tags}`
message: '<title> can only contain text and {tags}'
});
}
});

@ -28,7 +28,7 @@ export default class Transition extends Node {
: `An element cannot have both ${describe(parent_transition)} directive and ${describe(this)} directive`;
component.error(info, {
code: `duplicate-transition`,
code: 'duplicate-transition',
message
});
}
@ -41,6 +41,6 @@ export default class Transition extends Node {
function describe(transition: Transition) {
return transition.directive === 'transition'
? `a 'transition'`
? "a 'transition'"
: `an '${transition.directive}'`;
}
}

@ -28,15 +28,13 @@ export default class Window extends Node {
info.attributes.forEach(node => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(component, this, scope, node));
}
else if (node.type === 'Binding') {
} else if (node.type === 'Binding') {
if (node.expression.type !== 'Identifier') {
const { parts } = flatten_reference(node.expression);
// TODO is this constraint necessary?
component.error(node.expression, {
code: `invalid-binding`,
code: 'invalid-binding',
message: `Bindings on <svelte:window> must be to top-level properties, e.g. '${parts[parts.length - 1]}' rather than '${parts.join('.')}'`
});
}
@ -52,25 +50,21 @@ export default class Window extends Node {
if (match) {
component.error(node, {
code: `invalid-binding`,
code: 'invalid-binding',
message: `${message} (did you mean '${match}'?)`
});
} else {
component.error(node, {
code: `invalid-binding`,
code: 'invalid-binding',
message: `${message} — valid bindings are ${list(valid_bindings)}`
});
}
}
this.bindings.push(new Binding(component, this, scope, node));
}
else if (node.type === 'Action') {
} else if (node.type === 'Action') {
this.actions.push(new Action(component, this, scope, node));
}
else {
} else {
// TODO there shouldn't be anything else here...
}
});

@ -18,6 +18,7 @@ import Fragment from './Fragment';
import Head from './Head';
import IfBlock from './IfBlock';
import InlineComponent from './InlineComponent';
import KeyBlock from './KeyBlock';
import Let from './Let';
import MustacheTag from './MustacheTag';
import Options from './Options';
@ -31,7 +32,7 @@ import Transition from './Transition';
import Window from './Window';
// note: to write less types each of types in union below should have type defined as literal
// https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
// https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#discriminating-unions
export type INode = Action
| Animation
| Attribute
@ -50,6 +51,7 @@ export type INode = Action
| Head
| IfBlock
| InlineComponent
| KeyBlock
| Let
| MustacheTag
| Options

@ -4,7 +4,6 @@ import is_reference from 'is-reference';
import flatten_reference from '../../utils/flatten_reference';
import { create_scopes, Scope, extract_names } from '../../utils/scope';
import { sanitize } from '../../../utils/names';
import Wrapper from '../../render_dom/wrappers/shared/Wrapper';
import TemplateScope from './TemplateScope';
import get_object from '../../utils/get_object';
import Block from '../../render_dom/Block';
@ -12,12 +11,12 @@ import is_dynamic from '../../render_dom/wrappers/shared/is_dynamic';
import { b } from 'code-red';
import { invalidate } from '../../render_dom/invalidate';
import { Node, FunctionExpression, Identifier } from 'estree';
import { TemplateNode } from '../../../interfaces';
import { INode } from '../interfaces';
import { is_reserved_keyword } from '../../utils/reserved_keywords';
import replace_object from '../../utils/replace_object';
import EachBlock from '../EachBlock';
type Owner = Wrapper | TemplateNode;
type Owner = INode;
export default class Expression {
type: 'Expression' = 'Expression';
@ -37,7 +36,6 @@ export default class Expression {
manipulated: Node;
// todo: owner type
constructor(component: Component, owner: Owner, template_scope: TemplateScope, info, lazy?: boolean) {
// TODO revert to direct property access in prod?
Object.defineProperties(this, {
@ -85,8 +83,8 @@ export default class Expression {
const store_name = name.slice(1);
if (template_scope.names.has(store_name) || scope.has(store_name)) {
component.error(node, {
code: `contextual-store`,
message: `Stores must be declared at the top level of the component (this may change in a future version of Svelte)`
code: 'contextual-store',
message: 'Stores must be declared at the top level of the component (this may change in a future version of Svelte)'
});
}
}
@ -263,23 +261,21 @@ export default class Expression {
hoistable: true,
referenced: true
});
}
else if (contextual_dependencies.size === 0) {
} else if (contextual_dependencies.size === 0) {
// function can be hoisted inside the component init
component.partly_hoisted.push(declaration);
block.renderer.add_to_context(id.name);
this.replace(block.renderer.reference(id));
}
else {
} else {
// we need a combo block/init recipe
const deps = Array.from(contextual_dependencies);
const function_expression = node as FunctionExpression;
(node as FunctionExpression).params = [
const has_args = function_expression.params.length > 0;
function_expression.params = [
...deps.map(name => ({ type: 'Identifier', name } as Identifier)),
...(node as FunctionExpression).params
...function_expression.params
];
const context_args = deps.map(name => block.renderer.reference(name));
@ -291,18 +287,49 @@ export default class Expression {
this.replace(id as any);
if ((node as FunctionExpression).params.length > 0) {
declarations.push(b`
function ${id}(...args) {
return ${callee}(${context_args}, ...args);
}
`);
const func_declaration = has_args
? b`function ${id}(...args) {
return ${callee}(${context_args}, ...args);
}`
: b`function ${id}() {
return ${callee}(${context_args});
}`;
if (owner.type === 'Attribute' && owner.parent.name === 'slot') {
const dep_scopes = new Set<INode>(deps.map(name => template_scope.get_owner(name)));
// find the nearest scopes
let node: INode = owner.parent;
while (node && !dep_scopes.has(node)) {
node = node.parent;
}
const func_expression = func_declaration[0];
if (node.type === 'InlineComponent') {
// <Comp let:data />
this.replace(func_expression);
} else {
// {#each}, {#await}
const func_id = component.get_unique_name(id.name + '_func');
block.renderer.add_to_context(func_id.name, true);
// rename #ctx -> child_ctx;
walk(func_expression, {
enter(node) {
if (node.type === 'Identifier' && node.name === '#ctx') {
node.name = 'child_ctx';
}
}
});
// add to get_xxx_context
// child_ctx[x] = function () { ... }
(template_scope.get_owner(deps[0]) as EachBlock).contexts.push({
key: func_id,
modifier: () => func_expression
});
this.replace(block.renderer.reference(func_id));
}
} else {
declarations.push(b`
function ${id}() {
return ${callee}(${context_args});
}
`);
declarations.push(func_declaration);
}
}

@ -6,6 +6,7 @@ import Element from '../Element';
import Head from '../Head';
import IfBlock from '../IfBlock';
import InlineComponent from '../InlineComponent';
import KeyBlock from '../KeyBlock';
import MustacheTag from '../MustacheTag';
import Options from '../Options';
import RawMustacheTag from '../RawMustacheTag';
@ -28,6 +29,7 @@ function get_constructor(type) {
case 'Head': return Head;
case 'IfBlock': return IfBlock;
case 'InlineComponent': return InlineComponent;
case 'KeyBlock': return KeyBlock;
case 'MustacheTag': return MustacheTag;
case 'Options': return Options;
case 'RawMustacheTag': return RawMustacheTag;

@ -486,4 +486,4 @@ export default class Block {
}
}
}
}
}

@ -111,8 +111,9 @@ export default class Renderer {
// these determine whether variable is included in initial context
// array, so must have the highest priority
if (variable.export_name) member.priority += 16;
if (variable.referenced) member.priority += 32;
if (variable.is_reactive_dependency && (variable.mutated || variable.reassigned)) member.priority += 16;
if (variable.export_name) member.priority += 32;
if (variable.referenced) member.priority += 64;
} else if (member.is_non_contextual) {
// determine whether variable is included in initial context
// array, so must have the highest priority
@ -131,7 +132,7 @@ export default class Renderer {
while (i--) {
const member = this.context[i];
if (member.variable) {
if (member.variable.referenced || member.variable.export_name) break;
if (member.variable.referenced || member.variable.export_name || (member.variable.is_reactive_dependency && (member.variable.mutated || member.variable.reassigned))) break;
} else if (member.is_non_contextual) {
break;
}
@ -235,7 +236,7 @@ export default class Renderer {
if (!member) return;
if (member.index.value === -1) {
throw new Error(`unset index`);
throw new Error('unset index');
}
const value = member.index.value as number;

@ -7,6 +7,8 @@ import { extract_names, Scope } from '../utils/scope';
import { invalidate } from './invalidate';
import Block from './Block';
import { ClassDeclaration, FunctionExpression, Node, Statement, ObjectExpression, Expression } from 'estree';
import { apply_preprocessor_sourcemap } from '../../utils/string_with_sourcemap';
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
export default function dom(
component: Component,
@ -30,6 +32,9 @@ export default function dom(
}
const css = component.stylesheet.render(options.filename, !options.customElement);
css.map = apply_preprocessor_sourcemap(options.filename, css.map, options.sourcemap as string | RawSourceMap | DecodedSourceMap);
const styles = component.stylesheet.has_styles && options.dev
? `${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */`
: css.code;
@ -81,7 +86,7 @@ export default function dom(
const uses_props = component.var_lookup.has('$$props');
const uses_rest = component.var_lookup.has('$$restProps');
const $$props = uses_props || uses_rest ? `$$new_props` : `$$props`;
const $$props = uses_props || uses_rest ? '$$new_props' : '$$props';
const props = component.vars.filter(variable => !variable.module && variable.export_name);
const writable_props = props.filter(variable => variable.writable);
@ -467,6 +472,12 @@ export default function dom(
}
if (options.customElement) {
let init_props = x`@attribute_to_object(this.attributes)`;
if (uses_slots) {
init_props = x`{ ...${init_props}, $$slots: @get_custom_elements_slots(this) }`;
}
const declaration = b`
class ${name} extends @SvelteElement {
constructor(options) {
@ -474,7 +485,7 @@ export default function dom(
${css.code && b`this.shadowRoot.innerHTML = \`<style>${css.code.replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
@init(this, { target: this.shadowRoot }, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_indexes}, ${dirty});
@init(this, { target: this.shadowRoot, props: ${init_props} }, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_indexes}, ${dirty});
${dev_props_check}
@ -524,7 +535,7 @@ export default function dom(
const declaration = b`
class ${name} extends ${superclass} {
constructor(options) {
super(${options.dev && `options`});
super(${options.dev && 'options'});
${should_add_css && b`if (!@_document.getElementById("${component.stylesheet.id}-style")) ${add_css}();`}
@init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_indexes}, ${dirty});
${options.dev && b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`}

@ -36,47 +36,46 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
return renderer.invalidate(variable.name, undefined, main_execution_context);
}
if (head) {
component.has_reactive_assignments = true;
if (node.type === 'AssignmentExpression' && node.operator === '=' && nodes_match(node.left, node.right) && tail.length === 0) {
return get_invalidated(head, node);
} else {
const is_store_value = head.name[0] === '$' && head.name[1] !== '$';
const extra_args = tail.map(variable => get_invalidated(variable)).filter(Boolean);
const pass_value = (
!main_execution_context &&
(
extra_args.length > 0 ||
(node.type === 'AssignmentExpression' && node.left.type !== 'Identifier') ||
(node.type === 'UpdateExpression' && (!node.prefix || node.argument.type !== 'Identifier'))
)
);
if (!head) {
return node;
}
if (pass_value) {
extra_args.unshift({
type: 'Identifier',
name: head.name
});
}
component.has_reactive_assignments = true;
let invalidate = is_store_value
? x`@set_store_value(${head.name.slice(1)}, ${node}, ${head.name})`
: !main_execution_context
? x`$$invalidate(${renderer.context_lookup.get(head.name).index}, ${node}, ${extra_args})`
: extra_args.length
? [node, ...extra_args]
: node;
if (node.type === 'AssignmentExpression' && node.operator === '=' && nodes_match(node.left, node.right) && tail.length === 0) {
return get_invalidated(head, node);
}
if (head.subscribable && head.reassigned) {
const subscribe = `$$subscribe_${head.name}`;
invalidate = x`${subscribe}(${invalidate})`;
}
const is_store_value = head.name[0] === '$' && head.name[1] !== '$';
const extra_args = tail.map(variable => get_invalidated(variable)).filter(Boolean);
return invalidate;
if (is_store_value) {
return x`@set_store_value(${head.name.slice(1)}, ${node}, ${head.name}, ${extra_args})`;
}
let invalidate;
if (!main_execution_context) {
const pass_value = (
extra_args.length > 0 ||
(node.type === 'AssignmentExpression' && node.left.type !== 'Identifier') ||
(node.type === 'UpdateExpression' && (!node.prefix || node.argument.type !== 'Identifier'))
);
if (pass_value) {
extra_args.unshift({
type: 'Identifier',
name: head.name
});
}
invalidate = x`$$invalidate(${renderer.context_lookup.get(head.name).index}, ${node}, ${extra_args})`;
} else {
// skip `$$invalidate` if it is in the main execution context
invalidate = extra_args.length ? [node, ...extra_args] : node;
}
if (head.subscribable && head.reassigned) {
const subscribe = `$$subscribe_${head.name}`;
invalidate = x`${subscribe}(${invalidate})`;
}
return node;
}
return invalidate;
}

@ -96,7 +96,7 @@ class AwaitBlockBranch extends Wrapper {
`);
this.block.chunks.declarations.push(b`${get_context}(#ctx)`);
if (this.block.has_update_method) {
this.block.chunks.update.push(b`${get_context}(#ctx)`);
this.block.chunks.update.unshift(b`${get_context}(#ctx)`);
}
}
}
@ -177,8 +177,8 @@ export default class AwaitBlockWrapper extends Wrapper {
const snippet = this.node.expression.manipulate(block);
const info = block.get_unique_name(`info`);
const promise = block.get_unique_name(`promise`);
const info = block.get_unique_name('info');
const promise = block.get_unique_name('promise');
block.add_variable(promise);

@ -29,7 +29,7 @@ export class ElseBlockWrapper extends Wrapper {
this.block = block.child({
comment: create_debugging_comment(node, this.renderer.component),
name: this.renderer.component.get_unique_name(`create_else_block`),
name: this.renderer.component.get_unique_name('create_else_block'),
type: 'else'
});
@ -196,11 +196,6 @@ export default class EachBlockWrapper extends Wrapper {
? !this.next.is_dom_node() :
!parent_node || !this.parent.is_dom_node();
this.context_props = this.node.contexts.map(prop => b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.modifier(x`list[i]`)};`);
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;`);
const snippet = this.node.expression.manipulate(block);
block.chunks.init.push(b`let ${this.vars.each_block_value} = ${snippet};`);
@ -208,15 +203,6 @@ export default class EachBlockWrapper extends Wrapper {
block.chunks.init.push(b`@validate_each_argument(${this.vars.each_block_value});`);
}
// TODO which is better — Object.create(array) or array.slice()?
renderer.blocks.push(b`
function ${this.vars.get_each_context}(#ctx, list, i) {
const child_ctx = #ctx.slice();
${this.context_props}
return child_ctx;
}
`);
const initial_anchor_node: Identifier = { type: 'Identifier', name: parent_node ? 'null' : '#anchor' };
const initial_mount_node: Identifier = parent_node || { type: 'Identifier', name: '#target' };
const update_anchor_node = needs_anchor
@ -239,6 +225,11 @@ export default class EachBlockWrapper extends Wrapper {
this.node.expression.dynamic_dependencies().forEach((dependency: string) => {
all_dependencies.add(dependency);
});
if (this.node.key) {
this.node.key.dynamic_dependencies().forEach((dependency: string) => {
all_dependencies.add(dependency);
});
}
this.dependencies = all_dependencies;
if (this.node.key) {
@ -355,6 +346,19 @@ export default class EachBlockWrapper extends Wrapper {
if (this.else) {
this.else.fragment.render(this.else.block, null, x`#nodes` as Identifier);
}
this.context_props = this.node.contexts.map(prop => b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.modifier(x`list[i]`)};`);
if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`);
if (this.node.has_binding || this.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`);
// TODO which is better — Object.create(array) or array.slice()?
renderer.blocks.push(b`
function ${this.vars.get_each_context}(#ctx, list, i) {
const child_ctx = #ctx.slice();
${this.context_props}
return child_ctx;
}
`);
}
render_keyed({
@ -436,11 +440,11 @@ export default class EachBlockWrapper extends Wrapper {
const destroy = this.node.has_animation
? (this.block.has_outros
? `@fix_and_outro_and_destroy_block`
: `@fix_and_destroy_block`)
? '@fix_and_outro_and_destroy_block'
: '@fix_and_destroy_block')
: this.block.has_outros
? `@outro_and_destroy_block`
: `@destroy_block`;
? '@outro_and_destroy_block'
: '@destroy_block';
if (this.dependencies.size) {
this.updates.push(b`
@ -558,7 +562,7 @@ export default class EachBlockWrapper extends Wrapper {
}
`;
const start = this.block.has_update_method ? 0 : `#old_length`;
const start = this.block.has_update_method ? 0 : '#old_length';
let remove_old_blocks;

@ -48,9 +48,10 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
// special case — <option value={foo}> — see below
if (this.parent.node.name === 'option' && node.name === 'value') {
let select: ElementWrapper = this.parent;
while (select && (select.node.type !== 'Element' || select.node.name !== 'select'))
while (select && (select.node.type !== 'Element' || select.node.name !== 'select')) {
// @ts-ignore todo: doublecheck this, but looks to be correct
select = select.parent;
}
if (select && select.select_binding_dependencies) {
select.select_binding_dependencies.forEach(prop => {
@ -187,7 +188,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
if (this.is_input_value) {
const type = element.node.get_static_attribute_value('type');
if (type === null || type === "" || type === "text" || type === "email" || type === "password") {
if (type === null || type === '' || type === 'text' || type === 'email' || type === 'password') {
condition = x`${condition} && ${element.var}.${property_name} !== ${should_cache ? last : value}`;
}
}
@ -277,7 +278,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
if (this.node.is_true) return '';
const value = this.node.chunks;
if (value.length === 0) return `=""`;
if (value.length === 0) return '=""';
return `="${value.map(chunk => {
return chunk.type === 'Text'
@ -387,4 +388,4 @@ function is_indirectly_bound_value(attribute: AttributeWrapper) {
(binding) =>
/checked|group/.test(binding.name)
)));
}
}

@ -88,20 +88,20 @@ export default class BindingWrapper {
update_conditions.push(block.renderer.dirty(dependency_array));
}
if (parent.node.name === "input") {
const type = parent.node.get_static_attribute_value("type");
if (parent.node.name === 'input') {
const type = parent.node.get_static_attribute_value('type');
if (
type === null ||
type === "" ||
type === "text" ||
type === "email" ||
type === "password"
type === '' ||
type === 'text' ||
type === 'email' ||
type === 'password'
) {
update_conditions.push(
x`${parent.var}.${this.node.name} !== ${this.snippet}`
);
} else if (type === "number") {
} else if (type === 'number') {
update_conditions.push(
x`@to_number(${parent.var}.${this.node.name}) !== ${this.snippet}`
);
@ -118,7 +118,7 @@ export default class BindingWrapper {
{
const { binding_group, is_context, contexts, index } = get_binding_group(parent.renderer, this.node, block);
block.renderer.add_to_context(`$$binding_groups`);
block.renderer.add_to_context('$$binding_groups');
if (is_context) {
if (contexts.length > 1) {

@ -45,11 +45,17 @@ export default class EventHandlerWrapper {
const args = [];
const opts = ['passive', 'once', 'capture'].filter(mod => this.node.modifiers.has(mod));
const opts = ['nonpassive', 'passive', 'once', 'capture'].filter(mod => this.node.modifiers.has(mod));
if (opts.length) {
args.push((opts.length === 1 && opts[0] === 'capture')
? TRUE
: x`{ ${opts.map(opt => p`${opt}: true`)} }`);
if (opts.length === 1 && opts[0] === 'capture') {
args.push(TRUE);
} else {
args.push(x`{ ${ opts.map(opt =>
opt === 'nonpassive'
? p`passive: false`
: p`${opt}: true`
) } }`);
}
} else if (block.renderer.options.dev) {
args.push(FALSE);
}

@ -1,3 +1,3 @@
import { BaseAttributeWrapper } from "./Attribute";
import { BaseAttributeWrapper } from './Attribute';
export default class SpreadAttributeWrapper extends BaseAttributeWrapper {}

@ -164,9 +164,7 @@ function get_style_value(chunks: Array<Text | Expression>) {
break;
}
}
else {
} else {
value.push(chunk);
}
}

@ -0,0 +1,61 @@
import ElementWrapper from './index';
import SlotWrapper from '../Slot';
import Block from '../../Block';
import { sanitize } from '../../../../utils/names';
import InlineComponentWrapper from '../InlineComponent';
import create_debugging_comment from '../shared/create_debugging_comment';
import { get_slot_definition } from '../shared/get_slot_definition';
export default function create_slot_block(attribute, element: ElementWrapper | SlotWrapper, block: Block) {
const owner = find_slot_owner(element.parent);
if (owner && owner.node.type === 'InlineComponent') {
const name = attribute.get_static_value() as string;
if (!((owner as unknown) as InlineComponentWrapper).slots.has(name)) {
const child_block = block.child({
comment: create_debugging_comment(element.node, element.renderer.component),
name: element.renderer.component.get_unique_name(
`create_${sanitize(name)}_slot`
),
type: 'slot'
});
const { scope, lets } = element.node;
const seen = new Set(lets.map(l => l.name.name));
((owner as unknown) as InlineComponentWrapper).node.lets.forEach(l => {
if (!seen.has(l.name.name)) lets.push(l);
});
((owner as unknown) as InlineComponentWrapper).slots.set(
name,
get_slot_definition(child_block, scope, lets)
);
element.renderer.blocks.push(child_block);
}
element.slot_block = ((owner as unknown) as InlineComponentWrapper).slots.get(
name
).block;
return element.slot_block;
}
return block;
}
function find_slot_owner(owner) {
while (owner) {
if (owner.node.type === 'InlineComponent') {
break;
}
if (owner.node.type === 'Element' && /-/.test(owner.node.name)) {
break;
}
owner = owner.parent;
}
return owner;
}

@ -1,13 +1,13 @@
import AttributeWrapper from "./Attribute";
import BindingWrapper from "./Binding";
import ElementWrapper from "./index";
import AttributeWrapper from './Attribute';
import BindingWrapper from './Binding';
import ElementWrapper from './index';
export default function handle_select_value_binding(
attr: AttributeWrapper | BindingWrapper,
dependencies: Set<string>
) {
const { parent } = attr;
if (parent.node.name === "select") {
if (parent.node.name === 'select') {
(parent as ElementWrapper).select_binding_dependencies = dependencies;
dependencies.forEach((prop: string) => {
parent.renderer.component.indirect_dependencies.set(prop, new Set());

@ -2,7 +2,7 @@ import Renderer from '../../Renderer';
import Element from '../../../nodes/Element';
import Wrapper from '../shared/Wrapper';
import Block from '../../Block';
import { is_void, sanitize } from '../../../../utils/names';
import { is_void } from '../../../../utils/names';
import FragmentWrapper from '../Fragment';
import { escape_html, string_literal } from '../../../utils/stringify';
import TextWrapper from '../Text';
@ -14,12 +14,9 @@ import StyleAttributeWrapper from './StyleAttribute';
import SpreadAttributeWrapper from './SpreadAttribute';
import { dimensions } from '../../../../utils/patterns';
import Binding from './Binding';
import InlineComponentWrapper from '../InlineComponent';
import add_to_set from '../../../utils/add_to_set';
import { add_event_handler } from '../shared/add_event_handlers';
import { add_action } from '../shared/add_actions';
import create_debugging_comment from '../shared/create_debugging_comment';
import { get_slot_definition } from '../shared/get_slot_definition';
import bind_this from '../shared/bind_this';
import { is_head } from '../shared/is_head';
import { Identifier } from 'estree';
@ -28,6 +25,7 @@ import { extract_names } from 'periscopic';
import Action from '../../../nodes/Action';
import MustacheTagWrapper from '../MustacheTag';
import RawMustacheTagWrapper from '../RawMustacheTag';
import create_slot_block from './create_slot_block';
interface BindingGroup {
events: string[];
@ -177,47 +175,7 @@ export default class ElementWrapper extends Wrapper {
this.attributes = this.node.attributes.map(attribute => {
if (attribute.name === 'slot') {
// TODO make separate subclass for this?
let owner = this.parent;
while (owner) {
if (owner.node.type === 'InlineComponent') {
break;
}
if (owner.node.type === 'Element' && /-/.test(owner.node.name)) {
break;
}
owner = owner.parent;
}
if (owner && owner.node.type === 'InlineComponent') {
const name = attribute.get_static_value() as string;
if (!(owner as unknown as InlineComponentWrapper).slots.has(name)) {
const child_block = block.child({
comment: create_debugging_comment(node, this.renderer.component),
name: this.renderer.component.get_unique_name(`create_${sanitize(name)}_slot`),
type: 'slot'
});
const { scope, lets } = this.node;
const seen = new Set(lets.map(l => l.name.name));
(owner as unknown as InlineComponentWrapper).node.lets.forEach(l => {
if (!seen.has(l.name.name)) lets.push(l);
});
(owner as unknown as InlineComponentWrapper).slots.set(
name,
get_slot_definition(child_block, scope, lets)
);
this.renderer.blocks.push(child_block);
}
this.slot_block = (owner as unknown as InlineComponentWrapper).slots.get(name).block;
block = this.slot_block;
}
block = create_slot_block(attribute, this, block);
}
if (attribute.name === 'style') {
return new StyleAttributeWrapper(this, block, attribute);
@ -727,7 +685,7 @@ export default class ElementWrapper extends Wrapper {
`);
} else if (this.node.name === 'input' && this.attributes.find(attr => attr.node.name === 'value')) {
const type = this.node.get_static_attribute_value('type');
if (type === null || type === "" || type === "text" || type === "email" || type === "password") {
if (type === null || type === '' || type === 'text' || type === 'email' || type === 'password') {
block.chunks.mount.push(b`
${this.var}.value = ${data}.value;
`);
@ -787,9 +745,7 @@ export default class ElementWrapper extends Wrapper {
}
block.chunks.destroy.push(b`if (detaching && ${name}) ${name}.end();`);
}
else {
} else {
const intro_name = intro && block.get_unique_name(`${this.var.name}_intro`);
const outro_name = outro && block.get_unique_name(`${this.var.name}_outro`);
@ -962,22 +918,16 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapp
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\$/g, '\\$');
}
else if (wrapper instanceof MustacheTagWrapper || wrapper instanceof RawMustacheTagWrapper) {
} else if (wrapper instanceof MustacheTagWrapper || wrapper instanceof RawMustacheTagWrapper) {
literal.quasis.push(state.quasi);
literal.expressions.push(wrapper.node.expression.manipulate(block));
state.quasi = {
type: 'TemplateElement',
value: { raw: '' }
};
}
else if (wrapper.node.name === 'noscript') {
} else if (wrapper.node.name === 'noscript') {
// do nothing
}
else {
} else {
// element
state.quasi.value.raw += `<${wrapper.node.name}`;
@ -998,7 +948,7 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapp
}
});
state.quasi.value.raw += `"`;
state.quasi.value.raw += '"';
});
if (!wrapper.void) {

@ -6,6 +6,7 @@ import EachBlock from './EachBlock';
import Element from './Element/index';
import Head from './Head';
import IfBlock from './IfBlock';
import KeyBlock from './KeyBlock';
import InlineComponent from './InlineComponent/index';
import MustacheTag from './MustacheTag';
import RawMustacheTag from './RawMustacheTag';
@ -30,6 +31,7 @@ const wrappers = {
Head,
IfBlock,
InlineComponent,
KeyBlock,
MustacheTag,
Options: null,
RawMustacheTag,
@ -67,7 +69,7 @@ export default class FragmentWrapper {
const child = nodes[i];
if (!child.type) {
throw new Error(`missing type`);
throw new Error('missing type');
}
if (!(child.type in wrappers)) {

@ -55,7 +55,7 @@ class IfBlockBranch extends Wrapper {
});
if (should_cache) {
this.condition = block.get_unique_name(`show_if`);
this.condition = block.get_unique_name('show_if');
this.snippet = (expression.manipulate(block) as Node);
} else {
this.condition = expression.manipulate(block);
@ -65,7 +65,7 @@ class IfBlockBranch extends Wrapper {
this.block = block.child({
comment: create_debugging_comment(node, parent.renderer.component),
name: parent.renderer.component.get_unique_name(
is_else ? `create_else_block` : `create_if_block`
is_else ? 'create_else_block' : 'create_if_block'
),
type: (node as IfBlock).expression ? 'if' : 'else'
});
@ -257,8 +257,8 @@ export default class IfBlockWrapper extends Wrapper {
{ name, anchor, has_else, if_exists_condition, has_transitions },
detaching
) {
const select_block_type = this.renderer.component.get_unique_name(`select_block_type`);
const current_block_type = block.get_unique_name(`current_block_type`);
const select_block_type = this.renderer.component.get_unique_name('select_block_type');
const current_block_type = block.get_unique_name('current_block_type');
const get_block = has_else
? x`${current_block_type}(#ctx)`
: x`${current_block_type} && ${current_block_type}(#ctx)`;
@ -364,11 +364,11 @@ export default class IfBlockWrapper extends Wrapper {
{ name, anchor, has_else, has_transitions, if_exists_condition },
detaching
) {
const select_block_type = this.renderer.component.get_unique_name(`select_block_type`);
const current_block_type_index = block.get_unique_name(`current_block_type_index`);
const previous_block_index = block.get_unique_name(`previous_block_index`);
const if_block_creators = block.get_unique_name(`if_block_creators`);
const if_blocks = block.get_unique_name(`if_blocks`);
const select_block_type = this.renderer.component.get_unique_name('select_block_type');
const current_block_type_index = block.get_unique_name('current_block_type_index');
const previous_block_index = block.get_unique_name('previous_block_index');
const if_block_creators = block.get_unique_name('if_block_creators');
const if_blocks = block.get_unique_name('if_blocks');
const if_current_block_type_index = has_else
? nodes => nodes
@ -447,6 +447,8 @@ export default class IfBlockWrapper extends Wrapper {
if (!${name}) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#ctx);
${name}.c();
} else {
${name}.p(#ctx, #dirty);
}
${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor});

@ -78,7 +78,7 @@ export default class InlineComponentWrapper extends Wrapper {
const default_slot = block.child({
comment: create_debugging_comment(node, renderer.component),
name: renderer.component.get_unique_name(`create_default_slot`),
name: renderer.component.get_unique_name('create_default_slot'),
type: 'slot'
});

@ -0,0 +1,136 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import EachBlock from '../../nodes/EachBlock';
import KeyBlock from '../../nodes/KeyBlock';
import create_debugging_comment from './shared/create_debugging_comment';
import FragmentWrapper from './Fragment';
import { b, x } from 'code-red';
import { Identifier } from 'estree';
export default class KeyBlockWrapper extends Wrapper {
node: KeyBlock;
fragment: FragmentWrapper;
block: Block;
dependencies: string[];
var: Identifier = { type: 'Identifier', name: 'key_block' };
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: EachBlock,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
this.dependencies = node.expression.dynamic_dependencies();
if (this.dependencies.length) {
block = block.child({
comment: create_debugging_comment(node, renderer.component),
name: renderer.component.get_unique_name('create_key_block'),
type: 'key'
});
renderer.blocks.push(block);
}
this.block = block;
this.fragment = new FragmentWrapper(
renderer,
this.block,
node.children,
this,
strip_whitespace,
next_sibling
);
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
if (this.dependencies.length === 0) {
this.render_static_key(block, parent_node, parent_nodes);
} else {
this.render_dynamic_key(block, parent_node, parent_nodes);
}
}
render_static_key(_block: Block, parent_node: Identifier, parent_nodes: Identifier) {
this.fragment.render(this.block, parent_node, parent_nodes);
}
render_dynamic_key(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
this.fragment.render(
this.block,
null,
(x`#nodes` as unknown) as Identifier
);
const has_transitions = !!(
this.block.has_intro_method || this.block.has_outro_method
);
const dynamic = this.block.has_update_method;
const previous_key = block.get_unique_name('previous_key');
const snippet = this.node.expression.manipulate(block);
block.add_variable(previous_key, snippet);
const not_equal = this.renderer.component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`;
const condition = x`${this.renderer.dirty(this.dependencies)} && ${not_equal}(${previous_key}, ${previous_key} = ${snippet})`;
block.chunks.init.push(b`
let ${this.var} = ${this.block.name}(#ctx);
`);
block.chunks.create.push(b`${this.var}.c();`);
if (this.renderer.options.hydratable) {
block.chunks.claim.push(b`${this.var}.l(${parent_nodes});`);
}
block.chunks.mount.push(
b`${this.var}.m(${parent_node || '#target'}, ${
parent_node ? 'null' : '#anchor'
});`
);
const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes);
const body = b`
${
has_transitions
? b`
@group_outros();
@transition_out(${this.var}, 1, 1, @noop);
@check_outros();
`
: b`${this.var}.d(1);`
}
${this.var} = ${this.block.name}(#ctx);
${this.var}.c();
${has_transitions && b`@transition_in(${this.var})`}
${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor});
`;
if (dynamic) {
block.chunks.update.push(b`
if (${condition}) {
${body}
} else {
${this.var}.p(#ctx, #dirty);
}
`);
} else {
block.chunks.update.push(b`
if (${condition}) {
${body}
}
`);
}
if (has_transitions) {
block.chunks.intro.push(b`@transition_in(${this.var})`);
block.chunks.outro.push(b`@transition_out(${this.var})`);
}
block.chunks.destroy.push(b`${this.var}.d(detaching)`);
}
}

@ -36,9 +36,7 @@ export default class RawMustacheTagWrapper extends Tag {
);
block.chunks.mount.push(insert(init));
}
else {
} else {
const needs_anchor = in_head || (this.next ? !this.next.is_dom_node() : (!this.parent || !this.parent.is_dom_node()));
const html_tag = block.get_unique_name('html_tag');

@ -12,11 +12,13 @@ import Expression from '../../nodes/shared/Expression';
import is_dynamic from './shared/is_dynamic';
import { Identifier, ObjectExpression } from 'estree';
import create_debugging_comment from './shared/create_debugging_comment';
import create_slot_block from './Element/create_slot_block';
export default class SlotWrapper extends Wrapper {
node: Slot;
fragment: FragmentWrapper;
fallback: Block | null = null;
slot_block: Block;
var: Identifier = { type: 'Identifier', name: 'slot' };
dependencies: Set<string> = new Set(['$$scope']);
@ -36,12 +38,16 @@ export default class SlotWrapper extends Wrapper {
if (this.node.children.length) {
this.fallback = block.child({
comment: create_debugging_comment(this.node.children[0], this.renderer.component),
name: this.renderer.component.get_unique_name(`fallback_block`),
name: this.renderer.component.get_unique_name('fallback_block'),
type: 'fallback'
});
renderer.blocks.push(this.fallback);
}
if (this.node.values.has('slot')) {
block = create_slot_block(this.node.values.get('slot'), this, block);
}
this.fragment = new FragmentWrapper(
renderer,
this.fallback,
@ -71,6 +77,10 @@ export default class SlotWrapper extends Wrapper {
const { slot_name } = this.node;
if (this.slot_block) {
block = this.slot_block;
}
let get_slot_changes_fn;
let get_slot_context_fn;

@ -51,4 +51,4 @@ export default class TextWrapper extends Wrapper {
parent_node as Identifier
);
}
}
}

@ -59,7 +59,7 @@ export default class TitleWrapper extends Wrapper {
}
const last = this.node.should_cache && block.get_unique_name(
`title_value`
'title_value'
);
if (this.node.should_cache) block.add_variable(last);

@ -72,9 +72,9 @@ export default class WindowWrapper extends Wrapper {
});
});
const scrolling = block.get_unique_name(`scrolling`);
const clear_scrolling = block.get_unique_name(`clear_scrolling`);
const scrolling_timeout = block.get_unique_name(`scrolling_timeout`);
const scrolling = block.get_unique_name('scrolling');
const clear_scrolling = block.get_unique_name('clear_scrolling');
const scrolling_timeout = block.get_unique_name('scrolling_timeout');
Object.keys(events).forEach(event => {
const id = block.get_unique_name(`onwindow${event}`);
@ -156,7 +156,7 @@ export default class WindowWrapper extends Wrapper {
// another special case. (I'm starting to think these are all special cases.)
if (bindings.online) {
const id = block.get_unique_name(`onlinestatuschanged`);
const id = block.get_unique_name('onlinestatuschanged');
const name = bindings.online;
renderer.add_to_context(id.name);

@ -56,4 +56,4 @@ export default class Tag extends Wrapper {
return { init: content };
}
}
}

@ -94,4 +94,4 @@ export default function bind_this(component: Component, block: Block, binding: B
block.chunks.destroy.push(b`${callee}(null);`);
return b`${callee}(${variable});`;
}
}

@ -29,7 +29,7 @@ export default function create_debugging_comment(
} else {
// @ts-ignore
d = node.expression ? node.expression.node.end : c;
while (source[d] !== '}') d += 1;
while (source[d] !== '}' && d <= source.length) d += 1;
while (source[d] === '}') d += 1;
}

@ -112,4 +112,4 @@ export function get_slot_definition(block: Block, scope: TemplateScope, lets: Le
get_context: x`${context_input} => ${context}`,
get_changes: x`${changes_input} => ${changes}`
};
}
}

@ -9,4 +9,4 @@ export default function is_dynamic(variable: Var) {
}
return false;
}
}

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

Loading…
Cancel
Save