Merge branch 'master' into improveOutputSize

pull/5702/head
Ivan Hofer 6 years ago
commit dedd01eb86

@ -1,5 +1,27 @@
# Svelte changelog
## 3.31.2
* Rework SSR store handling to subscribe and unsubscribe as in DOM mode ([#3375](https://github.com/sveltejs/svelte/issues/3375), [#3582](https://github.com/sveltejs/svelte/issues/3582), [#3636](https://github.com/sveltejs/svelte/issues/3636))
* Fix error when removing elements that are already transitioning out ([#5789](https://github.com/sveltejs/svelte/issues/5789), [#5808](https://github.com/sveltejs/svelte/issues/5808))
* Fix duplicate content race condition with `{#await}` blocks and out transitions ([#5815](https://github.com/sveltejs/svelte/issues/5815))
* Deconflict variable names used for contextual actions ([#5834](https://github.com/sveltejs/svelte/issues/5834))
## 3.31.1
* Fix scrolling of element with resize listener by making the `<iframe>` have `z-index: -1` ([#5448](https://github.com/sveltejs/svelte/issues/5448))
* Fix location of automatically declared reactive variables ([#5749](https://github.com/sveltejs/svelte/issues/5749))
* Warn when using `className` or `htmlFor` attributes ([#5777](https://github.com/sveltejs/svelte/issues/5777))
* Fix checkbox `bind:group` in keyed `{#each}` where the array can be reordered ([#5779](https://github.com/sveltejs/svelte/issues/5779))
* Fix checkbox `bind:group` in nested `{#each}` contexts ([#5811](https://github.com/sveltejs/svelte/issues/5811))
* Add graphics roles as known ARIA roles ([#5822](https://github.com/sveltejs/svelte/pull/5822))
* Fix local transitions if a parent has a cancelled outro transition ([#5829](https://github.com/sveltejs/svelte/issues/5829))
* Support `use:obj.some.deep.function` as actions ([#5844](https://github.com/sveltejs/svelte/issues/5844))
## 3.31.0
* Use a separate `SvelteComponentTyped` interface for typed components ([#5738](https://github.com/sveltejs/svelte/pull/5738))
## 3.30.1
* Support consuming decoded sourcemaps as created by the `source-map` library's `SourceMapGenerator` ([#5722](https://github.com/sveltejs/svelte/issues/5722))

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

1214
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "svelte",
"version": "3.30.1",
"version": "3.31.2",
"description": "Cybernetically enhanced web apps",
"module": "index.mjs",
"main": "index",
@ -99,22 +99,22 @@
"@rollup/plugin-json": "^4.0.1",
"@rollup/plugin-node-resolve": "^6.0.0",
"@rollup/plugin-replace": "^2.3.0",
"@rollup/plugin-sucrase": "^3.0.0",
"@rollup/plugin-sucrase": "^3.1.0",
"@rollup/plugin-typescript": "^2.0.1",
"@rollup/plugin-virtual": "^2.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",
"@typescript-eslint/parser": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^4.9.0",
"@typescript-eslint/parser": "^4.9.0",
"acorn": "^7.4.0",
"agadoo": "^1.1.0",
"c8": "^5.0.1",
"code-red": "^0.1.4",
"codecov": "^3.5.0",
"css-tree": "1.0.0-alpha22",
"eslint": "^7.1.0",
"eslint-plugin-import": "^2.20.2",
"eslint": "^7.15.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-svelte3": "^2.7.3",
"estree-walker": "^1.0.0",
"is-reference": "^1.1.4",
@ -130,8 +130,8 @@
"source-map-support": "^0.5.13",
"sourcemap-codec": "^1.4.8",
"tiny-glob": "^0.2.6",
"tslib": "^1.10.0",
"typescript": "^3.5.3"
"tslib": "^2.0.3",
"typescript": "^3.7.5"
},
"nyc": {
"include": [

@ -14,5 +14,4 @@ environment:
install:
- ps: Install-Product node $env:nodejs_version
- npm install cypress
- npm install

@ -11,7 +11,7 @@ It's the last "What's new in Svelte" of the year and there's lots to celebrate!
1. `$$props`, `$$restProps`, and `$$slots` are all now supported in custom web components (**3.29.5**, [Example](https://svelte.dev/repl/ad8e6f39cd20403dacd1be84d71e498d?version=3.29.5)) and `slot` components now support spread props: `<slot {...foo} />` (**3.30.0**)
2. A new `hasContext` lifecycle function makes it easy to check whether a `key` has been set in the context of a parent component (**3.30.0** & **3.30.1**, [Docs](https://svelte.dev/docs#hasContext))
3. `SvelteComponent` is now typed which makes it easier to add typed classes that extend base Svelte Components. Component library and framework authors rejoice! An example: `export class YourComponent extends SvelteComponent<{aProp: boolean}, {click: MouseEvent}, {default: {aSlot: string}}> {}` (**3.30.0**, [RFC](https://github.com/sveltejs/rfcs/pull/37))
3. There is now a new `SvelteComponentTyped` class which makes it easier to add strongly typed components that extend base Svelte components. Component library and framework authors rejoice! An example: `export class YourComponent extends SvelteComponentTyped<{aProp: boolean}, {click: MouseEvent}, {default: {aSlot: string}}> {}` (**3.31.0**, [RFC](https://github.com/sveltejs/rfcs/pull/37))
4. Transitions within `{:else}` blocks should now complete successfully (**3.29.5**, [Example](https://svelte.dev/repl/49cef205e5da459594ef2eafcbd41593?version=3.29.5))
5. Svelte now includes an export map, which explicitly states which files can be imported from its npm package (**3.29.5** with some fixes in **3.29.6**, **3.29.7** and **3.30.0**)
6. `rollup-plugin-svelte` had a new [7.0.0 release](https://github.com/sveltejs/rollup-plugin-svelte/blob/master/CHANGELOG.md). The biggest change is that the `css` option was removed. Users who were using that option should add another plugin like `rollup-plugin-css-only` as demonstrated [in the template](https://github.com/sveltejs/template/blob/5b1135c286f7a649daa99825a077586655051649/rollup.config.js#L48)

@ -0,0 +1,86 @@
---
title: What's new in Svelte: January 2021
description: A Svelte-packed showcase to kick-off the new year!
author: Daniel Sandoval
authorURL: https://desandoval.net
---
Happy new year from Svelte! In the last month we made progress on Sapper's upcoming release, fine-tuned our `SvelteComponent` typings, and have seen some amazing apps, sites, and libraries coming out in the showcase.
## What's changed in Svelte?
A new minor release replaces the `SvelteComponent` class with a `SvelteComponentTyped` class. This renaming should help with backwards compatibility. We've updated [last month's blog post](https://svelte.dev/blog/whats-new-in-svelte-december-2020) to avoid any confusion with the name change.
If you're using `SvelteComponent` or the new `SvelteComponentTyped` in your project or library, let us know what you're using it for and we'll add it to the showcase!
## What's going on in Sapper?
More quality of life features are landing in the upcoming release every day. `0.29.0` will include new TypeScript definitions, fixes to scroll tracking and prefetching behavior, and improvements to the runtime router to support encoded query parameters.
If you're upgrading from 0.28.x, check out [the migration guide](https://sapper.svelte.dev/migrating/#0_28_to_0_29) for steps on updating to Sapper 0.29.
## Is SvelteKit ready yet?
To avoid too much churn during development, SvelteKit is still being worked on in a private repo. There will be an announcement on the Discord, blog and Twitter when it's ready for a larger group of users and contributors.
In the meantime, you can explore the current build by running `npm init svelte@next` from your command line.
As cautioned in _[What's the deal with SvelteKit?](https://svelte.dev/blog/whats-the-deal-with-sveltekit)_, there are no docs or support available yet... So use at your own risk / for your own enjoyment!
---
## Community Showcase
**Apps & Sites**
- [manitu.me](https://manitu.me/) is a background sound / pomodoro timer for focus and relaxation
- [Answer Socrates](https://answersocrates.com/) helps you find trending questions on the internet so that you can write the most relevant blog post, tweet, or billboard
- [multris](https://multris.s1h.org/) is a multiplayer Tetris game. You can read about its development [here](https://blog.s1h.org/svelte-multiplayer-game/)
- [weather-ab](https://github.com/ganochenkodg/weather-ab) compares the archive of weather in different cities of the world. Indispensable for people thinking about migration
- [Game Nibs](https://gamenibs.com/) is a platform for gamers to find and share concise bite-sized bits of gaming advice, tips, tricks, screenshots, builds, and much more
- [Ora](https://github.com/cupcakearmy/ora) is an open source website tracking and limiting tool for Chrome and Firefox
- [vscode-dms](https://github.com/techsyndicate/vscode-dms) is a group direct messaging chat app for VSCode
- [Zero.2](https://zero.oleksandrdemian.tech/) is a math-based challenge game where you try to get to zero as quickly as possible
- [Octave Compass](https://octavecompass.com/2741) is a chord table and scale explorer for many popular musical scales
- [Infinite Walking Bass Generator 2](https://github.com/elialbert/infinitewalkingbass2) is an online music player that generates a unique walking bass line
- [ListenAddict](https://www.listenaddict.com/) is a site that notifies you whenever a person has a new talk/interview on podcast
**Demos, Libraries & Components**
- [svelte-tiny-virtual-list](https://github.com/Skayo/svelte-tiny-virtual-list) speeds up long lists by only rendering visible items
- [svelte-query](https://github.com/TanStack/svelte-query) is a collection of helpful hooks for managing, caching and syncing asynchronous and remote data
- [svelte-previous](https://github.com/bryanmylee/svelte-previous) is a svelte store to remember previous values - helpful for transitions or a quick undo stack
- [Let's Build a Confetti Cannon](https://varun.ca/confetti/) explains how to build a particle system and integrate a Canvas based animation into a larger application
- [svelte-micro](https://github.com/ayndqy/svelte-micro) is a one-component router
- [svelte-standalone-router](https://github.com/hjalmar/svelte-standalone-router) is a standalone router with an API based on [standalone-router](https://github.com/hjalmar/standalone-router)
- [svelte-datepicker](https://github.com/beyonk-adventures/svelte-datepicker) is a datepicker component with variations for time selection, date ranges and responsive themes
- [svelte-slimscroll](https://github.com/MelihAltintas/svelte-slimscroll) is a action for Svelte.js, which can transforms any div into a scrollable area with a nice scrollbar.
- [Svelte Zoomable](https://svelte.dev/repl/58dfe87756ee4db897c281b52fdef7b7?version=3.31.0) is a custom transition with a nice zoom effect
**Have a component you'd like to share?** Check out the [Components](https://sveltesociety.dev/components) page on the Svelte Society site. You can contribute by making [a PR to this file](https://github.com/svelte-society/sveltesociety.dev/blob/master/src/pages/components/components.json).
**Learning Resources**
- [Using Svelte to create a scroll video effect](https://blog.koenvangilst.nl/tutorial-svelte-scroll-video/) showcases how the `bind` command can be used to create a cool scroll video effect with very little code
- [How to make a flappybird game in svelte and typescript](https://www.youtube.com/watch?v=nhrYBoVI8pQ) is a video tutorial including docs and code for reference
- [Accessible Svelte Transition](https://www.youtube.com/watch?v=QK_QuRL7nSo&feature=youtu.be) walks through `prefers-reduced-motion` to make svelte transitions more accessible
- [Svelte's module scripts explained](https://codechips.me/svelte-module-scripts-explained/) is a great introduction to the module context, a common Sapper pattern
- [Awesome Svelte](https://github.com/TheComputerM/awesome-svelte#readme) is a curated list of Svelte resources
- [.NET Core and Svelte](https://dev.to/cainux/net-core-and-svelte-f8o) explains how to get Svelte up and running with .NET Core
- [A la découverte de Svelte JS](https://www.youtube.com/watch?v=SLpx1Y8e1ek&list=PLff5I1miao9ZEUhpqkrOx7k8RGAZt-nm9) is a svelte tutorial series in French!
- [Svelte for React Developers](https://soshace.com/svelte-for-react-developers/) explains Svelte's core concepts to folks who are used to React
- [Building a Svelte Static Website with Smooth Page Transitions](https://www.youtube.com/watch?v=dvPfmcGtmrI&feature=emb_title) shows how to build a static website with Svelte and add smooth page transitions using Three.js and GSAP.
- [Using Apollo Client in Sapper](https://bjornlu.com/blog/using-apollo-client-in-sapper/) explains the "simplest" solutions to integrate the Apollo query client into Sapper
- [Reactive web apps with Crystal + Svelte](https://www.youtube.com/watch?v=i1xjLd6z7BU) explores how to build full-stack, server-rendered Svelte apps with a [Crystal](https://crystal-lang.org) backend
**Related Projects**
- [Snowpack's v3 release candidate](https://www.snowpack.dev/posts/2020-12-03-snowpack-3-release-candidate) is out now in preparation for a January 6 release date. Check out the [Getting Started with Svelte](https://www.snowpack.dev/tutorials/svelte) for more info on how to use Snowpack.
- [Uppy](https://uppy.io/blog/2020/12/1.24/), the open source file uploader, announced Svelte support in its new version 1.24
## See you next month!
Want to add your work to the Showcase? Want to contribute to Svelte? 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!

@ -130,6 +130,32 @@ Any top-level statement (i.e. not inside a block or a function) can be made reac
---
Only values which directly appear within the `$:` block will become dependencies of the reactive statement. For example, in the code below `total` will only update when `x` changes, but not `y`.
```sv
<script>
let x = 0;
let y = 0;
function yPlusAValue(value) {
return value + y;
}
$: total = yPlusAValue(x);
</script>
Total: {total}
<button on:click={() => x++}>
Increment X
</button>
<button on:click={() => y++}>
Increment Y
</button>
```
---
If a statement consists entirely of an assignment to an undeclared variable, Svelte will inject a `let` declaration on your behalf.
```sv

@ -418,8 +418,6 @@ The expression should be valid standalone HTML — `{@html "<div>"}content{@html
The `{@debug ...}` tag offers an alternative to `console.log(...)`. It logs the values of specific variables whenever they change, and pauses code execution if you have devtools open.
It accepts a comma-separated list of variable names (not arbitrary expressions).
```sv
<script>
let user = {

@ -2,11 +2,11 @@
import Thing from './Thing.svelte';
let things = [
{ id: 1, color: '#0d0887' },
{ id: 2, color: '#6a00a8' },
{ id: 3, color: '#b12a90' },
{ id: 4, color: '#e16462' },
{ id: 5, color: '#fca636' }
{ id: 1, color: 'darkblue' },
{ id: 2, color: 'indigo' },
{ id: 3, color: 'deeppink' },
{ id: 4, color: 'salmon' },
{ id: 5, color: 'gold' }
];
function handleClick() {

@ -18,7 +18,7 @@
let height = 200;
function formatMobile(tick) {
return "'" + tick % 100;
return "'" + tick.toString().slice(-2);
}
$: xScale = scaleLinear()

@ -23,7 +23,7 @@
$: area = `${path}L${xScale(maxX)},${yScale(0)}L${xScale(minX)},${yScale(0)}Z`;
function formatMobile (tick) {
return "'" + tick % 100;
return "'" + tick.toString().slice(-2);
}
</script>

@ -2,5 +2,10 @@
question: How do I test Svelte apps?
---
We don't have a good answer to this yet, but it is a priority. There are a few approaches that people take when testing, but it generally involves compiling the component and mounting it to something and then performing the tests.
You essentially need to create a bundle for each component you're testing (since svelte is a compiler and not a normal library) and then mount them. You can mount to a JSDOM instance, or you can use Puppeteer if you need a real browser, or you can use a tool like Cypress. There is an example of this in the Sapper starter template.
We recommend trying to seperate your view logic from your business logic. Data transformation or cross component state management is best kept outside of Svelte components. You can test those parts like you would test any JavaScript functionality that way. When it comes to testing the components, it is best to test the logic of the component and remember that the Svelte library has its own tests and you do not need to test implementation details provided by Svelte.
There are a few approaches that people take when testing, but it generally involves compiling the component and mounting it to something and then performing the tests. You essentially need to create a bundle for each component you're testing (since svelte is a compiler and not a normal library) and then mount them. You can mount to a JSDOM instance. Or you can use a real browser powered by a library like Playwright, Puppeteer, or Cypress.
Some resources for getting started with unit testing:
- [Svelte Testing Library](https://testing-library.com/docs/svelte-testing-library/example/)
- [Example using uvu test runner with JSDOM](https://github.com/lukeed/uvu/tree/master/examples/svelte)

@ -2,11 +2,11 @@
import Thing from './Thing.svelte';
let things = [
{ id: 1, color: '#0d0887' },
{ id: 2, color: '#6a00a8' },
{ id: 3, color: '#b12a90' },
{ id: 4, color: '#e16462' },
{ id: 5, color: '#fca636' }
{ id: 1, color: 'darkblue' },
{ id: 2, color: 'indigo' },
{ id: 3, color: 'deeppink' },
{ id: 4, color: 'salmon' },
{ id: 5, color: 'gold' }
];
function handleClick() {
@ -20,4 +20,4 @@
{#each things as thing}
<Thing current={thing.color}/>
{/each}
{/each}

@ -2,11 +2,11 @@
import Thing from './Thing.svelte';
let things = [
{ id: 1, color: '#0d0887' },
{ id: 2, color: '#6a00a8' },
{ id: 3, color: '#b12a90' },
{ id: 4, color: '#e16462' },
{ id: 5, color: '#fca636' }
{ id: 1, color: 'darkblue' },
{ id: 2, color: 'indigo' },
{ id: 3, color: 'deeppink' },
{ id: 4, color: 'salmon' },
{ id: 5, color: 'gold' }
];
function handleClick() {
@ -20,4 +20,4 @@
{#each things as thing (thing.id)}
<Thing current={thing.color}/>
{/each}
{/each}

@ -1,4 +0,0 @@
{
"baseUrl": "http://localhost:3000",
"video": false
}

3138
site/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -36,9 +36,9 @@
"@babel/preset-env": "^7.6.0",
"@babel/runtime": "^7.6.0",
"@rollup/plugin-babel": "^5.0.0",
"@rollup/plugin-commonjs": "^15.0.0",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"@rollup/plugin-replace": "^2.2.0",
"@sindresorhus/slugify": "^0.9.1",
"@sveltejs/site-kit": "^1.2.5",
@ -51,10 +51,10 @@
"node-fetch": "^2.6.1",
"node-pg-migrate": "^3.22.0",
"npm-run-all": "^4.1.5",
"rollup": "^2.26.10",
"rollup-plugin-svelte": "^6.0.0",
"rollup": "^2.30.0",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0",
"sapper": "^0.27.16",
"sapper": "^0.28.10",
"shelljs": "^0.8.3",
"svelte": "^3.12.0"
},

@ -31,9 +31,10 @@ export default {
'process.env.MAPBOX_ACCESS_TOKEN': JSON.stringify(process.env.MAPBOX_ACCESS_TOKEN)
}),
svelte({
dev,
hydratable: true,
emitCss: true
compilerOptions: {
dev,
hydratable: true
}
}),
resolve({
browser: true,
@ -77,8 +78,12 @@ export default {
'process.env.NODE_ENV': JSON.stringify(mode)
}),
svelte({
generate: 'ssr',
dev
compilerOptions: {
dev,
generate: 'ssr',
hydratable: true
},
emitCss: false
}),
resolve({
dedupe

@ -22,7 +22,7 @@
height: 1em;
padding: .2em .4em .3em;
border-radius: var(--border-r);
line-height: 1;
line-height: normal;
box-sizing: content-box;
color: #888;
border: 1px solid var(--back-light);

@ -18,6 +18,7 @@
<meta name="Description" content="Articles about Svelte and UI development">
</svelte:head>
<h1 class="visually-hidden">Blog</h1>
<div class='posts stretch'>
{#each posts as post}
<article class='post' data-pubdate={post.metadata.dateString}>

@ -19,4 +19,5 @@
<meta name="Description" content="Cybernetically enhanced web apps">
</svelte:head>
<h1 class="visually-hidden">API Docs</h1>
<Docs {sections}/>

@ -100,6 +100,7 @@
<meta name="Description" content="Interactive example Svelte apps">
</svelte:head>
<h1 class="visually-hidden">Examples</h1>
<div class='examples-container' bind:clientWidth={width}>
<div class="viewport offset-{offset}">
<TableOfContents {sections} active_section={active_slug} {isLoading} />

@ -34,6 +34,7 @@
<meta name="Description" content="Cybernetically enhanced web apps">
</svelte:head>
<h1 class="visually-hidden">Svelte</h1>
<Hero
title="Svelte"
tagline="Cybernetically enhanced web apps"

@ -10,7 +10,9 @@ const app = polka({
onError: (err, req, res) => {
const error = err.message || err;
const code = err.code || err.status || 500;
res.headersSent || send(res, code, { error });
res.headersSent || send(res, code, { error }, {
'content-type': 'text/plain'
});
}
});

@ -29,3 +29,16 @@ h5:hover .anchor,
h6:hover .anchor {
opacity: 1;
}
/* visually hidden, but accessible to assistive tech */
.visually-hidden {
border: 0;
clip: rect(0 0 0 0);
height: auto;
margin: 0;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}

@ -1,14 +1,17 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { Directive } from '../../interfaces';
export default class Action extends Node {
type: 'Action';
name: string;
expression: Expression;
uses_context: boolean;
template_scope: TemplateScope;
constructor(component: Component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: Directive) {
super(component, parent, scope, info);
const object = info.name.split('.')[0];
@ -21,6 +24,8 @@ export default class Action extends Node {
? new Expression(component, this, scope, info.expression)
: null;
this.template_scope = scope;
this.uses_context = this.expression && this.expression.uses_context;
}
}

@ -1,13 +1,17 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Element from './Element';
import EachBlock from './EachBlock';
export default class Animation extends Node {
type: 'Animation';
name: string;
expression: Expression;
constructor(component: Component, parent, scope, info) {
constructor(component: Component, parent: Element, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
component.warn_if_undefined(info.name, info, scope);
@ -31,7 +35,7 @@ export default class Animation extends Node {
});
}
block.has_animation = true;
(block as EachBlock).has_animation = true;
this.expression = info.expression
? new Expression(component, this, scope, info.expression, true)

@ -7,6 +7,7 @@ import Text from './Text';
import Expression from './shared/Expression';
import TemplateScope from './shared/TemplateScope';
import { x } from 'code-red';
import { TemplateNode } from '../../interfaces';
export default class Attribute extends Node {
type: 'Attribute' | 'Spread';
@ -24,7 +25,7 @@ export default class Attribute extends Node {
chunks: Array<Text | Expression>;
dependencies: Set<string>;
constructor(component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.scope = scope;

@ -23,7 +23,7 @@ export default class AwaitBlock extends Node {
then: ThenBlock;
catch: CatchBlock;
constructor(component: Component, parent, scope: TemplateScope, info: TemplateNode) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.expression = new Expression(component, this, scope, info.expression);

@ -5,6 +5,10 @@ import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import {dimensions} from '../../utils/patterns';
import { Node as ESTreeNode } from 'estree';
import { TemplateNode } from '../../interfaces';
import Element from './Element';
import InlineComponent from './InlineComponent';
import Window from './Window';
// TODO this should live in a specific binding
const read_only_media_attributes = new Set([
@ -26,7 +30,7 @@ export default class Binding extends Node {
is_contextual: boolean;
is_readonly: boolean;
constructor(component: Component, parent, scope: TemplateScope, info) {
constructor(component: Component, parent: Element | InlineComponent | Window, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') {
@ -68,7 +72,7 @@ export default class Binding extends Node {
const variable = component.var_lookup.get(name);
if (!variable || variable.global) {
component.error(this.expression.node, {
component.error(this.expression.node as any, {
code: 'binding-undeclared',
message: `${name} is not declared`
});
@ -77,7 +81,7 @@ export default class Binding extends Node {
variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
if (info.expression.type === 'Identifier' && !variable.writable) {
component.error(this.expression.node, {
component.error(this.expression.node as any, {
code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable'
});
@ -86,14 +90,18 @@ export default class Binding extends Node {
const type = parent.get_static_attribute_value('type');
this.is_readonly = (
this.is_readonly =
dimensions.test(this.name) ||
(parent.is_media_node && parent.is_media_node() && read_only_media_attributes.has(this.name)) ||
(parent.name === 'input' && type === 'file') // TODO others?
);
(isElement(parent) &&
((parent.is_media_node() && read_only_media_attributes.has(this.name)) ||
(parent.name === 'input' && type === 'file')) /* TODO others? */);
}
is_readonly_media_attribute() {
return read_only_media_attributes.has(this.name);
}
}
function isElement(node: Node): node is Element {
return !!(node as any).is_media_node;
}

@ -1,16 +1,19 @@
import Node from './shared/Node';
import EventHandler from './EventHandler';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
export default class Body extends Node {
type: 'Body';
handlers: EventHandler[];
constructor(component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.handlers = [];
info.attributes.forEach(node => {
info.attributes.forEach((node: Node) => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(component, this, scope, node));
} else {

@ -1,12 +1,15 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import { TemplateNode } from '../../interfaces';
import TemplateScope from './shared/TemplateScope';
import Component from '../Component';
export default class Class extends Node {
type: 'Class';
name: string;
expression: Expression;
constructor(component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.name = info.name;

@ -1,4 +1,7 @@
import { TemplateNode } from '../../interfaces';
import Component from '../Component';
import Node from './shared/Node';
import TemplateScope from './shared/TemplateScope';
const pattern = /^\s*svelte-ignore\s+([\s\S]+)\s*$/m;
@ -7,7 +10,7 @@ export default class Comment extends Node {
data: string;
ignores: string[];
constructor(component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.data = info.data;

@ -1,14 +1,19 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import { INode } from './interfaces';
import { Node as EsTreeNode } from 'estree';
export default class DebugTag extends Node {
type: 'DebugTag';
expressions: Expression[];
constructor(component, parent, scope, info) {
constructor(component: Component, parent: INode, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.expressions = info.identifiers.map(node => {
this.expressions = info.identifiers.map((node: EsTreeNode) => {
return new Expression(component, parent, scope, node);
});
}

@ -6,6 +6,8 @@ import AbstractBlock from './shared/AbstractBlock';
import Element from './Element';
import { Context, unpack_destructuring } from './shared/Context';
import { Node } from 'estree';
import Component from '../Component';
import { TemplateNode } from '../../interfaces';
export default class EachBlock extends AbstractBlock {
type: 'EachBlock';
@ -25,7 +27,7 @@ export default class EachBlock extends AbstractBlock {
else?: ElseBlock;
constructor(component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.expression = new Expression(component, this, scope, info.expression);

@ -23,7 +23,7 @@ const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateM
const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' ');
const aria_attribute_set = new Set(aria_attributes);
const aria_roles = 'alert alertdialog application article banner blockquote button caption cell checkbox code columnheader combobox complementary contentinfo definition deletion dialog directory document emphasis feed figure form generic grid gridcell group heading img link list listbox listitem log main marquee math meter menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option paragraph presentation progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox separator slider spinbutton status strong subscript superscript switch tab table tablist tabpanel term textbox time timer toolbar tooltip tree treegrid treeitem'.split(' ');
const aria_roles = 'alert alertdialog application article banner blockquote button caption cell checkbox code columnheader combobox complementary contentinfo definition deletion dialog directory document emphasis feed figure form generic graphics-document graphics-object graphics-symbol grid gridcell group heading img link list listbox listitem log main marquee math meter menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option paragraph presentation progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox separator slider spinbutton status strong subscript superscript switch tab table tablist tabpanel term textbox time timer toolbar tooltip tree treegrid treeitem'.split(' ');
const aria_role_set = new Set(aria_roles);
const a11y_required_attributes = {
@ -93,6 +93,11 @@ const passive_events = new Set([
'touchcancel'
]);
const react_attributes = new Map([
['className', 'class'],
['htmlFor', 'for']
]);
function get_namespace(parent: Element, element: Element, explicit_namespace: string) {
const parent_element = parent.find_nearest(/^Element/);
@ -125,11 +130,11 @@ export default class Element extends Node {
namespace: string;
needs_manual_style_scoping: boolean;
constructor(component: Component, parent, scope, info: any) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: any) {
super(component, parent, scope, info);
this.name = info.name;
this.namespace = get_namespace(parent, this, component.namespace);
this.namespace = get_namespace(parent as Element, this, component.namespace);
if (this.name === 'textarea') {
if (info.children.length > 0) {
@ -444,6 +449,13 @@ export default class Element extends Node {
});
}
if (react_attributes.has(attribute.name)) {
component.warn(attribute, {
code: 'invalid-html-attribute',
message: `'${attribute.name}' is not a valid HTML attribute. Did you mean '${react_attributes.get(attribute.name)}'?`
});
}
attribute_map.set(attribute.name, attribute);
});
}
@ -850,7 +862,7 @@ export default class Element extends Node {
type: 'Text',
data: ` ${id}`,
synthetic: true
})
} as any)
);
}
} else {
@ -859,7 +871,7 @@ export default class Element extends Node {
type: 'Attribute',
name: 'class',
value: [{ type: 'Text', data: id, synthetic: true }]
})
} as any)
);
}
}

@ -1,11 +1,14 @@
import map_children from './shared/map_children';
import AbstractBlock from './shared/AbstractBlock';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Node from './shared/Node';
export default class ElseBlock extends AbstractBlock {
type: 'ElseBlock';
constructor(component: Component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.children = map_children(component, this, scope, info.children);

@ -3,6 +3,8 @@ import Expression from './shared/Expression';
import Component from '../Component';
import { sanitize } from '../../utils/names';
import { Identifier } from 'estree';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
export default class EventHandler extends Node {
type: 'EventHandler';
@ -13,7 +15,7 @@ export default class EventHandler extends Node {
uses_context = false;
can_make_passive = false;
constructor(component: Component, parent, template_scope, info) {
constructor(component: Component, parent: Node, template_scope: TemplateScope, info: TemplateNode) {
super(component, parent, template_scope, info);
this.name = info.name;

@ -4,6 +4,7 @@ import map_children from './shared/map_children';
import Block from '../render_dom/Block';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
export default class Fragment extends Node {
type: 'Fragment';
@ -11,7 +12,7 @@ export default class Fragment extends Node {
children: INode[];
scope: TemplateScope;
constructor(component: Component, info: any) {
constructor(component: Component, info: TemplateNode) {
const scope = new TemplateScope();
super(component, null, scope, info);

@ -1,13 +1,16 @@
import Node from './shared/Node';
import map_children from './shared/map_children';
import hash from '../utils/hash';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
export default class Head extends Node {
type: 'Head';
children: any[]; // TODO
id: string;
constructor(component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
if (info.attributes.length) {

@ -2,13 +2,17 @@ import ElseBlock from './ElseBlock';
import Expression from './shared/Expression';
import map_children from './shared/map_children';
import AbstractBlock from './shared/AbstractBlock';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Node from './shared/Node';
export default class IfBlock extends AbstractBlock {
type: 'IfBlock';
expression: Expression;
else: ElseBlock;
constructor(component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.expression = new Expression(component, this, scope, info.expression);

@ -8,6 +8,7 @@ import Component from '../Component';
import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
export default class InlineComponent extends Node {
type: 'InlineComponent';
@ -20,7 +21,7 @@ export default class InlineComponent extends Node {
children: INode[];
scope: TemplateScope;
constructor(component: Component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
if (info.name !== 'svelte:component' && info.name !== 'svelte:self') {

@ -1,13 +1,17 @@
import Expression from './shared/Expression';
import map_children from './shared/map_children';
import AbstractBlock from './shared/AbstractBlock';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Node from './shared/Node';
export default class KeyBlock extends AbstractBlock {
type: 'KeyBlock';
expression: Expression;
constructor(component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.expression = new Expression(component, this, scope, info.expression);

@ -2,6 +2,8 @@ import Node from './shared/Node';
import Component from '../Component';
import { walk } from 'estree-walker';
import { BasePattern, Identifier } from 'estree';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
const applicable = new Set(['Identifier', 'ObjectExpression', 'ArrayExpression', 'Property']);
@ -11,7 +13,7 @@ export default class Let extends Node {
value: Identifier;
names: string[] = [];
constructor(component: Component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.name = { type: 'Identifier', name: info.name };

@ -1,9 +1,13 @@
import map_children from './shared/map_children';
import AbstractBlock from './shared/AbstractBlock';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Node from './shared/Node';
export default class PendingBlock extends AbstractBlock {
type: 'PendingBlock';
constructor(component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.children = map_children(component, parent, scope, info.children);

@ -3,6 +3,7 @@ import Attribute from './Attribute';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
export default class Slot extends Element {
type: 'Element';
@ -11,7 +12,7 @@ export default class Slot extends Element {
slot_name: string;
values: Map<string, Attribute> = new Map();
constructor(component: Component, parent: INode, scope: TemplateScope, info: any) {
constructor(component: Component, parent: INode, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
info.attributes.forEach(attr => {

@ -2,6 +2,7 @@ import Node from './shared/Node';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
// Whitespace inside one of these elements will not result in
// a whitespace node being created in any circumstances. (This
@ -20,7 +21,7 @@ export default class Text extends Node {
data: string;
synthetic: boolean;
constructor(component: Component, parent: INode, scope: TemplateScope, info: any) {
constructor(component: Component, parent: INode, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.data = info.data;
this.synthetic = info.synthetic || false;

@ -1,13 +1,15 @@
import Node from './shared/Node';
import map_children, { Children } from './shared/map_children';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
export default class Title extends Node {
type: 'Title';
children: Children;
should_cache: boolean;
constructor(component: Component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.children = map_children(component, parent, scope, info.children);

@ -1,6 +1,9 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Element from './Element';
export default class Transition extends Node {
type: 'Transition';
@ -9,7 +12,7 @@ export default class Transition extends Node {
expression: Expression;
is_local: boolean;
constructor(component: Component, parent, scope, info) {
constructor(component: Component, parent: Element, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
component.warn_if_undefined(info.name, info, scope);

@ -5,6 +5,9 @@ import flatten_reference from '../utils/flatten_reference';
import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list';
import Action from './Action';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
const valid_bindings = [
'innerWidth',
@ -22,7 +25,7 @@ export default class Window extends Node {
bindings: Binding[] = [];
actions: Action[] = [];
constructor(component, parent, scope, info) {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
info.attributes.forEach(node => {

@ -14,6 +14,7 @@ import { Node, FunctionExpression, Identifier } from 'estree';
import { INode } from '../interfaces';
import { is_reserved_keyword } from '../../utils/reserved_keywords';
import replace_object from '../../utils/replace_object';
import is_contextual from './is_contextual';
import EachBlock from '../EachBlock';
type Owner = INode;
@ -22,7 +23,7 @@ export default class Expression {
type: 'Expression' = 'Expression';
component: Component;
owner: Owner;
node: any;
node: Node;
references: Set<string> = new Set();
dependencies: Set<string> = new Set();
contextual_dependencies: Set<string> = new Set();
@ -36,7 +37,7 @@ export default class Expression {
manipulated: Node;
constructor(component: Component, owner: Owner, template_scope: TemplateScope, info, lazy?: boolean) {
constructor(component: Component, owner: Owner, template_scope: TemplateScope, info: Node, lazy?: boolean) {
// TODO revert to direct property access in prod?
Object.defineProperties(this, {
component: {
@ -314,7 +315,7 @@ export default class Expression {
block.renderer.add_to_context(func_id.name, true);
// rename #ctx -> child_ctx;
walk(func_expression, {
enter(node) {
enter(node: Node) {
if (node.type === 'Identifier' && node.name === '#ctx') {
node.name = 'child_ctx';
}
@ -409,18 +410,3 @@ function get_function_name(_node, parent) {
return 'func';
}
function is_contextual(component: Component, scope: TemplateScope, name: string) {
if (is_reserved_keyword(name)) return true;
// if it's a name below root scope, it's contextual
if (!scope.is_top_level(name)) return true;
const variable = component.var_lookup.get(name);
// hoistables, module declarations, and imports are non-contextual
if (!variable || variable.hoistable) return false;
// assume contextual
return true;
}

@ -2,6 +2,7 @@ import Attribute from '../Attribute';
import Component from '../../Component';
import { INode } from '../interfaces';
import Text from '../Text';
import { TemplateNode } from '../../../interfaces';
export default class Node {
readonly start: number;
@ -17,7 +18,7 @@ export default class Node {
var: string;
attributes: Attribute[];
constructor(component: Component, parent, _scope, info: any) {
constructor(component: Component, parent: Node, _scope, info: TemplateNode) {
this.start = info.start;
this.end = info.end;
this.type = info.type;

@ -0,0 +1,18 @@
import Component from '../../Component';
import TemplateScope from './TemplateScope';
import { is_reserved_keyword } from '../../utils/reserved_keywords';
export default function is_contextual(component: Component, scope: TemplateScope, name: string) {
if (is_reserved_keyword(name)) return true;
// if it's a name below root scope, it's contextual
if (!scope.is_top_level(name)) return true;
const variable = component.var_lookup.get(name);
// hoistables, module declarations, and imports are non-contextual
if (!variable || variable.hoistable) return false;
// assume contextual
return true;
}

@ -39,6 +39,7 @@ export default class Block {
dependencies: Set<string> = new Set();
bindings: Map<string, Bindings>;
binding_group_initialised: Set<string> = new Set();
chunks: {
declarations: Array<Node | Node[]>;

@ -6,6 +6,7 @@ import { x } from 'code-red';
import { Node, Identifier, MemberExpression, Literal, Expression, BinaryExpression } from 'estree';
import flatten_reference from '../utils/flatten_reference';
import { reserved_keywords } from '../utils/reserved_keywords';
import { renderer_invalidate } from './invalidate';
interface ContextMember {
name: string;
@ -32,7 +33,7 @@ export default class Renderer {
blocks: Array<Block | Node | Node[]> = [];
readonly: Set<string> = new Set();
meta_bindings: Array<Node | Node[]> = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
binding_groups: Map<string, { binding_group: (to_reference?: boolean) => Node; is_context: boolean; contexts: string[]; index: number }> = new Map();
binding_groups: Map<string, { binding_group: (to_reference?: boolean) => Node; is_context: boolean; contexts: string[]; index: number; keypath: string }> = new Map();
block: Block;
fragment: FragmentWrapper;
@ -168,57 +169,7 @@ export default class Renderer {
}
invalidate(name: string, value?, main_execution_context: boolean = false) {
const variable = this.component.var_lookup.get(name);
const member = this.context_lookup.get(name);
if (variable && (variable.subscribable && (variable.reassigned || variable.export_name))) {
return main_execution_context
? x`${`$$subscribe_${name}`}(${value || name})`
: x`${`$$subscribe_${name}`}($$invalidate(${member.index}, ${value || name}))`;
}
if (name[0] === '$' && name[1] !== '$') {
return x`${name.slice(1)}.set(${value || name})`;
}
if (
variable && (
variable.module || (
!variable.referenced &&
!variable.is_reactive_dependency &&
!variable.export_name &&
!name.startsWith('$$')
)
)
) {
return value || name;
}
if (value) {
return x`$$invalidate(${member.index}, ${value})`;
}
// if this is a reactive declaration, invalidate dependencies recursively
const deps = new Set([name]);
deps.forEach(name => {
const reactive_declarations = this.component.reactive_declarations.filter(x =>
x.assignees.has(name)
);
reactive_declarations.forEach(declaration => {
declaration.dependencies.forEach(name => {
deps.add(name);
});
});
});
// TODO ideally globals etc wouldn't be here in the first place
const filtered = Array.from(deps).filter(n => this.context_lookup.has(n));
if (!filtered.length) return null;
return filtered
.map(n => x`$$invalidate(${this.context_lookup.get(n).index}, ${n})`)
.reduce((lhs, rhs) => x`${lhs}, ${rhs}`);
return renderer_invalidate(this, name, value, main_execution_context);
}
dirty(names: string[], is_reactive_declaration = false): Expression {

@ -414,6 +414,8 @@ export default function dom(
body.push(b`
function ${definition}(${args}) {
${injected.map(name => b`let ${name};`)}
${rest}
${reactive_store_declarations}
@ -440,8 +442,6 @@ export default function dom(
${inject_state && b`$$self.$inject_state = ${inject_state};`}
${injected.map(name => b`let ${name};`)}
${/* before reactive declarations */ props_inject}
${reactive_declarations.length > 0 && b`

@ -33,7 +33,7 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
if (main_execution_context && !variable.subscribable && variable.name[0] !== '$') {
return node;
}
return renderer.invalidate(variable.name, undefined, main_execution_context);
return renderer_invalidate(renderer, variable.name, undefined, main_execution_context);
}
if (!head) {
@ -79,3 +79,66 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
return invalidate;
}
export function renderer_invalidate(renderer: Renderer, name: string, value?, main_execution_context: boolean = false) {
const variable = renderer.component.var_lookup.get(name);
if (variable && (variable.subscribable && (variable.reassigned || variable.export_name))) {
if (main_execution_context) {
return x`${`$$subscribe_${name}`}(${value || name})`;
} else {
const member = renderer.context_lookup.get(name);
return x`${`$$subscribe_${name}`}($$invalidate(${member.index}, ${value || name}))`;
}
}
if (name[0] === '$' && name[1] !== '$') {
return x`${name.slice(1)}.set(${value || name})`;
}
if (
variable && (
variable.module || (
!variable.referenced &&
!variable.is_reactive_dependency &&
!variable.export_name &&
!name.startsWith('$$')
)
)
) {
return value || name;
}
if (value) {
if (main_execution_context) {
return x`${value}`;
} else {
const member = renderer.context_lookup.get(name);
return x`$$invalidate(${member.index}, ${value})`;
}
}
if (main_execution_context) return;
// if this is a reactive declaration, invalidate dependencies recursively
const deps = new Set([name]);
deps.forEach(name => {
const reactive_declarations = renderer.component.reactive_declarations.filter(x =>
x.assignees.has(name)
);
reactive_declarations.forEach(declaration => {
declaration.dependencies.forEach(name => {
deps.add(name);
});
});
});
// TODO ideally globals etc wouldn't be here in the first place
const filtered = Array.from(deps).filter(n => renderer.context_lookup.has(n));
if (!filtered.length) return null;
return filtered
.map(n => x`$$invalidate(${renderer.context_lookup.get(n).index}, ${n})`)
.reduce((lhs, rhs) => x`${lhs}, ${rhs}`);
}

@ -56,12 +56,12 @@ export default class DebugTagWrapper extends Wrapper {
const contextual_identifiers = this.node.expressions
.filter(e => {
const variable = var_lookup.get(e.node.name);
const variable = var_lookup.get((e.node as Identifier).name);
return !(variable && variable.hoistable);
})
.map(e => e.node.name);
.map(e => (e.node as Identifier).name);
const logged_identifiers = this.node.expressions.map(e => p`${e.node.name}`);
const logged_identifiers = this.node.expressions.map(e => p`${(e.node as Identifier).name}`);
const debug_statements = b`
${contextual_identifiers.map(name => b`const ${name} = ${renderer.reference(name)};`)}

@ -57,8 +57,8 @@ export default class EachBlockWrapper extends Wrapper {
get_each_context: Identifier;
iterations: Identifier;
fixed_length: number;
data_length: string;
view_length: string;
data_length: Node|number;
view_length: Node|number;
}
context_props: Array<Node | Node[]>;
@ -447,8 +447,10 @@ export default class EachBlockWrapper extends Wrapper {
: '@destroy_block';
if (this.dependencies.size) {
this.block.maintain_context = true;
this.updates.push(b`
const ${this.vars.each_block_value} = ${snippet};
${this.vars.each_block_value} = ${snippet};
${this.renderer.options.dev && b`@validate_each_argument(${this.vars.each_block_value});`}
${this.block.has_outros && b`@group_outros();`}

@ -116,11 +116,11 @@ export default class BindingWrapper {
switch (this.node.name) {
case 'group':
{
const { binding_group, is_context, contexts, index } = get_binding_group(parent.renderer, this.node, block);
const { binding_group, is_context, contexts, index, keypath } = get_binding_group(parent.renderer, this.node, block);
block.renderer.add_to_context('$$binding_groups');
if (is_context) {
if (is_context && !block.binding_group_initialised.has(keypath)) {
if (contexts.length > 1) {
let binding_group = x`${block.renderer.reference('$$binding_groups')}[${index}]`;
for (const name of contexts.slice(0, -1)) {
@ -133,6 +133,7 @@ export default class BindingWrapper {
block.chunks.init.push(
b`${binding_group(true)} = [];`
);
block.binding_group_initialised.add(keypath);
}
block.chunks.hydrate.push(
@ -257,8 +258,22 @@ function get_binding_group(renderer: Renderer, value: Binding, block: Block) {
let keypath = parts.join('.');
const contexts = [];
const contextual_dependencies = new Set<string>();
const { template_scope } = value.expression;
const add_contextual_dependency = (dep: string) => {
contextual_dependencies.add(dep);
const owner = template_scope.get_owner(dep);
if (owner.type === 'EachBlock') {
for (const dep of owner.expression.contextual_dependencies) {
add_contextual_dependency(dep);
}
}
};
for (const dep of value.expression.contextual_dependencies) {
add_contextual_dependency(dep);
}
for (const dep of contextual_dependencies) {
const context = block.bindings.get(dep);
let key;
let name;
@ -302,7 +317,8 @@ function get_binding_group(renderer: Renderer, value: Binding, block: Block) {
},
is_context: contexts.length > 0,
contexts,
index
index,
keypath
});
}

@ -851,7 +851,21 @@ export default class ElementWrapper extends Wrapper {
${outro && b`@add_transform(${this.var}, ${rect});`}
`);
const params = this.node.animation.expression ? this.node.animation.expression.manipulate(block) : x`{}`;
let params;
if (this.node.animation.expression) {
params = this.node.animation.expression.manipulate(block);
if (this.node.animation.expression.dynamic_dependencies().length) {
// if `params` is dynamic, calculate params ahead of time in the `.r()` method
const params_var = block.get_unique_name('params');
block.add_variable(params_var);
block.chunks.measure.push(b`${params_var} = ${params};`);
params = params_var;
}
} else {
params = x`{}`;
}
const name = this.renderer.reference(this.node.animation.name);

@ -448,7 +448,7 @@ export default class IfBlockWrapper extends Wrapper {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#ctx);
${name}.c();
} else {
${name}.p(#ctx, #dirty);
${dynamic && b`${name}.p(#ctx, #dirty);`}
}
${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor});
@ -472,10 +472,13 @@ export default class IfBlockWrapper extends Wrapper {
}
`;
block.chunks.update.push(b`
let ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(#ctx, #dirty);
`);
if (dynamic) {
block.chunks.update.push(b`
let ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(#ctx, #dirty);
if (${current_block_type_index} === ${previous_block_index}) {
${if_current_block_type_index(b`${if_blocks}[${current_block_type_index}].p(#ctx, #dirty);`)}
} else {
@ -484,8 +487,6 @@ export default class IfBlockWrapper extends Wrapper {
`);
} else {
block.chunks.update.push(b`
let ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(#ctx, #dirty);
if (${current_block_type_index} !== ${previous_block_index}) {
${change_block}
}

@ -52,12 +52,15 @@ export default class WindowWrapper extends Wrapper {
add_event_handlers(block, '@_window', this.handlers);
this.node.bindings.forEach(binding => {
// TODO: what if it's a MemberExpression?
const binding_name = (binding.expression.node as Identifier).name;
// in dev mode, throw if read-only values are written to
if (readonly.has(binding.name)) {
renderer.readonly.add(binding.expression.node.name);
renderer.readonly.add(binding_name);
}
bindings[binding.name] = binding.expression.node.name;
bindings[binding.name] = binding_name;
// bind:online is a special case, we need to listen for two separate events
if (binding.name === 'online') return;
@ -67,7 +70,7 @@ export default class WindowWrapper extends Wrapper {
if (!events[associated_event]) events[associated_event] = [];
events[associated_event].push({
name: binding.expression.node.name,
name: binding_name,
value: property
});
});

@ -1,6 +1,7 @@
import { b, x } from 'code-red';
import Block from '../../Block';
import Action from '../../../nodes/Action';
import is_contextual from '../../../nodes/shared/is_contextual';
export default function add_actions(
block: Block,
@ -11,7 +12,7 @@ export default function add_actions(
}
export function add_action(block: Block, target: string, action: Action) {
const { expression } = action;
const { expression, template_scope } = action;
let snippet;
let dependencies;
@ -28,11 +29,14 @@ export function add_action(block: Block, target: string, action: Action) {
const [obj, ...properties] = action.name.split('.');
const fn = block.renderer.reference(obj);
const fn = is_contextual(action.component, template_scope, obj)
? block.renderer.reference(obj)
: obj;
if (properties.length) {
const member_expression = properties.reduce((lhs, rhs) => x`${lhs}.${rhs}`, fn);
block.event_listeners.push(
x`@action_destroyer(${id} = ${fn}.${properties.join('.')}(${target}, ${snippet}))`
x`@action_destroyer(${id} = ${member_expression}(${target}, ${snippet}))`
);
} else {
block.event_listeners.push(

@ -17,10 +17,18 @@ export default function mark_each_block_bindings(
});
if (binding.name === 'group') {
const add_index_binding = (name: string) => {
const each_block = parent.node.scope.get_owner(name);
if (each_block.type === 'EachBlock') {
each_block.has_index_binding = true;
for (const dep of each_block.expression.contextual_dependencies) {
add_index_binding(dep);
}
}
};
// for `<input bind:group={} >`, we make sure that all the each blocks creates context with `index`
for (const name of binding.expression.contextual_dependencies) {
const each_block = parent.node.scope.get_owner(name);
(each_block as EachBlock).has_index_binding = true;
add_index_binding(name);
}
}
}

@ -1,6 +1,7 @@
import DebugTag from '../../nodes/DebugTag';
import Renderer, { RenderOptions } from '../Renderer';
import { x, p } from 'code-red';
import { Identifier } from 'estree';
export default function(node: DebugTag, renderer: Renderer, options: RenderOptions) {
if (!options.dev) return;
@ -9,7 +10,7 @@ export default function(node: DebugTag, renderer: Renderer, options: RenderOptio
const { line, column } = options.locate(node.start + 1);
const obj = x`{
${node.expressions.map(e => p`${e.node.name}`)}
${node.expressions.map(e => p`${(e.node as Identifier).name}`)}
}`;
renderer.add_expression(x`@debug(${filename ? x`"${filename}"` : x`null`}, ${line - 1}, ${column}, ${obj})`);

@ -1,6 +1,7 @@
import Renderer, { RenderOptions } from '../Renderer';
import RawMustacheTag from '../../nodes/RawMustacheTag';
import { Expression } from 'estree';
export default function(node: RawMustacheTag, renderer: Renderer, _options: RenderOptions) {
renderer.add_expression(node.expression.node);
renderer.add_expression(node.expression.node as Expression);
}

@ -6,6 +6,9 @@ import Renderer from './Renderer';
import { INode as TemplateNode } from '../nodes/interfaces'; // TODO
import Text from '../nodes/Text';
import { LabeledStatement, Statement, Node } from 'estree';
import { walk } from 'estree-walker';
import { extract_names } from 'periscopic';
import { invalidate } from '../render_dom/invalidate';
export default function ssr(
component: Component,
@ -38,24 +41,94 @@ export default function ssr(
const slots = uses_slots ? b`let $$slots = @compute_slots(#slots);` : null;
const reactive_stores = component.vars.filter(variable => variable.name[0] === '$' && variable.name[1] !== '$');
const reactive_store_values = reactive_stores
const reactive_store_subscriptions = reactive_stores
.filter(store => {
const variable = component.var_lookup.get(store.name.slice(1));
return !variable || variable.hoistable;
})
.map(({ name }) => {
const store_name = name.slice(1);
return b`
${component.compile_options.dev && b`@validate_store(${store_name}, '${store_name}');`}
${`$$unsubscribe_${store_name}`} = @subscribe(${store_name}, #value => ${name} = #value)
${store_name}.subscribe($$value => ${name} = $$value);
`;
});
const reactive_store_unsubscriptions = reactive_stores.map(
({ name }) => b`${`$$unsubscribe_${name.slice(1)}`}()`
);
const reactive_store_declarations = reactive_stores
.map(({ name }) => {
const store_name = name.slice(1);
const store = component.var_lookup.get(store_name);
if (store && store.hoistable) return null;
const assignment = b`${name} = @get_store_value(${store_name});`;
if (store && store.reassigned) {
const unsubscribe = `$$unsubscribe_${store_name}`;
const subscribe = `$$subscribe_${store_name}`;
return component.compile_options.dev
? b`@validate_store(${store_name}, '${store_name}'); ${assignment}`
: assignment;
})
.filter(Boolean);
return b`let ${name}, ${unsubscribe} = @noop, ${subscribe} = () => (${unsubscribe}(), ${unsubscribe} = @subscribe(${store_name}, $$value => ${name} = $$value), ${store_name})`;
}
return b`let ${name}, ${`$$unsubscribe_${store_name}`};`;
});
// instrument get/set store value
if (component.ast.instance) {
let scope = component.instance_scope;
const map = component.instance_scope_map;
walk(component.ast.instance.content, {
enter(node: Node) {
if (map.has(node)) {
scope = map.get(node);
}
},
leave(node: Node) {
if (map.has(node)) {
scope = scope.parent;
}
if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument;
const names = new Set(extract_names(assignee));
const to_invalidate = new Set<string>();
for (const name of names) {
const variable = component.var_lookup.get(name);
if (variable &&
!variable.hoistable &&
!variable.global &&
!variable.module &&
(
variable.subscribable || variable.name[0] === '$'
)) {
to_invalidate.add(variable.name);
}
}
if (to_invalidate.size) {
this.replace(
invalidate(
{ component } as any,
scope,
node,
to_invalidate,
true
)
);
}
}
}
});
}
component.rewrite_props(({ name }) => {
component.rewrite_props(({ name, reassigned }) => {
const value = `$${name}`;
let insert = b`${value} = @get_store_value(${name})`;
let insert = reassigned
? b`${`$$subscribe_${name}`}()`
: b`${`$$unsubscribe_${name}`} = @subscribe(${name}, #value => $${value} = #value)`;
if (component.compile_options.dev) {
insert = b`@validate_store(${name}, '${name}'); ${insert}`;
}
@ -99,38 +172,28 @@ export default function ssr(
do {
$$settled = true;
${reactive_store_values}
${injected.map(name => b`let ${name};`)}
${reactive_declarations}
$$rendered = ${literal};
} while (!$$settled);
${reactive_store_unsubscriptions}
return $$rendered;
`
: b`
${reactive_store_values}
${injected.map(name => b`let ${name};`)}
${reactive_declarations}
${reactive_store_unsubscriptions}
return ${literal};`;
const blocks = [
...injected.map(name => b`let ${name};`),
rest,
slots,
...reactive_stores.map(({ name }) => {
const store_name = name.slice(1);
const store = component.var_lookup.get(store_name);
if (store && store.hoistable) {
return b`let ${name} = @get_store_value(${store_name});`;
}
return b`let ${name};`;
}),
...reactive_store_declarations,
...reactive_store_subscriptions,
instance_javascript,
...parent_bindings,
css.code && b`$$result.css.add(#css);`,

@ -39,6 +39,10 @@ function parse_attributes(str: string) {
return attrs;
}
function get_file_basename(filename: string) {
return filename.split(/[/\\]/).pop();
}
interface Replacement {
offset: number;
length: number;
@ -46,7 +50,7 @@ interface Replacement {
}
async function replace_async(
filename: string,
file_basename: string,
source: string,
get_location: ReturnType<typeof getLocator>,
re: RegExp,
@ -73,13 +77,13 @@ async function replace_async(
)) {
// content = unchanged source characters before the replaced segment
const content = StringWithSourcemap.from_source(
filename, source.slice(last_end, offset), get_location(last_end));
file_basename, source.slice(last_end, offset), get_location(last_end));
out.concat(content).concat(replacement);
last_end = offset + length;
}
// final_content = unchanged source characters after last replaced segment
const final_content = StringWithSourcemap.from_source(
filename, source.slice(last_end), get_location(last_end));
file_basename, source.slice(last_end), get_location(last_end));
return out.concat(final_content);
}
@ -160,7 +164,7 @@ function decoded_sourcemap_from_generator(generator: any) {
* Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap
*/
function get_replacement(
filename: string,
file_basename: string,
offset: number,
get_location: ReturnType<typeof getLocator>,
original: string,
@ -171,9 +175,9 @@ function get_replacement(
// Convert the unchanged prefix and suffix to StringWithSourcemap
const prefix_with_map = StringWithSourcemap.from_source(
filename, prefix, get_location(offset));
file_basename, prefix, get_location(offset));
const suffix_with_map = StringWithSourcemap.from_source(
filename, suffix, get_location(offset + prefix.length + original.length));
file_basename, suffix, get_location(offset + prefix.length + original.length));
// Convert the preprocessed code and its sourcemap to a StringWithSourcemap
let decoded_map: DecodedSourceMap;
@ -186,7 +190,11 @@ function get_replacement(
// import decoded sourcemap from mozilla/source-map/SourceMapGenerator
decoded_map = decoded_sourcemap_from_generator(decoded_map);
}
sourcemap_add_offset(decoded_map, get_location(offset + prefix.length));
// offset only segments pointing at original component source
const source_index = decoded_map.sources.indexOf(file_basename);
if (source_index !== -1) {
sourcemap_add_offset(decoded_map, get_location(offset + prefix.length), source_index);
}
}
const processed_with_map = StringWithSourcemap.from_processed(processed.code, decoded_map);
@ -203,6 +211,9 @@ export default async function preprocess(
const filename = (options && options.filename) || preprocessor.filename; // legacy
const dependencies = [];
// preprocess source must be relative to itself or equal null
const file_basename = filename == null ? null : get_file_basename(filename);
const preprocessors = preprocessor
? Array.isArray(preprocessor) ? preprocessor : [preprocessor]
: [];
@ -246,13 +257,13 @@ export default async function preprocess(
: /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
const res = await replace_async(
filename,
file_basename,
source,
get_location,
tag_regex,
async (match, attributes = '', content = '', offset) => {
const no_change = () => StringWithSourcemap.from_source(
filename, match, get_location(offset));
file_basename, match, get_location(offset));
if (!attributes && !content) {
return no_change();
}
@ -265,10 +276,13 @@ export default async function preprocess(
attributes: parse_attributes(attributes),
filename
});
if (!processed) return no_change();
if (processed.dependencies) dependencies.push(...processed.dependencies);
return get_replacement(filename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, `</${tag_name}>`);
if (processed && processed.dependencies) {
dependencies.push(...processed.dependencies);
}
if (!processed || !processed.map && processed.code === content) {
return no_change();
}
return get_replacement(file_basename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, `</${tag_name}>`);
}
);
source = res.string;
@ -285,7 +299,7 @@ export default async function preprocess(
// Combine all the source maps for each preprocessor function into one
const map: RawSourceMap = combine_sourcemaps(
filename,
file_basename,
sourcemap_list
);

@ -13,21 +13,22 @@ function last_line_length(s: string) {
// mutate map in-place
export function sourcemap_add_offset(
map: DecodedSourceMap, offset: SourceLocation
map: DecodedSourceMap, offset: SourceLocation, source_index: number
) {
if (map.mappings.length == 0) return map;
// shift columns in first line
const segment_list = map.mappings[0];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[3]) seg[3] += offset.column;
}
// shift lines
if (map.mappings.length == 0) return;
for (let line = 0; line < map.mappings.length; line++) {
const segment_list = map.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[2]) seg[2] += offset.line;
// shift only segments that belong to component source file
if (seg[1] === source_index) { // also ensures that seg.length >= 4
// shift column if it points at the first line
if (seg[2] === 0) {
seg[3] += offset.column;
}
// shift line
seg[2] += offset.line;
}
}
}
}
@ -97,6 +98,9 @@ export class StringWithSourcemap {
return this;
}
// compute last line length before mutating
const column_offset = last_line_length(this.string);
this.string += other.string;
const m1 = this.map;
@ -117,8 +121,8 @@ export class StringWithSourcemap {
const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[1]) seg[1] = new_source_idx[seg[1]];
if (seg[4]) seg[4] = new_name_idx[seg[4]];
if (seg[1] >= 0) seg[1] = new_source_idx[seg[1]];
if (seg[4] >= 0) seg[4] = new_name_idx[seg[4]];
}
}
} else if (sources_idx_changed) {
@ -126,7 +130,7 @@ export class StringWithSourcemap {
const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[1]) seg[1] = new_source_idx[seg[1]];
if (seg[1] >= 0) seg[1] = new_source_idx[seg[1]];
}
}
} else if (names_idx_changed) {
@ -134,7 +138,7 @@ export class StringWithSourcemap {
const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[4]) seg[4] = new_name_idx[seg[4]];
if (seg[4] >= 0) seg[4] = new_name_idx[seg[4]];
}
}
}
@ -146,7 +150,6 @@ export class StringWithSourcemap {
// 2. first line of second map
// columns of 2 must be shifted
const column_offset = last_line_length(this.string);
if (m2.mappings.length > 0 && column_offset > 0) {
const first_line = m2.mappings[0];
for (let i = 0; i < first_line.length; i++) {
@ -164,12 +167,23 @@ export class StringWithSourcemap {
}
static from_processed(string: string, map?: DecodedSourceMap): StringWithSourcemap {
if (map) return new StringWithSourcemap(string, map);
const line_count = string.split('\n').length;
if (map) {
// ensure that count of source map mappings lines
// is equal to count of generated code lines
// (some tools may produce less)
const missing_lines = line_count - map.mappings.length;
for (let i = 0; i < missing_lines; i++) {
map.mappings.push([]);
}
return new StringWithSourcemap(string, map);
}
if (string == '') return new StringWithSourcemap();
map = { version: 3, names: [], sources: [], mappings: [] };
// add empty SourceMapSegment[] for every line
const line_count = (string.match(/\n/g) || '').length;
for (let i = 0; i < line_count; i++) map.mappings.push([]);
return new StringWithSourcemap(string, map);
}

@ -10,5 +10,6 @@ export {
hasContext,
tick,
createEventDispatcher,
SvelteComponentDev as SvelteComponent
SvelteComponentDev as SvelteComponent,
SvelteComponentTyped
} from 'svelte/internal';

@ -214,19 +214,19 @@ if (typeof HTMLElement === 'function') {
};
}
export class SvelteComponent<
Props extends Record<string, any> = any,
Events extends Record<string, any> = any
> {
/**
* Base class for Svelte components. Used when dev=false.
*/
export class SvelteComponent {
$$: T$$;
$$set?: ($$props: Partial<Props>) => void;
$$set?: ($$props: any) => void;
$destroy() {
destroy_component(this, 1);
this.$destroy = noop;
}
$on<K extends Extract<keyof Events, string>>(type: K, callback: (e: Events[K]) => void) {
$on(type, callback) {
const { $$ } = this;
const callbacks = ($$.callbacks[type] || ($$.callbacks[type] = []));
callbacks.push(callback);
@ -237,7 +237,7 @@ export class SvelteComponent<
};
}
$set($$props: Partial<Props>) {
$set($$props) {
const { $$set, $$ } = this;
if ($$set && !is_empty($$props)) {
$$.skip_bound = true;

@ -28,7 +28,9 @@ export function handle_promise(promise, info) {
if (i !== index && block) {
group_outros();
transition_out(block, 1, 1, () => {
info.blocks[i] = null;
if (info.blocks[i] === block) {
info.blocks[i] = null;
}
});
check_outros();
}

@ -97,22 +97,102 @@ export function validate_slots(name, slot, keys) {
}
}
export interface SvelteComponentDev<
type Props = Record<string, any>;
export interface SvelteComponentDev {
$set(props?: Props): void;
$on(event: string, callback: (event: any) => void): () => void;
$destroy(): void;
[accessor: string]: any;
}
/**
* Base class for Svelte components with some minor dev-enhancements. Used when dev=true.
*/
export class SvelteComponentDev extends SvelteComponent {
/**
* @private
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
*/
$$prop_def: Props;
constructor(options: {
target: Element;
anchor?: Element;
props?: Props;
hydrate?: boolean;
intro?: boolean;
$$inline?: boolean;
}) {
if (!options || (!options.target && !options.$$inline)) {
throw new Error("'target' is a required option");
}
super();
}
$destroy() {
super.$destroy();
this.$destroy = () => {
console.warn('Component was already destroyed'); // eslint-disable-line no-console
};
}
$capture_state() {}
$inject_state() {}
}
// TODO https://github.com/microsoft/TypeScript/issues/41770 is the reason
// why we have to split out SvelteComponentTyped to not break existing usage of SvelteComponent.
// Try to find a better way for Svelte 4.0.
export interface SvelteComponentTyped<
Props extends Record<string, any> = any,
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
Slots extends Record<string, any> = any // eslint-disable-line @typescript-eslint/no-unused-vars
> {
$set(props?: Partial<Props>): void;
$on<K extends Extract<keyof Events, string>>(type: K, callback: (e: Events[K]) => void): () => void;
$destroy(): void;
[accessor: string]: any;
}
export class SvelteComponentDev<
/**
* Base class to create strongly typed Svelte components.
* This only exists for typing purposes and should be used in `.d.ts` files.
*
* ### Example:
*
* You have component library on npm called `component-library`, from which
* you export a component called `MyComponent`. For Svelte+TypeScript users,
* you want to provide typings. Therefore you create a `index.d.ts`:
* ```ts
* import { SvelteComponentTyped } from "svelte";
* export class MyComponent extends SvelteComponentTyped<{foo: string}> {}
* ```
* Typing this makes it possible for IDEs like VS Code with the Svelte extension
* to provide intellisense and to use the component like this in a Svelte file
* with TypeScript:
* ```svelte
* <script lang="ts">
* import { MyComponent } from "component-library";
* </script>
* <MyComponent foo={'bar'} />
* ```
*
* #### Why not make this part of `SvelteComponent(Dev)`?
* Because
* ```ts
* class ASubclassOfSvelteComponent extends SvelteComponent<{foo: string}> {}
* const component: typeof SvelteComponent = ASubclassOfSvelteComponent;
* ```
* will throw a type error, so we need to seperate the more strictly typed class.
*/
export class SvelteComponentTyped<
Props extends Record<string, any> = any,
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> extends SvelteComponent<Props, Events> {
> extends SvelteComponentDev {
/**
* @private
* For type checking capabilities only.
@ -142,24 +222,9 @@ export class SvelteComponentDev<
hydrate?: boolean;
intro?: boolean;
$$inline?: boolean;
}) {
if (!options || (!options.target && !options.$$inline)) {
throw new Error("'target' is a required option");
}
super();
}
$destroy() {
super.$destroy();
this.$destroy = () => {
console.warn('Component was already destroyed'); // eslint-disable-line no-console
};
}) {
super(options);
}
$capture_state() {}
$inject_state() {}
}
export function loop_guard(timeout) {

@ -261,7 +261,6 @@ export function is_crossorigin() {
export function add_resize_listener(node: HTMLElement, fn: () => void) {
const computed_style = getComputedStyle(node);
const z_index = (parseInt(computed_style.zIndex) || 0) - 1;
if (computed_style.position === 'static') {
node.style.position = 'relative';
@ -270,7 +269,7 @@ export function add_resize_listener(node: HTMLElement, fn: () => void) {
const iframe = element('iframe');
iframe.setAttribute('style',
'display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%; ' +
`overflow: hidden; border: 0; opacity: 0; pointer-events: none; z-index: ${z_index};`
'overflow: hidden; border: 0; opacity: 0; pointer-events: none; z-index: -1;'
);
iframe.setAttribute('aria-hidden', 'true');
iframe.tabIndex = -1;

@ -98,6 +98,8 @@ let shadowedByModule;
const priv = "priv";
function instance($$self, $$props, $$invalidate) {
let computed;
let $prop,
$$unsubscribe_prop = noop,
$$subscribe_prop = () => ($$unsubscribe_prop(), $$unsubscribe_prop = subscribe(prop, $$value => $$invalidate(2, $prop = $$value)), prop);
@ -145,8 +147,6 @@ function instance($$self, $$props, $$invalidate) {
if ("computed" in $$props) computed = $$props.computed;
};
let computed;
if ($$props && "$$inject" in $$props) {
$$self.$inject_state($$props.$$inject);
}

@ -43,7 +43,8 @@ function create_each_block(key_1, ctx) {
insert(target, div, anchor);
append(div, t);
},
p(ctx, dirty) {
p(new_ctx, dirty) {
ctx = new_ctx;
if (dirty & /*things*/ 1 && t_value !== (t_value = /*thing*/ ctx[1].name + "")) set_data(t, t_value);
},
r() {
@ -93,7 +94,7 @@ function create_fragment(ctx) {
},
p(ctx, [dirty]) {
if (dirty & /*things*/ 1) {
const each_value = /*things*/ ctx[0];
each_value = /*things*/ ctx[0];
for (let i = 0; i < each_blocks.length; i += 1) each_blocks[i].r();
each_blocks = update_keyed_each(each_blocks, dirty, get_key, 1, ctx, each_value, each_1_lookup, each_1_anchor.parentNode, fix_and_destroy_block, create_each_block, each_1_anchor, get_each_context);
for (let i = 0; i < each_blocks.length; i += 1) each_blocks[i].a();

@ -39,7 +39,8 @@ function create_each_block(key_1, ctx) {
insert(target, div, anchor);
append(div, t);
},
p(ctx, dirty) {
p(new_ctx, dirty) {
ctx = new_ctx;
if (dirty & /*things*/ 1 && t_value !== (t_value = /*thing*/ ctx[1].name + "")) set_data(t, t_value);
},
d(detaching) {
@ -78,7 +79,7 @@ function create_fragment(ctx) {
},
p(ctx, [dirty]) {
if (dirty & /*things*/ 1) {
const each_value = /*things*/ ctx[0];
each_value = /*things*/ ctx[0];
each_blocks = update_keyed_each(each_blocks, dirty, get_key, 1, ctx, each_value, each_1_lookup, each_1_anchor.parentNode, destroy_block, create_each_block, each_1_anchor, get_each_context);
}
},

@ -39,6 +39,8 @@ function create_fragment(ctx) {
}
function instance($$self, $$props, $$invalidate) {
let x;
let y;
let a = 1, b = 2, c = 3;
onMount(() => {
@ -54,9 +56,6 @@ function instance($$self, $$props, $$invalidate) {
return () => clearInterval(interval);
});
let x;
let y;
$$self.$$.update = () => {
if ($$self.$$.dirty & /*b*/ 2) {
$: $$invalidate(0, y = b * 2);

@ -0,0 +1,8 @@
export default {
html: `
<button>action</button>
`,
async test({ assert, target, window }) {
assert.equal(target.querySelector('button').foo, 'bar1337');
}
};

@ -0,0 +1,12 @@
<script>
const obj = {
deep: {
foo : 'bar',
action(element, { leet }) {
element.foo = this.foo + leet;
}
}
};
</script>
<button use:obj.deep.action={{ leet: 1337 }}>action</button>

@ -18,7 +18,7 @@ export default {
`,
test({ assert, component, target, window, raf }) {
let divs = document.querySelectorAll('div');
let divs = window.document.querySelectorAll('div');
divs.forEach(div => {
div.getBoundingClientRect = function() {
const index = [...this.parentNode.children].indexOf(this);
@ -41,7 +41,7 @@ export default {
{ id: 1, name: 'a' }
];
divs = document.querySelectorAll('div');
divs = window.document.querySelectorAll('div');
assert.equal(divs[0].dy, 120);
assert.equal(divs[4].dy, -120);
@ -56,5 +56,29 @@ export default {
raf.tick(150);
assert.equal(divs[0].dy, 0);
assert.equal(divs[4].dy, 0);
component.things = [
{ id: 1, name: 'a' },
{ id: 2, name: 'b' },
{ id: 3, name: 'c' },
{ id: 4, name: 'd' },
{ id: 5, name: 'e' }
];
divs = document.querySelectorAll('div');
assert.equal(divs[0].dy, 120);
assert.equal(divs[4].dy, -120);
raf.tick(200);
assert.equal(divs[0].dy, 108);
assert.equal(divs[4].dy, -60);
raf.tick(250);
assert.equal(divs[0].dy, 48);
assert.equal(divs[4].dy, 0);
raf.tick(300);
assert.equal(divs[0].dy, 0);
assert.equal(divs[4].dy, 0);
}
};

@ -0,0 +1,53 @@
export default {
html: `
<input type="checkbox" value="a" data-index="x-1">
<input type="checkbox" value="b" data-index="x-1">
<input type="checkbox" value="c" data-index="x-1">
<input type="checkbox" value="a" data-index="x-2">
<input type="checkbox" value="b" data-index="x-2">
<input type="checkbox" value="c" data-index="x-2">
<input type="checkbox" value="a" data-index="y-1">
<input type="checkbox" value="b" data-index="y-1">
<input type="checkbox" value="c" data-index="y-1">
<input type="checkbox" value="a" data-index="y-2">
<input type="checkbox" value="b" data-index="y-2">
<input type="checkbox" value="c" data-index="y-2">
<input type="checkbox" value="a" data-index="z-1">
<input type="checkbox" value="b" data-index="z-1">
<input type="checkbox" value="c" data-index="z-1">
<input type="checkbox" value="a" data-index="z-2">
<input type="checkbox" value="b" data-index="z-2">
<input type="checkbox" value="c" data-index="z-2">
`,
async test({ assert, component, target, window }) {
const inputs = target.querySelectorAll('input');
const checked = new Set();
const checkInbox = async (i) => {
checked.add(i);
inputs[i].checked = true;
await inputs[i].dispatchEvent(event);
};
for (let i = 0; i < 18; i++) {
assert.equal(inputs[i].checked, checked.has(i));
}
const event = new window.Event('change');
await checkInbox(2);
for (let i = 0; i < 18; i++) {
assert.equal(inputs[i].checked, checked.has(i));
}
await checkInbox(12);
for (let i = 0; i < 18; i++) {
assert.equal(inputs[i].checked, checked.has(i));
}
await checkInbox(8);
for (let i = 0; i < 18; i++) {
assert.equal(inputs[i].checked, checked.has(i));
}
}
};

@ -0,0 +1,15 @@
<script>
const list = [
{ id: 'x', data: [{ id: 1, data: [] }, { id: 2, data: [] }] },
{ id: 'y', data: [{ id: 1, data: [] }, { id: 2, data: [] }] },
{ id: 'z', data: [{ id: 1, data: [] }, { id: 2, data: [] }] }
];
</script>
{#each list as { id, data }}
{#each data as item}
<input type="checkbox" bind:group={item.data} value="a" data-index="{id}-{item.id}" />
<input type="checkbox" bind:group={item.data} value="b" data-index="{id}-{item.id}" />
<input type="checkbox" bind:group={item.data} value="c" data-index="{id}-{item.id}" />
{/each}
{/each}

@ -0,0 +1,7 @@
export default {
async test({ assert, target }) {
const iframe = target.querySelector('iframe');
assert.equal(iframe.style.zIndex, '-1');
}
};

@ -0,0 +1,8 @@
<script>
let offsetWidth = 0;
let offsetHeight = 0;
</script>
<div style="z-index: 42;" bind:offsetHeight bind:offsetWidth>
<h1>Hello</h1>
</div>

@ -0,0 +1,13 @@
let result;
export default {
before_test() {
result = [];
},
props: {
collect: (str) => result.push(str)
},
test({ assert }) {
assert.deepEqual(result, ['each_action', 'import_action']);
}
};

@ -0,0 +1,17 @@
<script>
import action from './util.js';
export let collect;
function each_action(_, fn) {
fn('each_action');
}
const array = [each_action];
</script>
<div use:action={collect} />
<ul>
{#each array as action}
<div use:action={collect} />
{/each}
</ul>

@ -0,0 +1,3 @@
export default function (_, fn) {
fn('import_action');
}

@ -0,0 +1,65 @@
export default {
html: `
<label><input type="checkbox" value="Vanilla"> Vanilla</label>
<label><input type="checkbox" value="Strawberry"> Strawberry</label>
<label><input type="checkbox" value="Chocolate"> Chocolate</label>
<label><input type="checkbox" value="Lemon"> Lemon</label>
<label><input type="checkbox" value="Coconut"> Coconut</label>
`,
async test({ assert, target, window }) {
const [input1, input2, input3, input4, input5] = target.querySelectorAll('input');
const event = new window.Event('change');
input3.checked = true;
await input3.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<label><input type="checkbox" value="Chocolate"> Chocolate</label>
<label><input type="checkbox" value="Vanilla"> Vanilla</label>
<label><input type="checkbox" value="Strawberry"> Strawberry</label>
<label><input type="checkbox" value="Lemon"> Lemon</label>
<label><input type="checkbox" value="Coconut"> Coconut</label>
`);
assert.equal(input1.checked, false);
assert.equal(input2.checked, false);
assert.equal(input3.checked, true);
assert.equal(input4.checked, false);
assert.equal(input5.checked, false);
input4.checked = true;
await input4.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<label><input type="checkbox" value="Chocolate"> Chocolate</label>
<label><input type="checkbox" value="Lemon"> Lemon</label>
<label><input type="checkbox" value="Vanilla"> Vanilla</label>
<label><input type="checkbox" value="Strawberry"> Strawberry</label>
<label><input type="checkbox" value="Coconut"> Coconut</label>
`);
assert.equal(input1.checked, false);
assert.equal(input2.checked, false);
assert.equal(input3.checked, true);
assert.equal(input4.checked, true);
assert.equal(input5.checked, false);
input3.checked = false;
await input3.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<label><input type="checkbox" value="Lemon"> Lemon</label>
<label><input type="checkbox" value="Chocolate"> Chocolate</label>
<label><input type="checkbox" value="Vanilla"> Vanilla</label>
<label><input type="checkbox" value="Strawberry"> Strawberry</label>
<label><input type="checkbox" value="Coconut"> Coconut</label>
`);
assert.equal(input1.checked, false);
assert.equal(input2.checked, false);
assert.equal(input3.checked, false);
assert.equal(input4.checked, true);
assert.equal(input5.checked, false);
}
};

@ -0,0 +1,21 @@
<script>
let flavours = [
'Vanilla',
'Strawberry',
'Chocolate',
'Lemon',
'Coconut'
];
let choices = [];
// Put choices first by sorting
$: flavours = flavours.sort((a, b) => choices.includes(b) - choices.includes(a));
</script>
{#each flavours as flavour (flavour)}
<label>
<input type=checkbox bind:group={choices} value={flavour}>
{flavour}
</label>
{/each}

@ -1,6 +1,5 @@
// destructure to store value
export default {
skip_if_ssr: true, // pending https://github.com/sveltejs/svelte/issues/3582
html: '<h1>2 2 xxx 5 6 9 10 2</h1>',
async test({ assert, target, component }) {
await component.update();

@ -1,7 +1,6 @@
// destructure to store
export default {
html: '<h1>2 2 xxx 5 6 9 10 2</h1>',
skip_if_ssr: true,
async test({ assert, target, component }) {
await component.update();
assert.htmlEqual(target.innerHTML, '<h1>11 11 yyy 12 13 14 15 11</h1>');

@ -0,0 +1,3 @@
export default {
html: '<p>aca</p>'
};

@ -0,0 +1,12 @@
<script>
export let a = 'a';
let b;
$: c = a;
function foo() {
b = c === 'a' ? 'b' : 'c';
}
foo();
</script>
<p>{a}{b}{c}</p>

@ -5,7 +5,6 @@ export default {
<button></button>
<button></button>
`,
skip_if_ssr: true,
async test({ assert, component, target, window }) {
const [btn1, btn2] = target.querySelectorAll('button');

@ -0,0 +1,3 @@
export default {
html: '{"answer":4}'
};

@ -0,0 +1,31 @@
<script>
import { writable } from '../../../../store';
let value = writable({ foo: 1, bar: 2 });
$value.foo = $value.foo + $value.bar; // 3
$value.bar = $value.foo * $value.bar; // 6
// should resubscribe immediately
value = writable({ foo: $value.foo + 2, bar: $value.bar - 2 }); // { foo: 5, bar: 4 }
// should mutate the store value
$value.baz = $value.foo + $value.bar; // { foo: 5, bar: 4, baz: 9 }
// should resubscribe immediately
value = writable({ qux: $value.baz - $value.foo }); // { qux: 4 }
// making sure instrumentation returns the expression value
$value = {
one: writable(
$value = {
two: ({ $value } = { $value: { fred: $value.qux } }) // { fred: 4 }
} // { two: { $value: { fred: 4 } } }
) // { one: { two: { $value: { fred: 4 } } } }
};
const one = $value.one;
value.update(val => ({ answer: $one.two.$value.fred })); // { answer: 4 }
</script>
{JSON.stringify($value)}

@ -0,0 +1,174 @@
let fulfil;
export default {
props: {
promise: new Promise((f) => {
fulfil = f;
})
},
intro: true,
async test({ assert, target, component, raf }) {
assert.htmlEqual(target.innerHTML, '<p class="pending" foo="0.0">loading...</p>');
let time = 0;
raf.tick(time += 50);
assert.htmlEqual(target.innerHTML, '<p class="pending" foo="0.5">loading...</p>');
await fulfil(42);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.0">42</p>
<p class="pending" foo="0.5">loading...</p>
`);
// see the transition 30% complete
raf.tick(time += 30);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.3">42</p>
<p class="pending" foo="0.2">loading...</p>
`);
// completely transition in the {:then} block
raf.tick(time += 70);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="1.0">42</p>
`);
// update promise #1
component.promise = new Promise((f) => {
fulfil = f;
});
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="1.0">42</p>
<p class="pending" foo="0.0">loading...</p>
`);
raf.tick(time += 100);
assert.htmlEqual(target.innerHTML, `
<p class="pending" foo="1.0">loading...</p>
`);
await fulfil(43);
assert.htmlEqual(target.innerHTML, `
<p class="pending" foo="1.0">loading...</p>
<p class="then" foo="0.0">43</p>
`);
raf.tick(time += 100);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="1.0">43</p>
`);
// update promise #2
component.promise = new Promise((f) => {
fulfil = f;
});
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="1.0">43</p>
<p class="pending" foo="0.0">loading...</p>
`);
raf.tick(time += 50);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.5">43</p>
<p class="pending" foo="0.5">loading...</p>
`);
await fulfil(44);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.5">43</p>
<p class="pending" foo="0.5">loading...</p>
<p class="then" foo="0.0">44</p>
`);
raf.tick(time += 100);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="1.0">44</p>
`);
// update promise #3 - quick succession
component.promise = new Promise((f) => {
fulfil = f;
});
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="1.0">44</p>
<p class="pending" foo="0.0">loading...</p>
`);
raf.tick(time += 40);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.6">44</p>
<p class="pending" foo="0.4">loading...</p>
`);
await fulfil(45);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.6">44</p>
<p class="pending" foo="0.4">loading...</p>
<p class="then" foo="0.0">45</p>
`);
raf.tick(time += 20);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.4">44</p>
<p class="pending" foo="0.2">loading...</p>
<p class="then" foo="0.2">45</p>
`);
component.promise = new Promise((f) => {
fulfil = f;
});
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.4">44</p>
<p class="pending" foo="0.2">loading...</p>
<p class="then" foo="0.2">45</p>
<p class="pending" foo="0.0">loading...</p>
`);
raf.tick(time += 10);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.3">44</p>
<p class="pending" foo="0.1">loading...</p>
<p class="then" foo="0.1">45</p>
<p class="pending" foo="0.1">loading...</p>
`);
await fulfil(46);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.3">44</p>
<p class="pending" foo="0.1">loading...</p>
<p class="then" foo="0.1">45</p>
<p class="pending" foo="0.1">loading...</p>
<p class="then" foo="0.0">46</p>
`);
raf.tick(time += 10);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.2">44</p>
<p class="then" foo="0.1">46</p>
`);
raf.tick(time += 20);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="0.3">46</p>
`);
raf.tick(time += 70);
assert.htmlEqual(target.innerHTML, `
<p class="then" foo="1.0">46</p>
`);
}
};

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

Loading…
Cancel
Save