Merge branch 'master' into pr/5600

pull/5600/head
Conduitry 5 years ago
commit e1e2f92488

@ -37,6 +37,8 @@ If you have a stack trace to include, we recommend putting inside a `<details>`
</details> </details>
**Information about your Svelte project:** **Information about your Svelte project:**
To make your life easier, just run `npx envinfo --system --npmPackages svelte,rollup,webpack --binaries --browsers` and paste the output here.
- Your browser and the version: (e.x. Chrome 52.1, Firefox 48.0, IE 10) - Your browser and the version: (e.x. Chrome 52.1, Firefox 48.0, IE 10)
- Your operating system: (e.x. OS X 10, Ubuntu Linux 19.10, Windows XP, etc) - Your operating system: (e.x. OS X 10, Ubuntu Linux 19.10, Windows XP, etc)

4
.gitignore vendored

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

@ -1,5 +1,29 @@
# Svelte changelog # Svelte changelog
## Unreleased
* Fix ordering of elements when using `{#if}` inside `{#key}` ([#5680](https://github.com/sveltejs/svelte/issues/5680))
* Add `hasContext` lifecycle function ([#5690](https://github.com/sveltejs/svelte/pull/5690))
* Fix missing `walk` types in `svelte/compiler` ([#5696](https://github.com/sveltejs/svelte/pull/5696))
## 3.29.7
* Include `./register` in exports map ([#5670](https://github.com/sveltejs/svelte/issues/5670))
## 3.29.6
* Include `./package.json` in export map ([#5659](https://github.com/sveltejs/svelte/issues/5659))
## 3.29.5
* Fix `$$props` and `$$restProps` when compiling to a custom element ([#5482](https://github.com/sveltejs/svelte/issues/5482))
* Include an export map in `package.json` ([#5556](https://github.com/sveltejs/svelte/issues/5556))
* Fix function calls in `<slot>` props that use contextual values ([#5565](https://github.com/sveltejs/svelte/issues/5565))
* Fix handling aborted transitions in `{:else}` blocks ([#5573](https://github.com/sveltejs/svelte/issues/5573))
* Add `Element` and `Node` to known globals ([#5586](https://github.com/sveltejs/svelte/issues/5586))
* Fix `$$slots` when compiling to custom elements ([#5594](https://github.com/sveltejs/svelte/issues/5594))
* Fix internal `import`s so that we're exposing a valid ES module ([#5617](https://github.com/sveltejs/svelte/issues/5617))
## 3.29.4 ## 3.29.4
* Fix code generation error with `??` alongside logical operators ([#5558](https://github.com/sveltejs/svelte/issues/5558)) * Fix code generation error with `??` alongside logical operators ([#5558](https://github.com/sveltejs/svelte/issues/5558))

@ -9,7 +9,7 @@
<img src="https://img.shields.io/npm/l/svelte.svg" alt="license"> <img src="https://img.shields.io/npm/l/svelte.svg" alt="license">
</a> </a>
<a href="https://svelte.dev/chat"> <a href="https://svelte.dev/chat">
<img src="https://img.shields.io/badge/chat-on%20discord-7289da.svg" alt="Chat"> <img src="https://img.shields.io/discord/457912077277855764?label=chat&logo=discord" alt="Chat">
</a> </a>
</p> </p>

28
package-lock.json generated

@ -1,9 +1,19 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.29.4", "version": "3.29.7",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-0.3.0.tgz",
"integrity": "sha512-dqmASpaTCavldZqwdEpokgG4yOXmEiEGPP3ATTsBbdXXSKf6kx8jt2fPcKhodABdZlYe82OehR2oFK1y9gwZxw==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "1.0.0",
"sourcemap-codec": "1.4.8"
}
},
"@babel/code-frame": { "@babel/code-frame": {
"version": "7.10.1", "version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
@ -36,6 +46,12 @@
"integrity": "sha512-KioOCsSvSvXx6xUNLiJz+P+VMb7NRcePjoefOr74Y5P6lEKsiOn35eZyZzgpK4XCNJdXTDR7+zykj0lwxRvZ2g==", "integrity": "sha512-KioOCsSvSvXx6xUNLiJz+P+VMb7NRcePjoefOr74Y5P6lEKsiOn35eZyZzgpK4XCNJdXTDR7+zykj0lwxRvZ2g==",
"dev": true "dev": true
}, },
"@jridgewell/resolve-uri": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-1.0.0.tgz",
"integrity": "sha512-9oLAnygRMi8Q5QkYEU4XWK04B+nuoXoxjRvRxgjuChkLZFBja0YPSgdZ7dZtwhncLBcQe/I/E+fLuk5qxcYVJA==",
"dev": true
},
"@rollup/plugin-commonjs": { "@rollup/plugin-commonjs": {
"version": "11.0.0", "version": "11.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.0.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.0.0.tgz",
@ -144,8 +160,8 @@
} }
}, },
"@sveltejs/eslint-config": { "@sveltejs/eslint-config": {
"version": "github:sveltejs/eslint-config#5d1ba28f99568e42f26d9b7484ab57720f6ec9b4", "version": "github:sveltejs/eslint-config#cca8177349dd5a02b19a5865afc4a7066921409a",
"from": "github:sveltejs/eslint-config#v5.4.0", "from": "github:sveltejs/eslint-config#v5.6.0",
"dev": true "dev": true
}, },
"@tootallnate/once": { "@tootallnate/once": {
@ -3737,9 +3753,9 @@
} }
}, },
"sourcemap-codec": { "sourcemap-codec": {
"version": "1.4.6", "version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg==", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true "dev": true
}, },
"spdx-correct": { "spdx-correct": {

@ -1,6 +1,6 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.29.4", "version": "3.29.7",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"module": "index.mjs", "module": "index.mjs",
"main": "index", "main": "index",
@ -18,6 +18,44 @@
"svelte", "svelte",
"README.md" "README.md"
], ],
"exports": {
"./package.json": "./package.json",
".": {
"import": "./index.mjs",
"require": "./index.js"
},
"./compiler": {
"import": "./compiler.mjs",
"require": "./compiler.js"
},
"./animate": {
"import": "./animate/index.mjs",
"require": "./animate/index.js"
},
"./easing": {
"import": "./easing/index.mjs",
"require": "./easing/index.js"
},
"./internal": {
"import": "./internal/index.mjs",
"require": "./internal/index.js"
},
"./motion": {
"import": "./motion/index.mjs",
"require": "./motion/index.js"
},
"./register": {
"require": "./register.js"
},
"./store": {
"import": "./store/index.mjs",
"require": "./store/index.js"
},
"./transition": {
"import": "./transition/index.mjs",
"require": "./transition/index.js"
}
},
"engines": { "engines": {
"node": ">= 8" "node": ">= 8"
}, },
@ -37,7 +75,7 @@
"posttest": "agadoo internal/index.mjs", "posttest": "agadoo internal/index.mjs",
"prepublishOnly": "npm run lint && PUBLISH=true npm test", "prepublishOnly": "npm run lint && PUBLISH=true npm test",
"tsd": "tsc -p src/compiler --emitDeclarationOnly && tsc -p src/runtime --emitDeclarationOnly", "tsd": "tsc -p src/compiler --emitDeclarationOnly && tsc -p src/runtime --emitDeclarationOnly",
"lint": "eslint '{src,test}/**/*.{ts,js}'" "lint": "eslint \"{src,test}/**/*.{ts,js}\""
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -56,6 +94,7 @@
}, },
"homepage": "https://github.com/sveltejs/svelte#README", "homepage": "https://github.com/sveltejs/svelte#README",
"devDependencies": { "devDependencies": {
"@ampproject/remapping": "^0.3.0",
"@rollup/plugin-commonjs": "^11.0.0", "@rollup/plugin-commonjs": "^11.0.0",
"@rollup/plugin-json": "^4.0.1", "@rollup/plugin-json": "^4.0.1",
"@rollup/plugin-node-resolve": "^6.0.0", "@rollup/plugin-node-resolve": "^6.0.0",
@ -63,7 +102,7 @@
"@rollup/plugin-sucrase": "^3.0.0", "@rollup/plugin-sucrase": "^3.0.0",
"@rollup/plugin-typescript": "^2.0.1", "@rollup/plugin-typescript": "^2.0.1",
"@rollup/plugin-virtual": "^2.0.0", "@rollup/plugin-virtual": "^2.0.0",
"@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.4.0", "@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.6.0",
"@types/mocha": "^7.0.0", "@types/mocha": "^7.0.0",
"@types/node": "^8.10.53", "@types/node": "^8.10.53",
"@typescript-eslint/eslint-plugin": "^3.0.2", "@typescript-eslint/eslint-plugin": "^3.0.2",
@ -89,6 +128,7 @@
"rollup": "^1.27.14", "rollup": "^1.27.14",
"source-map": "^0.7.3", "source-map": "^0.7.3",
"source-map-support": "^0.5.13", "source-map-support": "^0.5.13",
"sourcemap-codec": "^1.4.8",
"tiny-glob": "^0.2.6", "tiny-glob": "^0.2.6",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"typescript": "^3.5.3" "typescript": "^3.5.3"

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

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

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

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

@ -300,6 +300,9 @@ An each block can also have an `{:else}` clause, which is rendered if the list i
```sv ```sv
{#await expression then name}...{/await} {#await expression then name}...{/await}
``` ```
```sv
{#await expression catch name}...{/await}
```
--- ---
@ -342,6 +345,16 @@ If you don't care about the pending state, you can also omit the initial block.
{/await} {/await}
``` ```
---
If conversely you only want to show the error state, you can omit the `then` block.
```sv
{#await promise catch error}
<p>The error is {error}</p>
{/await}
```
### {#key ...} ### {#key ...}
```sv ```sv
@ -1317,6 +1330,31 @@ Named slots allow consumers to target specific areas. They can also have fallbac
</div> </div>
``` ```
#### [`$$slots`](slots_object)
---
`$$slots` is an object whose keys are the names of the slots passed into the component by the parent. If the parent does not pass in a slot with a particular name, that name will not be a present in `$$slots`. This allows components to render a slot (and other elements, like wrappers for styling) only if the parent provides it.
Note that explicitly passing in an empty named slot will add that slot's name to `$$slots`. For example, if a parent passes `<div slot="title" />` to a child component, `$$slots.title` will be truthy within the child.
```sv
<!-- App.svelte -->
<Card>
<h1 slot="title">Blog Post Title</h1>
</Card>
<!-- Card.svelte -->
<div>
<slot name="title"></slot>
{#if $$slots.description}
<!-- This slot and the <hr> before it will not render. -->
<hr>
<slot name="description"></slot>
{/if}
</div>
```
#### [`<slot let:`*name*`={`*value*`}>`](slot_let) #### [`<slot let:`*name*`={`*value*`}>`](slot_let)
--- ---

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -29,7 +29,9 @@ import add_to_set from './utils/add_to_set';
import check_graph_for_cycles from './utils/check_graph_for_cycles'; import check_graph_for_cycles from './utils/check_graph_for_cycles';
import { print, x, b } from 'code-red'; import { print, x, b } from 'code-red';
import { is_reserved_keyword } from './utils/reserved_keywords'; import { is_reserved_keyword } from './utils/reserved_keywords';
import { apply_preprocessor_sourcemap } from '../utils/string_with_sourcemap';
import Element from './nodes/Element'; import Element from './nodes/Element';
import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types';
interface ComponentOptions { interface ComponentOptions {
namespace?: string; namespace?: string;
@ -267,17 +269,13 @@ export default class Component {
this.helpers.set(name, alias); this.helpers.set(name, alias);
node.name = alias.name; node.name = alias.name;
} }
} } else if (node.name[0] !== '#' && !is_valid(node.name)) {
else if (node.name[0] !== '#' && !is_valid(node.name)) {
// this hack allows x`foo.${bar}` where bar could be invalid // this hack allows x`foo.${bar}` where bar could be invalid
const literal: Literal = { type: 'Literal', value: node.name }; const literal: Literal = { type: 'Literal', value: node.name };
if (parent.type === 'Property' && key === 'key') { if (parent.type === 'Property' && key === 'key') {
parent.key = literal; parent.key = literal;
} } else if (parent.type === 'MemberExpression' && key === 'property') {
else if (parent.type === 'MemberExpression' && key === 'property') {
parent.property = literal; parent.property = literal;
parent.computed = true; parent.computed = true;
} }
@ -330,6 +328,8 @@ export default class Component {
js.map.sourcesContent = [ js.map.sourcesContent = [
this.source this.source
]; ];
js.map = apply_preprocessor_sourcemap(this.file, js.map, compile_options.sourcemap as (string | RawSourceMap | DecodedSourceMap));
} }
return { return {
@ -521,8 +521,7 @@ export default class Component {
if (this.hoistable_nodes.has(node)) return false; if (this.hoistable_nodes.has(node)) return false;
if (this.reactive_declaration_nodes.has(node)) return false; if (this.reactive_declaration_nodes.has(node)) return false;
if (node.type === 'ImportDeclaration') return false; if (node.type === 'ImportDeclaration') return false;
if (node.type === 'ExportDeclaration' && node.specifiers.length > 0) if (node.type === 'ExportDeclaration' && node.specifiers.length > 0) return false;
return false;
return true; return true;
}); });
} }
@ -1038,8 +1037,9 @@ export default class Component {
this.vars.find( this.vars.find(
variable => variable.name === name && variable.module variable => variable.name === name && variable.module
) )
) ) {
return false; return false;
}
return true; return true;
}); });
@ -1288,8 +1288,9 @@ export default class Component {
declaration.dependencies.forEach(name => { declaration.dependencies.forEach(name => {
if (declaration.assignees.has(name)) return; if (declaration.assignees.has(name)) return;
const earlier_declarations = lookup.get(name); const earlier_declarations = lookup.get(name);
if (earlier_declarations) if (earlier_declarations) {
earlier_declarations.forEach(add_declaration); earlier_declarations.forEach(add_declaration);
}
}); });
this.reactive_declarations.push(declaration); this.reactive_declarations.push(declaration);
@ -1319,8 +1320,9 @@ export default class Component {
if (globals.has(name) && node.type !== 'InlineComponent') return; if (globals.has(name) && node.type !== 'InlineComponent') return;
let message = `'${name}' is not defined`; let message = `'${name}' is not defined`;
if (!this.ast.instance) if (!this.ast.instance) {
message += `. Consider adding a <script> block with 'export let ${name}' to declare a prop`; message += `. Consider adding a <script> block with 'export let ${name}' to declare a prop`;
}
this.warn(node, { this.warn(node, {
code: 'missing-declaration', code: 'missing-declaration',
@ -1382,8 +1384,9 @@ function process_component_options(component: Component, nodes) {
const message = "'tag' must be a string literal"; const message = "'tag' must be a string literal";
const tag = get_value(attribute, code, message); const tag = get_value(attribute, code, message);
if (typeof tag !== 'string' && tag !== null) if (typeof tag !== 'string' && tag !== null) {
component.error(attribute, { code, message }); component.error(attribute, { code, message });
}
if (tag && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) { if (tag && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
component.error(attribute, { component.error(attribute, {
@ -1408,8 +1411,9 @@ function process_component_options(component: Component, nodes) {
const message = "The 'namespace' attribute must be a string literal representing a valid namespace"; const message = "The 'namespace' attribute must be a string literal representing a valid namespace";
const ns = get_value(attribute, code, message); const ns = get_value(attribute, code, message);
if (typeof ns !== 'string') if (typeof ns !== 'string') {
component.error(attribute, { code, message }); component.error(attribute, { code, message });
}
if (valid_namespaces.indexOf(ns) === -1) { if (valid_namespaces.indexOf(ns) === -1) {
const match = fuzzymatch(ns, valid_namespaces); const match = fuzzymatch(ns, valid_namespaces);
@ -1437,8 +1441,9 @@ function process_component_options(component: Component, nodes) {
const message = `${name} attribute must be true or false`; const message = `${name} attribute must be true or false`;
const value = get_value(attribute, code, message); const value = get_value(attribute, code, message);
if (typeof value !== 'boolean') if (typeof value !== 'boolean') {
component.error(attribute, { code, message }); component.error(attribute, { code, message });
}
component_options[name] = value; component_options[name] = value;
break; break;

@ -248,25 +248,17 @@ function block_might_apply_to_node(block: Block, node: Element): BlockAppliesToN
if (selector.type === 'ClassSelector') { if (selector.type === 'ClassSelector') {
if (!attribute_matches(node, 'class', name, '~=', false) && !node.classes.some(c => c.name === name)) return BlockAppliesToNode.NotPossible; if (!attribute_matches(node, 'class', name, '~=', false) && !node.classes.some(c => c.name === name)) return BlockAppliesToNode.NotPossible;
} } else if (selector.type === 'IdSelector') {
else if (selector.type === 'IdSelector') {
if (!attribute_matches(node, 'id', name, '=', false)) return BlockAppliesToNode.NotPossible; if (!attribute_matches(node, 'id', name, '=', false)) return BlockAppliesToNode.NotPossible;
} } else if (selector.type === 'AttributeSelector') {
else if (selector.type === 'AttributeSelector') {
if ( if (
!(whitelist_attribute_selector.has(node.name.toLowerCase()) && whitelist_attribute_selector.get(node.name.toLowerCase()).has(selector.name.name.toLowerCase())) && !(whitelist_attribute_selector.has(node.name.toLowerCase()) && whitelist_attribute_selector.get(node.name.toLowerCase()).has(selector.name.name.toLowerCase())) &&
!attribute_matches(node, selector.name.name, selector.value && unquote(selector.value), selector.matcher, selector.flags)) { !attribute_matches(node, selector.name.name, selector.value && unquote(selector.value), selector.matcher, selector.flags)) {
return BlockAppliesToNode.NotPossible; return BlockAppliesToNode.NotPossible;
} }
} } else if (selector.type === 'TypeSelector') {
else if (selector.type === 'TypeSelector') {
if (node.name.toLowerCase() !== name.toLowerCase() && name !== '*') return BlockAppliesToNode.NotPossible; if (node.name.toLowerCase() !== name.toLowerCase() && name !== '*') return BlockAppliesToNode.NotPossible;
} } else {
else {
return BlockAppliesToNode.UnknownSelectorType; return BlockAppliesToNode.UnknownSelectorType;
} }
} }

@ -167,9 +167,7 @@ class Atrule {
this.children.forEach(child => { this.children.forEach(child => {
child.apply(node); child.apply(node);
}); });
} } else if (is_keyframes_node(this.node)) {
else if (is_keyframes_node(this.node)) {
this.children.forEach((rule: Rule) => { this.children.forEach((rule: Rule) => {
rule.selectors.forEach(selector => { rule.selectors.forEach(selector => {
selector.used = true; selector.used = true;

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

@ -11,6 +11,7 @@ const valid_options = [
'format', 'format',
'name', 'name',
'filename', 'filename',
'sourcemap',
'generate', 'generate',
'outputFilename', 'outputFilename',
'cssOutputFilename', 'cssOutputFilename',

@ -27,7 +27,7 @@ export default class Animation extends Node {
// TODO can we relax the 'immediate child' rule? // TODO can we relax the 'immediate child' rule?
component.error(this, { component.error(this, {
code: 'invalid-animation', code: 'invalid-animation',
message: 'An element that use the animate directive must be the immediate child of a keyed each block' message: 'An element that uses the animate directive must be the immediate child of a keyed each block'
}); });
} }

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

@ -67,17 +67,21 @@ export default class Binding extends Node {
} else { } else {
const variable = component.var_lookup.get(name); const variable = component.var_lookup.get(name);
if (!variable || variable.global) component.error(this.expression.node, { if (!variable || variable.global) {
code: 'binding-undeclared', component.error(this.expression.node, {
message: `${name} is not declared` code: 'binding-undeclared',
}); message: `${name} is not declared`
});
}
variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true; variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
if (info.expression.type === 'Identifier' && !variable.writable) component.error(this.expression.node, { if (info.expression.type === 'Identifier' && !variable.writable) {
code: 'invalid-binding', component.error(this.expression.node, {
message: 'Cannot bind to a variable which is not writable' code: 'invalid-binding',
}); message: 'Cannot bind to a variable which is not writable'
});
}
} }
const type = parent.get_static_attribute_value('type'); const type = parent.get_static_attribute_value('type');

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

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

@ -28,9 +28,7 @@ export default class Window extends Node {
info.attributes.forEach(node => { info.attributes.forEach(node => {
if (node.type === 'EventHandler') { if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(component, this, scope, node)); this.handlers.push(new EventHandler(component, this, scope, node));
} } else if (node.type === 'Binding') {
else if (node.type === 'Binding') {
if (node.expression.type !== 'Identifier') { if (node.expression.type !== 'Identifier') {
const { parts } = flatten_reference(node.expression); const { parts } = flatten_reference(node.expression);
@ -64,13 +62,9 @@ export default class Window extends Node {
} }
this.bindings.push(new Binding(component, this, scope, node)); this.bindings.push(new Binding(component, this, scope, node));
} } else if (node.type === 'Action') {
else if (node.type === 'Action') {
this.actions.push(new Action(component, this, scope, node)); this.actions.push(new Action(component, this, scope, node));
} } else {
else {
// TODO there shouldn't be anything else here... // TODO there shouldn't be anything else here...
} }
}); });

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

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

@ -196,11 +196,6 @@ export default class EachBlockWrapper extends Wrapper {
? !this.next.is_dom_node() : ? !this.next.is_dom_node() :
!parent_node || !this.parent.is_dom_node(); !parent_node || !this.parent.is_dom_node();
this.context_props = this.node.contexts.map(prop => b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.modifier(x`list[i]`)};`);
if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`);
if (this.node.has_binding || this.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`);
const snippet = this.node.expression.manipulate(block); const snippet = this.node.expression.manipulate(block);
block.chunks.init.push(b`let ${this.vars.each_block_value} = ${snippet};`); block.chunks.init.push(b`let ${this.vars.each_block_value} = ${snippet};`);
@ -208,15 +203,6 @@ export default class EachBlockWrapper extends Wrapper {
block.chunks.init.push(b`@validate_each_argument(${this.vars.each_block_value});`); block.chunks.init.push(b`@validate_each_argument(${this.vars.each_block_value});`);
} }
// TODO which is better — Object.create(array) or array.slice()?
renderer.blocks.push(b`
function ${this.vars.get_each_context}(#ctx, list, i) {
const child_ctx = #ctx.slice();
${this.context_props}
return child_ctx;
}
`);
const initial_anchor_node: Identifier = { type: 'Identifier', name: parent_node ? 'null' : '#anchor' }; const initial_anchor_node: Identifier = { type: 'Identifier', name: parent_node ? 'null' : '#anchor' };
const initial_mount_node: Identifier = parent_node || { type: 'Identifier', name: '#target' }; const initial_mount_node: Identifier = parent_node || { type: 'Identifier', name: '#target' };
const update_anchor_node = needs_anchor const update_anchor_node = needs_anchor
@ -360,6 +346,19 @@ export default class EachBlockWrapper extends Wrapper {
if (this.else) { if (this.else) {
this.else.fragment.render(this.else.block, null, x`#nodes` as Identifier); this.else.fragment.render(this.else.block, null, x`#nodes` as Identifier);
} }
this.context_props = this.node.contexts.map(prop => b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.modifier(x`list[i]`)};`);
if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`);
if (this.node.has_binding || this.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`);
// TODO which is better — Object.create(array) or array.slice()?
renderer.blocks.push(b`
function ${this.vars.get_each_context}(#ctx, list, i) {
const child_ctx = #ctx.slice();
${this.context_props}
return child_ctx;
}
`);
} }
render_keyed({ render_keyed({

@ -48,9 +48,10 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
// special case — <option value={foo}> — see below // special case — <option value={foo}> — see below
if (this.parent.node.name === 'option' && node.name === 'value') { if (this.parent.node.name === 'option' && node.name === 'value') {
let select: ElementWrapper = this.parent; let select: ElementWrapper = this.parent;
while (select && (select.node.type !== 'Element' || select.node.name !== 'select')) while (select && (select.node.type !== 'Element' || select.node.name !== 'select')) {
// @ts-ignore todo: doublecheck this, but looks to be correct // @ts-ignore todo: doublecheck this, but looks to be correct
select = select.parent; select = select.parent;
}
if (select && select.select_binding_dependencies) { if (select && select.select_binding_dependencies) {
select.select_binding_dependencies.forEach(prop => { select.select_binding_dependencies.forEach(prop => {

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

@ -745,9 +745,7 @@ export default class ElementWrapper extends Wrapper {
} }
block.chunks.destroy.push(b`if (detaching && ${name}) ${name}.end();`); block.chunks.destroy.push(b`if (detaching && ${name}) ${name}.end();`);
} } else {
else {
const intro_name = intro && block.get_unique_name(`${this.var.name}_intro`); const intro_name = intro && block.get_unique_name(`${this.var.name}_intro`);
const outro_name = outro && block.get_unique_name(`${this.var.name}_outro`); const outro_name = outro && block.get_unique_name(`${this.var.name}_outro`);
@ -920,22 +918,16 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapp
.replace(/\\/g, '\\\\') .replace(/\\/g, '\\\\')
.replace(/`/g, '\\`') .replace(/`/g, '\\`')
.replace(/\$/g, '\\$'); .replace(/\$/g, '\\$');
} } else if (wrapper instanceof MustacheTagWrapper || wrapper instanceof RawMustacheTagWrapper) {
else if (wrapper instanceof MustacheTagWrapper || wrapper instanceof RawMustacheTagWrapper) {
literal.quasis.push(state.quasi); literal.quasis.push(state.quasi);
literal.expressions.push(wrapper.node.expression.manipulate(block)); literal.expressions.push(wrapper.node.expression.manipulate(block));
state.quasi = { state.quasi = {
type: 'TemplateElement', type: 'TemplateElement',
value: { raw: '' } value: { raw: '' }
}; };
} } else if (wrapper.node.name === 'noscript') {
else if (wrapper.node.name === 'noscript') {
// do nothing // do nothing
} } else {
else {
// element // element
state.quasi.value.raw += `<${wrapper.node.name}`; state.quasi.value.raw += `<${wrapper.node.name}`;

@ -447,6 +447,8 @@ export default class IfBlockWrapper extends Wrapper {
if (!${name}) { if (!${name}) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#ctx); ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#ctx);
${name}.c(); ${name}.c();
} else {
${name}.p(#ctx, #dirty);
} }
${has_transitions && b`@transition_in(${name}, 1);`} ${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor}); ${name}.m(${update_mount_node}, ${anchor});

@ -44,7 +44,7 @@ export default class KeyBlockWrapper extends Wrapper {
renderer, renderer,
this.block, this.block,
node.children, node.children,
parent, this,
strip_whitespace, strip_whitespace,
next_sibling next_sibling
); );

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

@ -110,6 +110,7 @@ export interface CompileOptions {
filename?: string; filename?: string;
generate?: 'dom' | 'ssr' | false; generate?: 'dom' | 'ssr' | false;
sourcemap?: object | string;
outputFilename?: string; outputFilename?: string;
cssOutputFilename?: string; cssOutputFilename?: string;
sveltePath?: string; sveltePath?: string;

@ -178,11 +178,12 @@ export class Parser {
} }
read_until(pattern: RegExp) { read_until(pattern: RegExp) {
if (this.index >= this.template.length) if (this.index >= this.template.length) {
this.error({ this.error({
code: 'unexpected-eof', code: 'unexpected-eof',
message: 'Unexpected end of input' message: 'Unexpected end of input'
}); });
}
const start = this.index; const start = this.index;
const match = pattern.exec(this.template.slice(start)); const match = pattern.exec(this.template.slice(start));

@ -32,10 +32,12 @@ export default function read_script(parser: Parser, start: number, attributes: N
const script_start = parser.index; const script_start = parser.index;
const script_end = parser.template.indexOf(script_closing_tag, script_start); const script_end = parser.template.indexOf(script_closing_tag, script_start);
if (script_end === -1) parser.error({ if (script_end === -1) {
code: 'unclosed-script', parser.error({
message: '<script> must have a closing tag' code: 'unclosed-script',
}); message: '<script> must have a closing tag'
});
}
const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') + const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') +
parser.template.slice(script_start, script_end); parser.template.slice(script_start, script_end);

@ -142,10 +142,8 @@ export default function mustache(parser: Parser) {
}; };
parser.stack.push(block.else.children[0]); parser.stack.push(block.else.children[0]);
} } else {
// :else
// :else
else {
const block = parser.current(); const block = parser.current();
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') { if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
parser.error({ parser.error({
@ -288,10 +286,12 @@ export default function mustache(parser: Parser) {
if (parser.eat(',')) { if (parser.eat(',')) {
parser.allow_whitespace(); parser.allow_whitespace();
block.index = parser.read_identifier(); block.index = parser.read_identifier();
if (!block.index) parser.error({ if (!block.index) {
code: 'expected-name', parser.error({
message: 'Expected name' code: 'expected-name',
}); message: 'Expected name'
});
}
parser.allow_whitespace(); parser.allow_whitespace();
} }

@ -1,6 +1,11 @@
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
import { decode as decode_mappings } from 'sourcemap-codec';
import { getLocator } from 'locate-character';
import { StringWithSourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/string_with_sourcemap';
export interface Processed { export interface Processed {
code: string; code: string;
map?: object | string; map?: string | object; // we are opaque with the type here to avoid dependency on the remapping module for our public types.
dependencies?: string[]; dependencies?: string[];
} }
@ -37,12 +42,18 @@ function parse_attributes(str: string) {
interface Replacement { interface Replacement {
offset: number; offset: number;
length: number; length: number;
replacement: string; replacement: StringWithSourcemap;
} }
async function replace_async(str: string, re: RegExp, func: (...any) => Promise<string>) { async function replace_async(
filename: string,
source: string,
get_location: ReturnType<typeof getLocator>,
re: RegExp,
func: (...any) => Promise<StringWithSourcemap>
): Promise<StringWithSourcemap> {
const replacements: Array<Promise<Replacement>> = []; const replacements: Array<Promise<Replacement>> = [];
str.replace(re, (...args) => { source.replace(re, (...args) => {
replacements.push( replacements.push(
func(...args).then( func(...args).then(
res => res =>
@ -55,16 +66,55 @@ async function replace_async(str: string, re: RegExp, func: (...any) => Promise<
); );
return ''; return '';
}); });
let out = ''; const out = new StringWithSourcemap();
let last_end = 0; let last_end = 0;
for (const { offset, length, replacement } of await Promise.all( for (const { offset, length, replacement } of await Promise.all(
replacements replacements
)) { )) {
out += str.slice(last_end, offset) + replacement; // content = unchanged source characters before the replaced segment
const content = StringWithSourcemap.from_source(
filename, source.slice(last_end, offset), get_location(last_end));
out.concat(content).concat(replacement);
last_end = offset + length; last_end = offset + length;
} }
out += str.slice(last_end); // final_content = unchanged source characters after last replaced segment
return out; const final_content = StringWithSourcemap.from_source(
filename, source.slice(last_end), get_location(last_end));
return out.concat(final_content);
}
/**
* Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap
*/
function get_replacement(
filename: string,
offset: number,
get_location: ReturnType<typeof getLocator>,
original: string,
processed: Processed,
prefix: string,
suffix: string
): StringWithSourcemap {
// Convert the unchanged prefix and suffix to StringWithSourcemap
const prefix_with_map = StringWithSourcemap.from_source(
filename, prefix, get_location(offset));
const suffix_with_map = StringWithSourcemap.from_source(
filename, suffix, get_location(offset + prefix.length + original.length));
// Convert the preprocessed code and its sourcemap to a StringWithSourcemap
let decoded_map: DecodedSourceMap;
if (processed.map) {
decoded_map = typeof processed.map === 'string' ? JSON.parse(processed.map) : processed.map;
if (typeof(decoded_map.mappings) === 'string') {
decoded_map.mappings = decode_mappings(decoded_map.mappings);
}
sourcemap_add_offset(decoded_map, get_location(offset + prefix.length));
}
const processed_with_map = StringWithSourcemap.from_processed(processed.code, decoded_map);
// Surround the processed code with the prefix and suffix, retaining valid sourcemappings
return prefix_with_map.concat(processed_with_map).concat(suffix_with_map);
} }
export default async function preprocess( export default async function preprocess(
@ -76,60 +126,92 @@ export default async function preprocess(
const filename = (options && options.filename) || preprocessor.filename; // legacy const filename = (options && options.filename) || preprocessor.filename; // legacy
const dependencies = []; const dependencies = [];
const preprocessors = Array.isArray(preprocessor) ? preprocessor : [preprocessor]; const preprocessors = preprocessor
? Array.isArray(preprocessor) ? preprocessor : [preprocessor]
: [];
const markup = preprocessors.map(p => p.markup).filter(Boolean); const markup = preprocessors.map(p => p.markup).filter(Boolean);
const script = preprocessors.map(p => p.script).filter(Boolean); const script = preprocessors.map(p => p.script).filter(Boolean);
const style = preprocessors.map(p => p.style).filter(Boolean); const style = preprocessors.map(p => p.style).filter(Boolean);
// sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1)
// so we use sourcemap_list.unshift() to add new maps
// https://github.com/ampproject/remapping#multiple-transformations-of-a-file
const sourcemap_list: Array<DecodedSourceMap | RawSourceMap> = [];
// TODO keep track: what preprocessor generated what sourcemap? to make debugging easier = detect low-resolution sourcemaps in fn combine_mappings
for (const fn of markup) { for (const fn of markup) {
// run markup preprocessor
const processed = await fn({ const processed = await fn({
content: source, content: source,
filename filename
}); });
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
source = processed ? processed.code : source; if (!processed) continue;
if (processed.dependencies) dependencies.push(...processed.dependencies);
source = processed.code;
if (processed.map) {
sourcemap_list.unshift(
typeof(processed.map) === 'string'
? JSON.parse(processed.map)
: processed.map
);
}
} }
for (const fn of script) { async function preprocess_tag_content(tag_name: 'style' | 'script', preprocessor: Preprocessor) {
source = await replace_async( const get_location = getLocator(source);
const tag_regex = tag_name == 'style'
? /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi
: /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
const res = await replace_async(
filename,
source, source,
/<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi, get_location,
async (match, attributes = '', content = '') => { tag_regex,
async (match, attributes = '', content = '', offset) => {
const no_change = () => StringWithSourcemap.from_source(
filename, match, get_location(offset));
if (!attributes && !content) { if (!attributes && !content) {
return match; return no_change();
} }
attributes = attributes || ''; attributes = attributes || '';
const processed = await fn({ content = content || '';
// run script preprocessor
const processed = await preprocessor({
content, content,
attributes: parse_attributes(attributes), attributes: parse_attributes(attributes),
filename filename
}); });
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
return processed ? `<script${attributes}>${processed.code}</script>` : match; 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}>`);
} }
); );
source = res.string;
sourcemap_list.unshift(res.map);
}
for (const fn of script) {
await preprocess_tag_content('script', fn);
} }
for (const fn of style) { for (const fn of style) {
source = await replace_async( await preprocess_tag_content('style', fn);
source,
/<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi,
async (match, attributes = '', content = '') => {
if (!attributes && !content) {
return match;
}
const processed: Processed = await fn({
content,
attributes: parse_attributes(attributes),
filename
});
if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
return processed ? `<style${attributes}>${processed.code}</style>` : match;
}
);
} }
// Combine all the source maps for each preprocessor function into one
const map: RawSourceMap = combine_sourcemaps(
filename,
sourcemap_list
);
return { return {
// TODO return separated output, in future version where svelte.compile supports it: // TODO return separated output, in future version where svelte.compile supports it:
// style: { code: styleCode, map: styleMap }, // style: { code: styleCode, map: styleMap },
@ -138,7 +220,7 @@ export default async function preprocess(
code: source, code: source,
dependencies: [...new Set(dependencies)], dependencies: [...new Set(dependencies)],
map: (map as object),
toString() { toString() {
return source; return source;
} }

@ -13,8 +13,9 @@ const GRAM_SIZE_UPPER = 3;
// return an edit distance from 0 to 1 // return an edit distance from 0 to 1
function _distance(str1: string, str2: string) { function _distance(str1: string, str2: string) {
if (str1 === null && str2 === null) if (str1 === null && str2 === null) {
throw 'Trying to compare two null values'; throw 'Trying to compare two null values';
}
if (str1 === null || str2 === null) return 0; if (str1 === null || str2 === null) return 0;
str1 = String(str1); str1 = String(str1);
str2 = String(str2); str2 = String(str2);

@ -13,6 +13,7 @@ export const globals = new Set([
'decodeURI', 'decodeURI',
'decodeURIComponent', 'decodeURIComponent',
'document', 'document',
'Element',
'encodeURI', 'encodeURI',
'encodeURIComponent', 'encodeURIComponent',
'Error', 'Error',
@ -36,6 +37,7 @@ export const globals = new Set([
'NaN', 'NaN',
'navigator', 'navigator',
'Number', 'Number',
'Node',
'Object', 'Object',
'parseFloat', 'parseFloat',
'parseInt', 'parseInt',

@ -0,0 +1,276 @@
import { DecodedSourceMap, RawSourceMap, SourceMapLoader } from '@ampproject/remapping/dist/types/types';
import remapping from '@ampproject/remapping';
import { SourceMap } from 'magic-string';
type SourceLocation = {
line: number;
column: number;
};
function last_line_length(s: string) {
return s.length - s.lastIndexOf('\n') - 1;
}
// mutate map in-place
export function sourcemap_add_offset(
map: DecodedSourceMap, offset: SourceLocation
) {
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
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;
}
}
}
function merge_tables<T>(this_table: T[], other_table: T[]): [T[], number[], boolean, boolean] {
const new_table = this_table.slice();
const idx_map = [];
other_table = other_table || [];
let val_changed = false;
for (const [other_idx, other_val] of other_table.entries()) {
const this_idx = this_table.indexOf(other_val);
if (this_idx >= 0) {
idx_map[other_idx] = this_idx;
} else {
const new_idx = new_table.length;
new_table[new_idx] = other_val;
idx_map[other_idx] = new_idx;
val_changed = true;
}
}
let idx_changed = val_changed;
if (val_changed) {
if (idx_map.find((val, idx) => val != idx) === undefined) {
// idx_map is identity map [0, 1, 2, 3, 4, ....]
idx_changed = false;
}
}
return [new_table, idx_map, val_changed, idx_changed];
}
function pushArray<T>(_this: T[], other: T[]) {
// We use push to mutate in place for memory and perf reasons
// We use the for loop instead of _this.push(...other) to avoid the JS engine's function argument limit (65,535 in JavascriptCore)
for (let i = 0; i < other.length; i++) {
_this.push(other[i]);
}
}
export class StringWithSourcemap {
string: string;
map: DecodedSourceMap;
constructor(string = '', map: DecodedSourceMap = null) {
this.string = string;
if (map) {
this.map = map as DecodedSourceMap;
} else {
this.map = {
version: 3,
mappings: [],
sources: [],
names: []
};
}
}
/**
* concat in-place (mutable), return this (chainable)
* will also mutate the `other` object
*/
concat(other: StringWithSourcemap): StringWithSourcemap {
// noop: if one is empty, return the other
if (other.string == '') return this;
if (this.string == '') {
this.string = other.string;
this.map = other.map;
return this;
}
this.string += other.string;
const m1 = this.map;
const m2 = other.map;
if (m2.mappings.length == 0) return this;
// combine sources and names
const [sources, new_source_idx, sources_changed, sources_idx_changed] = merge_tables(m1.sources, m2.sources);
const [names, new_name_idx, names_changed, names_idx_changed] = merge_tables(m1.names, m2.names);
if (sources_changed) m1.sources = sources;
if (names_changed) m1.names = names;
// unswitched loops are faster
if (sources_idx_changed && names_idx_changed) {
for (let line = 0; line < m2.mappings.length; line++) {
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]];
}
}
} else if (sources_idx_changed) {
for (let line = 0; line < m2.mappings.length; line++) {
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]];
}
}
} else if (names_idx_changed) {
for (let line = 0; line < m2.mappings.length; line++) {
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]];
}
}
}
// combine the mappings
// combine
// 1. last line of first map
// 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++) {
first_line[i][0] += column_offset;
}
}
// combine last line + first line
pushArray(m1.mappings[m1.mappings.length - 1], m2.mappings.shift());
// append other lines
pushArray(m1.mappings, m2.mappings);
return this;
}
static from_processed(string: string, map?: DecodedSourceMap): StringWithSourcemap {
if (map) 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);
}
static from_source(
source_file: string, source: string, offset?: SourceLocation
): StringWithSourcemap {
if (!offset) offset = { line: 0, column: 0 };
const map: DecodedSourceMap = { version: 3, names: [], sources: [source_file], mappings: [] };
if (source == '') return new StringWithSourcemap(source, map);
// we create a high resolution identity map here,
// we know that it will eventually be merged with svelte's map,
// at which stage the resolution will decrease.
const line_list = source.split('\n');
for (let line = 0; line < line_list.length; line++) {
map.mappings.push([]);
const token_list = line_list[line].split(/([^\d\w\s]|\s+)/g);
for (let token = 0, column = 0; token < token_list.length; token++) {
if (token_list[token] == '') continue;
map.mappings[line].push([column, 0, offset.line + line, column]);
column += token_list[token].length;
}
}
// shift columns in first line
const segment_list = map.mappings[0];
for (let segment = 0; segment < segment_list.length; segment++) {
segment_list[segment][3] += offset.column;
}
return new StringWithSourcemap(source, map);
}
}
export function combine_sourcemaps(
filename: string,
sourcemap_list: Array<DecodedSourceMap | RawSourceMap>
): RawSourceMap {
if (sourcemap_list.length == 0) return null;
let map_idx = 1;
const map: RawSourceMap =
sourcemap_list.slice(0, -1)
.find(m => m.sources.length !== 1) === undefined
? remapping( // use array interface
// only the oldest sourcemap can have multiple sources
sourcemap_list,
() => null,
true // skip optional field `sourcesContent`
)
: remapping( // use loader interface
sourcemap_list[0], // last map
function loader(sourcefile) {
if (sourcefile === filename && sourcemap_list[map_idx]) {
return sourcemap_list[map_idx++]; // idx 1, 2, ...
// bundle file = branch node
} else {
return null; // source file = leaf node
}
} as SourceMapLoader,
true
);
if (!map.file) delete map.file; // skip optional field `file`
return map;
}
// browser vs node.js
const b64enc = typeof btoa == 'function' ? btoa : b => Buffer.from(b).toString('base64');
export function apply_preprocessor_sourcemap(filename: string, svelte_map: SourceMap, preprocessor_map_input: string | DecodedSourceMap | RawSourceMap): SourceMap {
if (!svelte_map || !preprocessor_map_input) return svelte_map;
const preprocessor_map = typeof preprocessor_map_input === 'string' ? JSON.parse(preprocessor_map_input) : preprocessor_map_input;
const result_map = combine_sourcemaps(
filename,
[
svelte_map as RawSourceMap,
preprocessor_map
]
) as RawSourceMap;
// Svelte expects a SourceMap which includes toUrl and toString. Instead of wrapping our output in a class,
// we just tack on the extra properties.
Object.defineProperties(result_map, {
toString: {
enumerable: false,
value: function toString() {
return JSON.stringify(this);
}
},
toUrl: {
enumerable: false,
value: function toUrl() {
return 'data:application/json;charset=utf-8;base64,' + b64enc(this.toString());
}
}
});
return result_map as SourceMap;
}

@ -360,3 +360,19 @@ export class HtmlTag {
this.n.forEach(detach); this.n.forEach(detach);
} }
} }
export function attribute_to_object(attributes: NamedNodeMap) {
const result = {};
for (const attribute of attributes) {
result[attribute.name] = attribute.value;
}
return result;
}
export function get_custom_elements_slots(element: HTMLElement) {
const result = {};
element.childNodes.forEach((node: Element) => {
result[node.slot || 'default'] = true;
});
return result;
}

@ -73,19 +73,13 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list
next = new_block.first; next = new_block.first;
o--; o--;
n--; n--;
} } else if (!new_lookup.has(old_key)) {
else if (!new_lookup.has(old_key)) {
// remove old block // remove old block
destroy(old_block, lookup); destroy(old_block, lookup);
o--; o--;
} } else if (!lookup.has(new_key) || will_move.has(new_key)) {
else if (!lookup.has(new_key) || will_move.has(new_key)) {
insert(new_block); insert(new_block);
} } else if (did_move.has(old_key)) {
else if (did_move.has(old_key)) {
o--; o--;
} else if (deltas.get(new_key) > deltas.get(old_key)) { } else if (deltas.get(new_key) > deltas.get(old_key)) {

@ -54,6 +54,10 @@ export function getContext<T>(key): T {
return get_current_component().$$.context.get(key); return get_current_component().$$.context.get(key);
} }
export function hasContext(key): boolean {
return get_current_component().$$.context.has(key);
}
// TODO figure out if we still want to support // TODO figure out if we still want to support
// shorthand events, or if we want to implement // shorthand events, or if we want to implement
// a real bubbling mechanism // a real bubbling mechanism

@ -318,9 +318,7 @@ export function create_bidirectional_transition(node: Element & ElementCSSInline
} }
running_program = null; running_program = null;
} } else if (now >= running_program.start) {
else if (now >= running_program.start) {
const p = now - running_program.start; const p = now - running_program.start;
t = running_program.a + running_program.d * easing(p / running_program.duration); t = running_program.a + running_program.d * easing(p / running_program.duration);
tick(t, 1 - t); tick(t, 1 - t);

@ -34,9 +34,10 @@ function tick_spring<T>(ctx: TickContext<T>, last_value: T, current_value: T, ta
tick_spring(ctx, last_value[i], current_value[i], target_value[i])); tick_spring(ctx, last_value[i], current_value[i], target_value[i]));
} else if (typeof current_value === 'object') { } else if (typeof current_value === 'object') {
const next_value = {}; const next_value = {};
for (const k in current_value) for (const k in current_value) {
// @ts-ignore // @ts-ignore
next_value[k] = tick_spring(ctx, last_value[k], current_value[k], target_value[k]); next_value[k] = tick_spring(ctx, last_value[k], current_value[k], target_value[k]);
}
// @ts-ignore // @ts-ignore
return next_value; return next_value;
} else { } else {
@ -121,8 +122,9 @@ export function spring<T=any>(value?: T, opts: SpringOpts = {}): Spring<T> {
last_value = value; last_value = value;
store.set(value = next_value); store.set(value = next_value);
if (ctx.settled) if (ctx.settled) {
task = null; task = null;
}
return !ctx.settled; return !ctx.settled;
}); });
} }

@ -31,3 +31,24 @@ export function equal(a, b, message) {
export function ok(condition, message) { export function ok(condition, message) {
if (!condition) throw new Error(message || `Expected ${condition} to be truthy`); if (!condition) throw new Error(message || `Expected ${condition} to be truthy`);
} }
export function htmlEqual(actual, expected, message) {
return deepEqual(
normalizeHtml(window, actual),
normalizeHtml(window, expected),
message
);
}
function normalizeHtml(window, html) {
try {
const node = window.document.createElement('div');
node.innerHTML = html
.replace(/<!--.*?-->/g, '')
.replace(/>[\s\r\n]+</g, '><')
.trim();
return node.innerHTML.replace(/<\/?noscript\/?>/g, '');
} catch (err) {
throw new Error(`Failed to normalize HTML:\n${html}`);
}
}

@ -0,0 +1,10 @@
<svelte:options tag="custom-element"/>
<script>
export let name;
</script>
<p>name: {name}</p>
<p>$$props: {JSON.stringify($$props)}</p>
<p>$$restProps: {JSON.stringify($$restProps)}</p>

@ -0,0 +1,13 @@
import * as assert from 'assert';
import './main.svelte';
export default function (target) {
target.innerHTML = '<custom-element name="world" answer="42" test="svelte"></custom-element>';
const el = target.querySelector('custom-element');
assert.htmlEqual(el.shadowRoot.innerHTML, `
<p>name: world</p>
<p>$$props: {"name":"world","answer":"42","test":"svelte"}</p>
<p>$$restProps: {"answer":"42","test":"svelte"}</p>
`);
}

@ -0,0 +1,31 @@
<script>
let data = '';
if ($$slots.b) {
data = 'foo';
}
export function getData() {
return data;
}
function toString(data) {
const result = {};
const sortedKeys = Object.keys(data).sort();
sortedKeys.forEach(key => result[key] = data[key]);
return JSON.stringify(result);
}
</script>
<svelte:options tag="custom-element"/>
<slot></slot>
<slot name="a"></slot>
<p>$$slots: {toString($$slots)}</p>
{#if $$slots.b}
<div>
<slot name="b"></slot>
</div>
{:else}
<p>Slot b is not available</p>
{/if}

@ -0,0 +1,28 @@
import * as assert from 'assert';
import './main.svelte';
export default function (target) {
target.innerHTML = `
<custom-element><span slot="a">hello world</span><span>bye</span><span>world</span></custom-element>
<custom-element><span slot="a">hello world</span><span slot="b">hello world</span><span>bye world</span></custom-element>
`;
const [a, b] = target.querySelectorAll('custom-element');
assert.htmlEqual(a.shadowRoot.innerHTML, `
<slot></slot>
<slot name="a"></slot>
<p>$$slots: {"a":true,"default":true}</p>
<p>Slot b is not available</p>
`);
assert.htmlEqual(b.shadowRoot.innerHTML, `
<slot></slot>
<slot name="a"></slot>
<p>$$slots: {"a":true,"b":true,"default":true}</p>
<div><slot name="b"></slot></div>
`);
assert.equal(a.getData(), '');
assert.equal(b.getData(), 'foo');
}

@ -118,9 +118,11 @@ describe('hydration', () => {
throw err; throw err;
} }
if (config.show) showOutput(cwd, { if (config.show) {
hydratable: true showOutput(cwd, {
}); hydratable: true
});
}
}); });
} }

@ -1,6 +1,7 @@
/* generated by Svelte vX.Y.Z */ /* generated by Svelte vX.Y.Z */
import { import {
SvelteElement, SvelteElement,
attribute_to_object,
detach, detach,
element, element,
init, init,
@ -34,7 +35,18 @@ class Component extends SvelteElement {
constructor(options) { constructor(options) {
super(); super();
this.shadowRoot.innerHTML = `<style>div{animation:foo 1s}@keyframes foo{0%{opacity:0}100%{opacity:1}}</style>`; this.shadowRoot.innerHTML = `<style>div{animation:foo 1s}@keyframes foo{0%{opacity:0}100%{opacity:1}}</style>`;
init(this, { target: this.shadowRoot }, null, create_fragment, safe_not_equal, {});
init(
this,
{
target: this.shadowRoot,
props: attribute_to_object(this.attributes)
},
null,
create_fragment,
safe_not_equal,
{}
);
if (options) { if (options) {
if (options.target) { if (options.target) {

@ -8,17 +8,25 @@ describe('preprocess', () => {
const config = loadConfig(`${__dirname}/samples/${dir}/_config.js`); const config = loadConfig(`${__dirname}/samples/${dir}/_config.js`);
const solo = config.solo || /\.solo/.test(dir); const solo = config.solo || /\.solo/.test(dir);
const skip = config.skip || /\.skip/.test(dir);
if (solo && process.env.CI) { if (solo && process.env.CI) {
throw new Error('Forgot to remove `solo: true` from test'); throw new Error('Forgot to remove `solo: true` from test');
} }
(config.skip ? it.skip : solo ? it.only : it)(dir, async () => { (skip ? it.skip : solo ? it.only : it)(dir, async () => {
const input = fs.readFileSync(`${__dirname}/samples/${dir}/input.svelte`, 'utf-8'); const input = fs.readFileSync(`${__dirname}/samples/${dir}/input.svelte`, 'utf-8');
const expected = fs.readFileSync(`${__dirname}/samples/${dir}/output.svelte`, 'utf-8'); const expected = fs.readFileSync(`${__dirname}/samples/${dir}/output.svelte`, 'utf-8');
const result = await svelte.preprocess(input, config.preprocess); const result = await svelte.preprocess(
input,
config.preprocess || {},
config.options || { filename: 'input.svelte' }
);
fs.writeFileSync(`${__dirname}/samples/${dir}/_actual.html`, result.code); fs.writeFileSync(`${__dirname}/samples/${dir}/_actual.html`, result.code);
if (result.map) {
fs.writeFileSync(`${__dirname}/samples/${dir}/_actual.html.map`, JSON.stringify(result.map, null, 2));
}
assert.equal(result.code, expected); assert.equal(result.code, expected);

@ -1,6 +1,5 @@
export default { export default {
preprocess: { preprocess: {
filename: 'file.svelte',
markup: ({ content, filename }) => { markup: ({ content, filename }) => {
return { return {
code: content.replace('__MARKUP_FILENAME__', filename) code: content.replace('__MARKUP_FILENAME__', filename)
@ -16,5 +15,8 @@ export default {
code: content.replace('__SCRIPT_FILENAME__', filename) code: content.replace('__SCRIPT_FILENAME__', filename)
}; };
} }
},
options: {
filename: 'file.svelte'
} }
}; };

@ -0,0 +1,14 @@
<script>
let keys = ['a', 'b'];
let items = ['c', 'd'];
export let log;
function setKey(key, value, item) {
log.push(`setKey(${key}, ${value}, ${item})`);
}
</script>
{#each items as item (item)}
{#each keys as key (key)}
<slot {key} {item} set={(value) => setKey(key, value, item)} />
{/each}
{/each}

@ -0,0 +1,36 @@
export default {
html: `
<button type="button">Set a-c</button>
<button type="button">Set b-c</button>
<button type="button">Set a-d</button>
<button type="button">Set b-d</button>
`,
async test({ assert, target, window, component }) {
const [btn1, btn2, btn3, btn4] = target.querySelectorAll('button');
const click = new window.MouseEvent('click');
await btn1.dispatchEvent(click);
assert.deepEqual(component.log, ['setKey(a, value-a-c, c)']);
await btn2.dispatchEvent(click);
assert.deepEqual(component.log, [
'setKey(a, value-a-c, c)',
'setKey(b, value-b-c, c)'
]);
await btn3.dispatchEvent(click);
assert.deepEqual(component.log, [
'setKey(a, value-a-c, c)',
'setKey(b, value-b-c, c)',
'setKey(a, value-a-d, d)'
]);
await btn4.dispatchEvent(click);
assert.deepEqual(component.log, [
'setKey(a, value-a-c, c)',
'setKey(b, value-b-c, c)',
'setKey(a, value-a-d, d)',
'setKey(b, value-b-d, d)'
]);
}
};

@ -0,0 +1,8 @@
<script>
import Nested from './Nested.svelte';
export let log = [];
</script>
<Nested {log} let:set let:key let:item>
<button type="button" on:click={() => set(`value-${key}-${item}`)}>Set {key}-{item}</button>
</Nested>

@ -0,0 +1,11 @@
<script>
let keys = ['a', 'b'];
export let log;
function setKey(key, value) {
log.push(`setKey(${key}, ${value})`);
}
</script>
{#each keys as key (key)}
<slot {key} set={(value) => setKey(key, value)} />
{/each}

@ -0,0 +1,19 @@
export default {
html: `
<button type="button">Set a</button>
<button type="button">Set b</button>
`,
async test({ assert, target, window, component }) {
const [btn1, btn2] = target.querySelectorAll('button');
const click = new window.MouseEvent('click');
await btn1.dispatchEvent(click);
assert.deepEqual(component.log, ['setKey(a, value-a)']);
await btn2.dispatchEvent(click);
assert.deepEqual(component.log, [
'setKey(a, value-a)',
'setKey(b, value-b)'
]);
}
};

@ -0,0 +1,8 @@
<script>
import Nested from './Nested.svelte';
export let log = [];
</script>
<Nested {log} let:set let:key>
<button type="button" on:click={() => set(`value-${key}`)}>Set {key}</button>
</Nested>

@ -0,0 +1,9 @@
<script>
export let log;
function setKey(key, value) {
log.push(`setKey(${key}, ${value})`);
}
</script>
<slot key="a" set={setKey} />
<slot key="b" set={setKey} />

@ -0,0 +1,8 @@
<script>
import Inner from './Inner.svelte';
export let log;
</script>
<Inner {log} let:key let:set>
<slot {key} set={(value) => set(key, value)} />
</Inner>

@ -0,0 +1,19 @@
export default {
html: `
<button type="button">Set a</button>
<button type="button">Set b</button>
`,
async test({ assert, target, window, component }) {
const [btn1, btn2] = target.querySelectorAll('button');
const click = new window.MouseEvent('click');
await btn1.dispatchEvent(click);
assert.deepEqual(component.log, ['setKey(a, value-a)']);
await btn2.dispatchEvent(click);
assert.deepEqual(component.log, [
'setKey(a, value-a)',
'setKey(b, value-b)'
]);
}
};

@ -0,0 +1,8 @@
<script>
import Nested from './Nested.svelte';
export let log = [];
</script>
<Nested {log} let:set let:key>
<button type="button" on:click={() => set(`value-${key}`)}>Set {key}</button>
</Nested>

@ -0,0 +1,21 @@
export default {
html: `
<section>
<div>Second</div>
</section>
<button>Click</button>
`,
async test({ assert, component, target, window }) {
const button = target.querySelector('button');
await button.dispatchEvent(new window.Event('click'));
assert.htmlEqual(target.innerHTML, `
<section>
<div>First</div>
<div>Second</div>
</section>
<button>Click</button>
`);
}
};

@ -0,0 +1,17 @@
<script>
let slide = 0;
let num = false;
const changeNum = () => num = !num;
</script>
<section>
{#key slide}
{#if num}
<div>First</div>
{/if}
{/key}
<div>Second</div>
</section>
<button on:click={changeNum}>Click</button>

@ -0,0 +1,31 @@
// expect aborting halfway through outro transition
// to behave the same in `{#if}` block as in `{:else}` block
export default {
html: `
<div>a</div>
<div>a</div>
`,
async test({ assert, component, target, window, raf }) {
component.visible = false;
// abort halfway through the outro transition
raf.tick(50);
await component.$set({
visible: true,
array: ['a', 'b', 'c']
});
assert.htmlEqual(target.innerHTML, `
<div>a</div>
<div>b</div>
<div>c</div>
<div>a</div>
<div>b</div>
<div>c</div>
`);
}
};

@ -0,0 +1,21 @@
<script>
export let array = ['a'];
export let visible = true;
function slide(_, params) {
return params;
}
</script>
{#if visible}
{#each array as item}
<div transition:slide={{duration:100}}>{item}</div>
{/each}
{/if}
{#if !visible}
{:else}
{#each array as item}
<div transition:slide={{duration:100}}>{item}</div>
{/each}
{/if}

@ -12,7 +12,7 @@ require.extensions['.js'] = function(module, filename) {
.replace(/^import (\w+) from ['"]([^'"]+)['"];?/gm, 'var {default: $1} = require("$2");') .replace(/^import (\w+) from ['"]([^'"]+)['"];?/gm, 'var {default: $1} = require("$2");')
.replace(/^import {([^}]+)} from ['"](.+)['"];?/gm, 'var {$1} = require("$2");') .replace(/^import {([^}]+)} from ['"](.+)['"];?/gm, 'var {$1} = require("$2");')
.replace(/^export default /gm, 'exports.default = ') .replace(/^export default /gm, 'exports.default = ')
.replace(/^export (const|let|var|class|function) (\w+)/gm, (match, type, name) => { .replace(/^export (const|let|var|class|function|async\s+function) (\w+)/gm, (match, type, name) => {
exports.push(name); exports.push(name);
return `${type} ${name}`; return `${type} ${name}`;
}) })

@ -0,0 +1,20 @@
import MagicString from 'magic-string';
export function magic_string_preprocessor_result(filename: string, src: MagicString) {
return {
code: src.toString(),
map: src.generateMap({
source: filename,
hires: true,
includeContent: false
})
};
}
export function magic_string_replace_all(src: MagicString, search: string, replace: string) {
let idx = src.original.indexOf(search);
if (idx == -1) throw new Error('search not found in src');
do {
src.overwrite(idx, idx + search.length, replace, { storeName: true });
} while ((idx = src.original.indexOf(search, idx + 1)) != -1);
}

@ -1,73 +1,115 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as assert from 'assert'; import * as assert from 'assert';
import { svelte } from '../helpers'; import { loadConfig, svelte } from '../helpers';
import { SourceMapConsumer } from 'source-map'; // keep source-map at version 0.7.x
// https://github.com/mozilla/source-map/issues/400
import { getLocator } from 'locate-character'; import { getLocator } from 'locate-character';
import { SourceMapConsumer } from 'source-map';
describe('sourcemaps', () => { describe('sourcemaps', () => {
fs.readdirSync(`${__dirname}/samples`).forEach(dir => { fs.readdirSync(`${__dirname}/samples`).forEach(dir => {
if (dir[0] === '.') return; if (dir[0] === '.') return;
const config = loadConfig(`${__dirname}/samples/${dir}/_config.js`);
// add .solo to a sample directory name to only run that test // add .solo to a sample directory name to only run that test
const solo = /\.solo/.test(dir); const solo = config.solo || /\.solo/.test(dir);
const skip = /\.skip/.test(dir); const skip = config.skip || /\.skip/.test(dir);
if (solo && process.env.CI) { if (solo && process.env.CI) {
throw new Error('Forgot to remove `solo: true` from test'); throw new Error('Forgot to remove `solo: true` from test');
} }
(solo ? it.only : skip ? it.skip : it)(dir, async () => { (solo ? it.only : skip ? it.skip : it)(dir, async () => {
const filename = path.resolve( const { test } = require(`./samples/${dir}/test.js`);
`${__dirname}/samples/${dir}/input.svelte` const inputFile = path.resolve(`${__dirname}/samples/${dir}/input.svelte`);
); const outputName = '_actual';
const outputFilename = path.resolve( const outputBase = path.resolve(`${__dirname}/samples/${dir}/${outputName}`);
`${__dirname}/samples/${dir}/output`
); const inputCode = fs.readFileSync(inputFile, 'utf-8');
const input = {
code: inputCode,
locate: getLocator(inputCode)
};
const input = fs.readFileSync(filename, 'utf-8').replace(/\s+$/, ''); const preprocessed = await svelte.preprocess(
const { js, css } = svelte.compile(input, { input.code,
filename, config.preprocess || {},
outputFilename: `${outputFilename}.js`, config.options || {
cssOutputFilename: `${outputFilename}.css` filename: 'input.svelte'
}
);
const { js, css } = svelte.compile(
preprocessed.code, {
filename: 'input.svelte',
// filenames for sourcemaps
sourcemap: preprocessed.map,
outputFilename: `${outputName}.js`,
cssOutputFilename: `${outputName}.css`,
...(config.compile_options || {})
}); });
const _code = js.code.replace(/Svelte v\d+\.\d+\.\d+/, match => match.replace(/\d/g, 'x')); js.code = js.code.replace(
/generated by Svelte v\d+\.\d+\.\d+/,
match => match.replace(/\d/g, 'x')
);
fs.writeFileSync(`${outputBase}.svelte`, preprocessed.code);
if (preprocessed.map) {
fs.writeFileSync(
`${outputBase}.svelte.map`,
// TODO encode mappings for output - svelte.preprocess returns decoded mappings
JSON.stringify(preprocessed.map, null, 2)
);
}
fs.writeFileSync( fs.writeFileSync(
`${outputFilename}.js`, `${outputBase}.js`,
`${_code}\n//# sourceMappingURL=output.js.map` `${js.code}\n//# sourceMappingURL=${outputName}.js.map`
); );
fs.writeFileSync( fs.writeFileSync(
`${outputFilename}.js.map`, `${outputBase}.js.map`,
JSON.stringify(js.map, null, ' ') JSON.stringify(js.map, null, 2)
); );
if (css.code) { if (css.code) {
fs.writeFileSync( fs.writeFileSync(
`${outputFilename}.css`, `${outputBase}.css`,
`${css.code}\n/*# sourceMappingURL=output.css.map */` `${css.code}\n/*# sourceMappingURL=${outputName}.css.map */`
); );
fs.writeFileSync( fs.writeFileSync(
`${outputFilename}.css.map`, `${outputBase}.css.map`,
JSON.stringify(css.map, null, ' ') JSON.stringify(css.map, null, ' ')
); );
} }
assert.deepEqual(js.map.sources, ['input.svelte']); assert.deepEqual(
if (css.map) assert.deepEqual(css.map.sources, ['input.svelte']); js.map.sources.slice().sort(),
(config.js_map_sources || ['input.svelte']).sort()
const { test } = require(`./samples/${dir}/test.js`); );
if (css.map) {
assert.deepEqual(
css.map.sources.slice().sort(),
(config.css_map_sources || ['input.svelte']).sort()
);
}
const locateInSource = getLocator(input); // use locate_1 with mapConsumer:
// lines are one-based, columns are zero-based
const smc = await new SourceMapConsumer(js.map); preprocessed.mapConsumer = preprocessed.map && await new SourceMapConsumer(preprocessed.map);
const locateInGenerated = getLocator(_code); preprocessed.locate = getLocator(preprocessed.code);
preprocessed.locate_1 = getLocator(preprocessed.code, { offsetLine: 1 });
const smcCss = css.map && await new SourceMapConsumer(css.map); js.mapConsumer = js.map && await new SourceMapConsumer(js.map);
const locateInGeneratedCss = getLocator(css.code || ''); js.locate = getLocator(js.code);
js.locate_1 = getLocator(js.code, { offsetLine: 1 });
test({ assert, code: _code, map: js.map, smc, smcCss, locateInSource, locateInGenerated, locateInGeneratedCss }); css.mapConsumer = css.map && await new SourceMapConsumer(css.map);
css.locate = getLocator(css.code || '');
css.locate_1 = getLocator(css.code || '', { offsetLine: 1 });
await test({ assert, input, preprocessed, js, css });
}); });
}); });
}); });

@ -1,12 +1,12 @@
export function test({ assert, smc, locateInSource, locateInGenerated }) { export function test({ assert, input, js }) {
const expected = locateInSource('foo.bar.baz'); const expected = input.locate('foo.bar.baz');
let start; let start;
let actual; let actual;
start = locateInGenerated('ctx[0].bar.baz'); start = js.locate('ctx[0].bar.baz');
actual = smc.originalPositionFor({ actual = js.mapConsumer.originalPositionFor({
line: start.line + 1, line: start.line + 1,
column: start.column column: start.column
}); });
@ -18,9 +18,9 @@ export function test({ assert, smc, locateInSource, locateInGenerated }) {
column: expected.column column: expected.column
}); });
start = locateInGenerated('ctx[0].bar.baz', start.character + 1); start = js.locate('ctx[0].bar.baz', start.character + 1);
actual = smc.originalPositionFor({ actual = js.mapConsumer.originalPositionFor({
line: start.line + 1, line: start.line + 1,
column: start.column column: start.column
}); });

@ -1,13 +1,14 @@
export function test({ assert, smc, locateInSource, locateInGenerated }) { export function test({ assert, input, js }) {
const expected = locateInSource('potato'); const expected = input.locate('potato');
let start; let start;
start = locateInGenerated('potato'); start = js.locate('potato');
start = locateInGenerated('potato', start.character + 1); start = js.locate('potato', start.character + 1);
start = locateInGenerated('potato', start.character + 1); // we need the third instance of 'potato' start = js.locate('potato', start.character + 1);
// we need the third instance of 'potato'
const actual = smc.originalPositionFor({ const actual = js.mapConsumer.originalPositionFor({
line: start.line + 1, line: start.line + 1,
column: start.column column: start.column
}); });

@ -1,12 +1,12 @@
export function test({ assert, smc, locateInSource, locateInGenerated }) { export function test({ assert, input, js }) {
const expected = locateInSource('bar.baz'); const expected = input.locate('bar.baz');
let start; let start;
let actual; let actual;
start = locateInGenerated('bar.baz'); start = js.locate('bar.baz');
actual = smc.originalPositionFor({ actual = js.mapConsumer.originalPositionFor({
line: start.line + 1, line: start.line + 1,
column: start.column column: start.column
}); });
@ -18,9 +18,9 @@ export function test({ assert, smc, locateInSource, locateInGenerated }) {
column: expected.column column: expected.column
}); });
start = locateInGenerated('bar.baz', start.character + 1); start = js.locate('bar.baz', start.character + 1);
actual = smc.originalPositionFor({ actual = js.mapConsumer.originalPositionFor({
line: start.line + 1, line: start.line + 1,
column: start.column column: start.column
}); });

@ -0,0 +1,21 @@
import MagicString from 'magic-string';
import { magic_string_preprocessor_result, magic_string_replace_all } from '../../helpers';
export default {
compile_options: {
dev: true
},
preprocess: [
{ style: ({ content, filename }) => {
const src = new MagicString(content);
magic_string_replace_all(src, '--replace-me-once', '\n --done-replace-once');
magic_string_replace_all(src, '--replace-me-twice', '\n--almost-done-replace-twice');
return magic_string_preprocessor_result(filename, src);
} },
{ style: ({ content, filename }) => {
const src = new MagicString(content);
magic_string_replace_all(src, '--almost-done-replace-twice', '\n --done-replace-twice');
return magic_string_preprocessor_result(filename, src);
} }
]
};

@ -0,0 +1,15 @@
<h1>Testing Styles</h1>
<h2>Testing Styles 2</h2>
<div>Testing Styles 3</div>
<script>export const b = 2;</script>
<style>
h1 {
--replace-me-once: red;
}
h2 {
--replace-me-twice: green;
}
div {
--keep-me: blue;
}
</style>

@ -0,0 +1,40 @@
import { SourceMapConsumer } from 'source-map';
const b64dec = s => Buffer.from(s, 'base64').toString();
export async function test({ assert, css, js }) {
// We check that the css source map embedded in the js is accurate
const match = js.code.match(/\tstyle\.textContent = "(.*?)(?:\\n\/\*# sourceMappingURL=data:(.*?);charset=(.*?);base64,(.*?) \*\/)?";\n/);
assert.notEqual(match, null);
const [mimeType, encoding, cssMapBase64] = match.slice(2);
assert.equal(mimeType, 'application/json');
assert.equal(encoding, 'utf-8');
const cssMapJson = b64dec(cssMapBase64);
css.mapConsumer = await new SourceMapConsumer(cssMapJson);
// TODO make util fn + move to test index.js
const sourcefile = 'input.svelte';
[
// TODO how to get line + column numbers?
[css, '--keep-me', 13, 2],
[css, '--done-replace-once', 6, 5],
[css, '--done-replace-twice', 9, 5]
]
.forEach(([where, content, line, column]) => {
assert.deepEqual(
where.mapConsumer.originalPositionFor(
where.locate_1(content)
),
{
source: sourcefile,
name: null,
line,
column
},
`failed to locate "${content}" from "${sourcefile}"`
);
});
}

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

Loading…
Cancel
Save