Merge branch 'master' into pr/5890

pull/5890/head
Conduitry 6 years ago
commit 0a1967beb7

3
.gitattributes vendored

@ -0,0 +1,3 @@
/site/** -linguist-detectable
/test/**/samples/** -linguist-detectable
/**/*.svelte linguist-detectable

2
.gitignore vendored

@ -34,4 +34,4 @@ _output
/site/scripts/svelte-app /site/scripts/svelte-app
/site/scripts/community /site/scripts/community
/site/src/routes/_contributors.js /site/src/routes/_contributors.js
/site/src/routes/_components/WhosUsingSvelte.svelte /site/src/routes/_components/WhosUsingSvelte.*

@ -1,5 +1,27 @@
# Svelte changelog # Svelte changelog
## Unreleased
* Fix scoping of selectors with `:global()` and `~` sibling combinators ([#5499](https://github.com/sveltejs/svelte/issues/5499))
* Fix removal of `:host` selectors as unused when compiling to a custom element ([#5946](https://github.com/sveltejs/svelte/issues/5946))
## 3.32.1
* Warn when using `module` variables reactively, and close weird reactivity loophole ([#5847](https://github.com/sveltejs/svelte/pull/5847))
* Throw a parser error for `class:` directives with an empty class name ([#5858](https://github.com/sveltejs/svelte/issues/5858))
* Fix extraneous store subscription in SSR mode ([#5883](https://github.com/sveltejs/svelte/issues/5883))
* Don't emit update code for `class:` directives whose expression is not dynamic ([#5919](https://github.com/sveltejs/svelte/issues/5919))
* Fix type inference for derived stores ([#5935](https://github.com/sveltejs/svelte/pull/5935))
* Make parameters of built-in animations and transitions optional ([#5936](https://github.com/sveltejs/svelte/pull/5936))
* Make `SvelteComponentDev` typings more forgiving ([#5937](https://github.com/sveltejs/svelte/pull/5937))
* Fix `foreign` elements incorrectly disallowing `bind:this` ([#5942](https://github.com/sveltejs/svelte/pull/5942))
## 3.32.0
* Allow multiple instances of the same action on an element ([#5516](https://github.com/sveltejs/svelte/issues/5516))
* Support `foreign` namespace, which disables certain HTML5-specific behaviour and checks ([#5652](https://github.com/sveltejs/svelte/pull/5652))
* Support inline comment sourcemaps in code from preprocessors ([#5854](https://github.com/sveltejs/svelte/pull/5854))
## 3.31.2 ## 3.31.2
* Rework SSR store handling to subscribe and unsubscribe as in DOM mode ([#3375](https://github.com/sveltejs/svelte/issues/3375), [#3582](https://github.com/sveltejs/svelte/issues/3582), [#3636](https://github.com/sveltejs/svelte/issues/3636)) * Rework SSR store handling to subscribe and unsubscribe as in DOM mode ([#3375](https://github.com/sveltejs/svelte/issues/3375), [#3582](https://github.com/sveltejs/svelte/issues/3582), [#3636](https://github.com/sveltejs/svelte/issues/3636))

@ -23,7 +23,7 @@ Learn more at the [Svelte website](https://svelte.dev), or stop by the [Discord
## Supporting Svelte ## Supporting Svelte
Svelte is an MIT-licensed open source project with its ongoing development made possible entirely by the support of awesome volunteers. If you'd like to support their efforts, please consider: Svelte is an MIT-licensed open source project with its ongoing development made possible entirely by fantastic volunteers. If you'd like to support their efforts, please consider:
- [Becoming a backer on Open Collective](https://opencollective.com/svelte). - [Becoming a backer on Open Collective](https://opencollective.com/svelte).
@ -44,7 +44,7 @@ npm install
> Do not use Yarn to install the dependencies, as the specific package versions in `package-lock.json` are used to build and test Svelte. > Do not use Yarn to install the dependencies, as the specific package versions in `package-lock.json` are used to build and test Svelte.
To build the compiler, and all the other modules included in the package: To build the compiler and all the other modules included in the package:
```bash ```bash
npm run build npm run build
@ -56,7 +56,7 @@ To watch for changes and continually rebuild the package (this is useful if you'
npm run dev npm run dev
``` ```
The compiler is written in [TypeScript](https://www.typescriptlang.org/), but don't let that put you off — it's basically just JavaScript with type annotations. You'll pick it up in no time. If you're using an editor other than [Visual Studio Code](https://code.visualstudio.com/) you may need to install a plugin in order to get syntax highlighting and code hints etc. The compiler is written in [TypeScript](https://www.typescriptlang.org/), but don't let that put you off — it's basically just JavaScript with type annotations. You'll pick it up in no time. If you're using an editor other than [Visual Studio Code](https://code.visualstudio.com/), you may need to install a plugin in order to get syntax highlighting and code hints, etc.
### Running Tests ### Running Tests

24
package-lock.json generated

@ -1,6 +1,6 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.31.2", "version": "3.32.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -1051,19 +1051,19 @@
} }
}, },
"css-tree": { "css-tree": {
"version": "1.0.0-alpha22", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha22.tgz", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.2.tgz",
"integrity": "sha1-M4oAbjMce0+dq3tq9Tns5W/3ivI=", "integrity": "sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"mdn-data": "^1.0.0", "mdn-data": "2.0.14",
"source-map": "^0.5.3" "source-map": "^0.6.1"
}, },
"dependencies": { "dependencies": {
"source-map": { "source-map": {
"version": "0.5.7", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true "dev": true
} }
} }
@ -2904,9 +2904,9 @@
} }
}, },
"mdn-data": { "mdn-data": {
"version": "1.2.0", "version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.2.0.tgz", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-esDqNvsJB2q5V28+u7NdtdMg6Rmg4khQmAVSjUiX7BY/7haIv0K2yWM43hYp0or+3nvG7+UaTF1JHz31hgU1TA==", "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"dev": true "dev": true
}, },
"mem": { "mem": {

@ -1,6 +1,6 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.31.2", "version": "3.32.1",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"module": "index.mjs", "module": "index.mjs",
"main": "index", "main": "index",
@ -112,7 +112,7 @@
"c8": "^5.0.1", "c8": "^5.0.1",
"code-red": "^0.1.4", "code-red": "^0.1.4",
"codecov": "^3.5.0", "codecov": "^3.5.0",
"css-tree": "1.0.0-alpha22", "css-tree": "^1.1.2",
"eslint": "^7.15.0", "eslint": "^7.15.0",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-svelte3": "^2.7.3", "eslint-plugin-svelte3": "^2.7.3",

@ -43,12 +43,17 @@ In the terminal, you can instantly create a new project like so:
```bash ```bash
npx degit sveltejs/template my-svelte-project npx degit sveltejs/template my-svelte-project
cd my-svelte-project cd my-svelte-project
# to use TypeScript run:
# node scripts/setupTypeScript.js
npm install npm install
npm run dev npm run dev
``` ```
This will create a new project in the `my-svelte-project` directory, install its dependencies, and start a server on http://localhost:5000. This will create a new project in the `my-svelte-project` directory, install its dependencies, and start a server on http://localhost:5000.
You can find more information about using TypeScript [here](blog/svelte-and-typescript).
Once you've tinkered a bit and understood how everything fits together, you can fork [sveltejs/template](https://github.com/sveltejs/template) and start doing this instead: Once you've tinkered a bit and understood how everything fits together, you can fork [sveltejs/template](https://github.com/sveltejs/template) and start doing this instead:
```bash ```bash

@ -0,0 +1,86 @@
---
title: What's new in Svelte: February 2021
description: Integrations and improvements at lightning speed...
author: Daniel Sandoval
authorURL: https://desandoval.net
---
With the shortest month of the year coming up, Svelte maintainers and community members alike have been busy this last month from big changes in `svelte-loader`, `prettier-plugin-svelte`, `rollup-plugin-svelte`, and `language-tools` to steady progress in Sapper and `svelte-preprocess`. Meanwhile, lots of folks have been busy integrating Svelte with other popular frameworks.
## New compiler features
- Aria roles from the [WAI-ARIA Graphics Module](https://www.w3.org/TR/graphics-aria-1.0/#role_definitions) are now recognized as valid aria roles in Svelte components (**3.31.1**)
- Compiler warnings for the common React attributes, `className` and `htmlFor`, now make it easier to port React components to Svelte (**3.31.1**)
Have a suggestion for a compiler feature or want to help implement new features/bug fixes? Check out the ["triage: good first issue" tag for Svelte](https://github.com/sveltejs/svelte/issues?q=is%3Aopen+is%3Aissue+label%3A%22triage%3A+good+first+issue%22)
## New bits in language-tools
- User disabled auto import suggestions no longer show in VS Code (**103.0.0**)
- Renaming a variable is now safer with smart additions of a prefix/suffix to renamed variables (**104.0.0**)
- Semantic (token) highlighting for TypeScript users lets theme makers apply semantic styling in their themes, if they support it (**104.0.0**)
- "Extract Component" has been added to the context menu - allowing you to extract components out of files without having to open the command window to type "Svelte: Extract Component" (**104.0.0**)
- The VS Code extension now listens to JavaScript/TypeScript file changes - you no longer need to save the files for the changes to be noticed (**104.1.0**)
For the complete list of changes, check out the language-tools [Releases page](https://github.com/sveltejs/language-tools/releases).
A great way to try the language tools is to download the [Svelte Extension for VSCode](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). This extension provides syntax highlighting and rich intellisense for Svelte components in VS Code, using the svelte language server. Check your editor's extension sources to see if there's a Svelte plugin for your IDE or build your own (see [coc-svelte](https://github.com/coc-extensions/coc-svelte) for example)!
## Big improvements across the Svelte ecosystem
- [svelte-loader](https://github.com/sveltejs/svelte-loader) released a major version, 3.0.0 - featuring Webpack 5 and Node 14 support, better hot reloading, and new `compilerOptions` to match `rollup-plugin-svelte`. Breaking changes include dropping Svelte 2 and Node 8 support. [More info in the changelog](https://github.com/sveltejs/svelte-loader/blob/master/CHANGELOG.md)
- [rollup-plugin-svelte](https://github.com/sveltejs/rollup-plugin-svelte) is now at version 7.x - with support for relative filenames, better handling of sourcemaps, and consistent `compilerOptions`. Be sure to [checkout the changelog](https://github.com/sveltejs/rollup-plugin-svelte/blob/master/CHANGELOG.md) for breaking changes when upgrading
- [svelte-preprocess](https://github.com/sveltejs/svelte-preprocess) iterated through some 4.6.x releases this month to improve postcss and scss handling and fix sourcemap transformation for typescript users. [More info in the changelog](https://github.com/sveltejs/svelte-preprocess/blob/main/CHANGELOG.md)
- [Sapper](https://github.com/sveltejs/sapper) got some improvements in scroll tracking and handling encoding query parameters. Dynamic imports also now work as expected in browsers that don't support ES modules. These changes from 0.29.0 and a step-by-step migration guide can be found [in the changelog](https://github.com/sveltejs/sapper/blob/master/CHANGELOG.md)
- [prettier-plugin-svelte](https://github.com/sveltejs/prettier-plugin-svelte) version 2 was released. It received an overhaul and comes with a rewrite of the HTML formatting. The output is now much more in line with how standard Prettier formats HTML. Better defaults for `svelteBracketNewLine` and `options-scripts-markup-styles` should now match how the majority of users like to order the code blocks. Additionally, Prettier's `htmlWhitespaceSensitivity` setting is now supported. [More info in the changelog](https://github.com/sveltejs/prettier-plugin-svelte/blob/master/CHANGELOG.md)
New changes to the Svelte Society website include [a new cheat sheet](https://sveltesociety.dev/cheatsheet) for easy access to useful code patterns and some smaller visual fixes across the site. **Want to help make the Svelte Society website ready for prime time**? [Checkout the GitHub repo](https://github.com/svelte-society/sveltesociety.dev) to get started!
---
## Community Showcase
**Apps & Sites**
- [The official German vaccination dashboard](https://impfdashboard.de/) tracks the current COVID vaccine rollout and features some well-done dataviz.
- [La neuva era de la educatión conectada](https://elfuturoesapasionante.vodafone.es/especiales/educacion-conectada/) is a Vodaphone site that highlights the ways that technology and COVID-19 has changed the education landscape
- [sho.rest](https://github.com/Melonai/shorest) is a self-hostable url shortener
- [night.fm](https://night.fm/) is a cyberpunk-themed radio station
**Demos, Libraries & Components**
- [Svelte Reactive Debugger](https://addons.mozilla.org/en-US/firefox/addon/svelte-reactive-debugger/) is a way to monitor Svelte reactive statements in Firefox devtools
- [svelte-actions](https://github.com/sw-yx/svelte-actions) is a set of prototype Svelte actions for inclusion into official actions in future. [See RFC](https://github.com/sveltejs/rfcs/pull/24) and [Discuss High Level Policy](https://github.com/sw-yx/svelte-actions/issues/7).
- [This css grid gallery](https://svelte.dev/repl/3a1b7fae13b242fe9cd4a4f7aa092fa4?version=3.31.2) made by @joja (in the Svelte Discord) features grid transitions based on a user's mouse position
- [Patchcab](https://github.com/spectrome/patchcab) is a modular Eurorack style synthesizer made with Web Audio.
- [svelte-knob](https://github.com/MelihAltintas/svelte-knob) is a knob control to help with speedometer-style visualization
- [descent-ripple](https://github.com/micha-lmxt/descent-ripple) is a highly customizable javascript ripple animation for buttons
- [makeItSnow](https://github.com/florianlouvet/make-it-snow/blob/main/makeItSnowAction.js) is a Svelte action made by @MrPoule (in the Svelte Discord) that can be used to add ❄snow❄ to any component ([Demo](https://svelte.dev/repl/de5223beb45540a5a11c9bd7b318304f?version=3.31.2))
- [svelte-video-player](https://github.com/meigo/svelte-video-player) is a customizable `VideoPlayer` component
- [svelte-readonly](https://github.com/Crisfole/svelte-readonly) is a very small store that exposes only a readable interface.
**New Integrations & Starters**
- [svelte-derver-starter](https://github.com/AlexxNB/svelte-derver-starter) is a starter for baking fullstack application with the client based on Svelte and server side powered by Derver.
- [eleventy-plugin-embed-svelte](https://github.com/shalomscott/eleventy-plugin-embed-svelte) makes it easy to embed Svelte components into an 11ty site.
- [svelte-tailwind-extension-boilerplate](https://github.com/kyrelldixon/svelte-tailwind-extension-boilerplate) is a good foundation for a Chrome extension using either JavaScript or TypeScript, Svelte for the frontend, Tailwind CSS for styling, Jest for testing, and Rollup as the build system.
- [snowpack-ui](https://github.com/rajasegar/snowpack-ui) lets you run & manage Snowpack projects from the browser instead of the terminal
- [Svelte for Appwrite](https://dev.to/torstendittmann/svelte-for-appwrite-4fkg) explains how to integrate with Appwrite, a self-hosted Firebase alternative [Github Repo](https://github.com/appwrite/sdk-for-svelte)
- [here-maps-svelte](https://github.com/peopledrivemecrazy/here-maps-svelte) makes it easy to include HERE maps in a Svelte app
- [p5-svelte](https://github.com/tonyketcham/p5-svelte) is an absolutely dead simple way of tossing the creative coding/sketching tool, p5, into a project
- [svelte-windicss-preprocess](https://github.com/voorjaar/svelte-windicss-preprocess) is a Svelte preprocessor to compile tailwindcss at build time based on windicss compiler
- [MitzaCoder/svelte-boilerplate](https://github.com/MitzaCoder/svelte-boilerplate) features configurations for Typescript, TailwindCSS, IE11 compatibility (with Babel) and lazy loaded modules.
**Want to share your Svelte Component with the world?** Head over to the [Components](https://sveltesociety.dev/components) page on the Svelte Society site. You can contribute by making [a PR to this file](https://github.com/svelte-society/sveltesociety.dev/blob/master/src/pages/components/components.json).
**Learning Resources**
- [lihautan's Svelte Actions Playlist](https://www.youtube.com/watch?v=ciaMT_MswzE&list=PLoKaNN3BjQX3Gl14MBygFf8buPIw9pAeK) teaches how actions work and how they can help solve common problems when developing Svelte applications
- [One-click Portfolio/Personal blog generator from dev.to API ](https://dev.to/shriji/one-click-portfolio-personal-blog-generator-from-dev-to-api-3apb) walks through creating a Sapper site that also fetches your articles from DEV.to using the API
- [How to Code a VSCode Extension](https://www.youtube.com/watch?v=a5DX5pQ9p5M) features Svelte as a way to render a custom UI within VSCode
- [This YouTube series on Plenti](https://www.youtube.com/watch?v=wyNC7R_VVyQ&list=PLbWvcwWtuDm12y3Hye6oKDwI2gAS0ccHW) walks through the new static site generator in detail
## See you next month!
Want to add your work to the Showcase? Want to contribute to Svelte? Check out the [Svelte Society](https://sveltesociety.dev/), [Reddit](https://www.reddit.com/r/sveltejs/) and [Discord](https://discord.com/invite/yy75DKs) to get involved!

@ -1292,19 +1292,19 @@ Components can have child content, in the same way that elements can.
The content is exposed in the child component using the `<slot>` element, which can contain fallback content that is rendered if no children are provided. The content is exposed in the child component using the `<slot>` element, which can contain fallback content that is rendered if no children are provided.
```sv ```sv
<!-- App.svelte -->
<Widget></Widget>
<Widget>
<p>this is some child content that will overwrite the default slot content</p>
</Widget>
<!-- Widget.svelte --> <!-- Widget.svelte -->
<div> <div>
<slot> <slot>
this fallback content will be rendered when no content is provided, like in the first example this fallback content will be rendered when no content is provided, like in the first example
</slot> </slot>
</div> </div>
<!-- App.svelte -->
<Widget></Widget> <!-- this component will render the default content -->
<Widget>
<p>this is some child content that will overwrite the default slot content</p>
</Widget>
``` ```
#### [`<slot name="`*name*`">`](slot_name) #### [`<slot name="`*name*`">`](slot_name)
@ -1314,18 +1314,18 @@ The content is exposed in the child component using the `<slot>` element, which
Named slots allow consumers to target specific areas. They can also have fallback content. Named slots allow consumers to target specific areas. They can also have fallback content.
```sv ```sv
<!-- App.svelte -->
<Widget>
<h1 slot="header">Hello</h1>
<p slot="footer">Copyright (c) 2019 Svelte Industries</p>
</Widget>
<!-- Widget.svelte --> <!-- Widget.svelte -->
<div> <div>
<slot name="header">No header was provided</slot> <slot name="header">No header was provided</slot>
<p>Some content between header and footer</p> <p>Some content between header and footer</p>
<slot name="footer"></slot> <slot name="footer"></slot>
</div> </div>
<!-- App.svelte -->
<Widget>
<h1 slot="header">Hello</h1>
<p slot="footer">Copyright (c) 2019 Svelte Industries</p>
</Widget>
``` ```
#### [`$$slots`](slots_object) #### [`$$slots`](slots_object)
@ -1337,20 +1337,21 @@ Named slots allow consumers to target specific areas. They can also have fallbac
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. 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 ```sv
<!-- App.svelte -->
<Card>
<h1 slot="title">Blog Post Title</h1>
</Card>
<!-- Card.svelte --> <!-- Card.svelte -->
<div> <div>
<slot name="title"></slot> <slot name="title"></slot>
{#if $$slots.description} {#if $$slots.description}
<!-- This slot and the <hr> before it will not render. --> <!-- This <hr> and slot will render only if a slot named "description" is provided. -->
<hr> <hr>
<slot name="description"></slot> <slot name="description"></slot>
{/if} {/if}
</div> </div>
<!-- App.svelte -->
<Card>
<h1 slot="title">Blog Post Title</h1>
<!-- No slot named "description" was provided so the optional slot will not be rendered. -->
</Card>
``` ```
#### [`<slot let:`*name*`={`*value*`}>`](slot_let) #### [`<slot let:`*name*`={`*value*`}>`](slot_let)
@ -1362,11 +1363,6 @@ Slots can be rendered zero or more times, and can pass values *back* to the pare
The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}`, and `<slot {item}>` is equivalent to `<slot item={item}>`. The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}`, and `<slot {item}>` is equivalent to `<slot item={item}>`.
```sv ```sv
<!-- App.svelte -->
<FancyList {items} let:prop={thing}>
<div>{thing.text}</div>
</FancyList>
<!-- FancyList.svelte --> <!-- FancyList.svelte -->
<ul> <ul>
{#each items as item} {#each items as item}
@ -1375,6 +1371,11 @@ The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}
</li> </li>
{/each} {/each}
</ul> </ul>
<!-- App.svelte -->
<FancyList {items} let:prop={thing}>
<div>{thing.text}</div>
</FancyList>
``` ```
--- ---
@ -1382,12 +1383,6 @@ The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}
Named slots can also expose values. The `let:` directive goes on the element with the `slot` attribute. Named slots can also expose values. The `let:` directive goes on the element with the `slot` attribute.
```sv ```sv
<!-- App.svelte -->
<FancyList {items}>
<div slot="item" let:item>{item.text}</div>
<p slot="footer">Copyright (c) 2019 Svelte Industries</p>
</FancyList>
<!-- FancyList.svelte --> <!-- FancyList.svelte -->
<ul> <ul>
{#each items as item} {#each items as item}
@ -1398,6 +1393,12 @@ Named slots can also expose values. The `let:` directive goes on the element wit
</ul> </ul>
<slot name="footer"></slot> <slot name="footer"></slot>
<!-- App.svelte -->
<FancyList {items}>
<div slot="item" let:item>{item.text}</div>
<p slot="footer">Copyright (c) 2019 Svelte Industries</p>
</FancyList>
``` ```
@ -1530,7 +1531,7 @@ The `<svelte:options>` element provides a place to specify per-component compile
* `immutable={false}` — the default. Svelte will be more conservative about whether or not mutable objects have changed * `immutable={false}` — the default. Svelte will be more conservative about whether or not mutable objects have changed
* `accessors={true}` — adds getters and setters for the component's props * `accessors={true}` — adds getters and setters for the component's props
* `accessors={false}` — the default * `accessors={false}` — the default
* `namespace="..."` — the namespace where this component will be used, most commonly "svg" * `namespace="..."` — the namespace where this component will be used, most commonly "svg"; use the "foreign" namespace to opt out of case-insensitive attribute names and HTML-specific warnings
* `tag="..."` — the name to use when compiling this component as a custom element * `tag="..."` — the name to use when compiling this component as a custom element
```sv ```sv

@ -80,7 +80,7 @@ The following options can be passed to the compiler. None are required:
| `outputFilename` | `null` | A `string` used for your JavaScript sourcemap. | `outputFilename` | `null` | A `string` used for your JavaScript sourcemap.
| `cssOutputFilename` | `null` | A `string` used for your CSS sourcemap. | `cssOutputFilename` | `null` | A `string` used for your CSS sourcemap.
| `sveltePath` | `"svelte"` | The location of the `svelte` package. Any imports from `svelte` or `svelte/[module]` will be modified accordingly. | `sveltePath` | `"svelte"` | The location of the `svelte` package. Any imports from `svelte` or `svelte/[module]` will be modified accordingly.
| `namespace` | `"html"` | The namespace of the element; e.g., `"mathml"`, `"svg"`, `"foreign"`.
--- ---

@ -19,3 +19,9 @@ Components can also dispatch events. To do so, they must create an event dispatc
``` ```
> `createEventDispatcher` must be called when the component is first instantiated — you can't do it later inside e.g. a `setTimeout` callback. This links `dispatch` to the component instance. > `createEventDispatcher` must be called when the component is first instantiated — you can't do it later inside e.g. a `setTimeout` callback. This links `dispatch` to the component instance.
Notice that the `App` component is listening to the messages dispatched by `Inner` component thanks to the `on:message` directive. This directive is an attribute prefixed with `on:` followed by the event name that we are dispatching (in this case, `message`).
Without this attribute, messages would still be dispatched, but the App would not react to it. You can try removing the `on:message` attribute and pressing the button again.
> You can also try changing the event name to something else. For instance, change `dispatch('message')` to `dispatch('myevent')` in `Inner.svelte` and change the attribute name from `on:message` to `on:myevent` in the `App.svelte` component.

@ -8,5 +8,6 @@ sh.exec('npx degit sveltejs/community scripts/community');
// copy over relevant files // copy over relevant files
sh.cp('scripts/community/whos-using-svelte/WhosUsingSvelte.svelte', 'src/routes/_components/WhosUsingSvelte.svelte'); sh.cp('scripts/community/whos-using-svelte/WhosUsingSvelte.svelte', 'src/routes/_components/WhosUsingSvelte.svelte');
sh.cp('scripts/community/whos-using-svelte/WhosUsingSvelte.js', 'src/routes/_components/WhosUsingSvelte.js');
sh.rm('-rf', 'static/organisations'); sh.rm('-rf', 'static/organisations');
sh.cp('-r', 'scripts/community/whos-using-svelte/organisations', 'static'); sh.cp('-r', 'scripts/community/whos-using-svelte/organisations', 'static');

@ -28,7 +28,7 @@
p { margin: 1em auto } p { margin: 1em auto }
.error { .error {
background-color: #da106e; background-color: var(--second);
color: white; color: white;
padding: 12px 16px; padding: 12px 16px;
font: 600 16px/1.7 var(--font); font: 600 16px/1.7 var(--font);

@ -77,6 +77,8 @@
npx degit <a href="https://github.com/sveltejs/template" style="user-select: initial;">sveltejs/template</a> my-svelte-project npx degit <a href="https://github.com/sveltejs/template" style="user-select: initial;">sveltejs/template</a> my-svelte-project
<span class="token comment"># or download and extract <a href="https://github.com/sveltejs/template/archive/master.zip">this .zip file</a></span> <span class="token comment"># or download and extract <a href="https://github.com/sveltejs/template/archive/master.zip">this .zip file</a></span>
cd my-svelte-project cd my-svelte-project
<span class="token comment"># to use <a href="blog/svelte-and-typescript">TypeScript</a> run:</span>
<span class="token comment"># node scripts/setupTypeScript.js</span>
npm install npm install
npm run dev npm run dev

@ -29,7 +29,7 @@ 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 { apply_preprocessor_sourcemap } from '../utils/mapped_code';
import Element from './nodes/Element'; import Element from './nodes/Element';
import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types'; import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types';
@ -1175,15 +1175,20 @@ export default class Component {
extract_reactive_declarations() { extract_reactive_declarations() {
const component = this; const component = this;
const unsorted_reactive_declarations = []; const unsorted_reactive_declarations: Array<{
assignees: Set<string>;
dependencies: Set<string>;
node: Node;
declaration: Node;
}> = [];
this.ast.instance.content.body.forEach(node => { this.ast.instance.content.body.forEach(node => {
if (node.type === 'LabeledStatement' && node.label.name === '$') { if (node.type === 'LabeledStatement' && node.label.name === '$') {
this.reactive_declaration_nodes.add(node); this.reactive_declaration_nodes.add(node);
const assignees = new Set(); const assignees = new Set<string>();
const assignee_nodes = new Set(); const assignee_nodes = new Set();
const dependencies = new Set(); const dependencies = new Set<string>();
let scope = this.instance_scope; let scope = this.instance_scope;
const map = this.instance_scope_map; const map = this.instance_scope_map;
@ -1214,10 +1219,22 @@ export default class Component {
const { name } = identifier; const { name } = identifier;
const owner = scope.find_owner(name); const owner = scope.find_owner(name);
const variable = component.var_lookup.get(name); const variable = component.var_lookup.get(name);
if (variable) variable.is_reactive_dependency = true; let should_add_as_dependency = true;
if (variable) {
variable.is_reactive_dependency = true;
if (variable.module) {
should_add_as_dependency = false;
component.warn(node as any, {
code: 'module-script-reactive-declaration',
message: `"${name}" is declared in a module script and will not be reactive`
});
}
}
const is_writable_or_mutated = const is_writable_or_mutated =
variable && (variable.writable || variable.mutated); variable && (variable.writable || variable.mutated);
if ( if (
should_add_as_dependency &&
(!owner || owner === component.instance_scope) && (!owner || owner === component.instance_scope) &&
(name[0] === '$' || is_writable_or_mutated) (name[0] === '$' || is_writable_or_mutated)
) { ) {
@ -1349,7 +1366,8 @@ function process_component_options(component: Component, nodes) {
'accessors' in component.compile_options 'accessors' in component.compile_options
? component.compile_options.accessors ? component.compile_options.accessors
: !!component.compile_options.customElement, : !!component.compile_options.customElement,
preserveWhitespace: !!component.compile_options.preserveWhitespace preserveWhitespace: !!component.compile_options.preserveWhitespace,
namespace: component.compile_options.namespace
}; };
const node = nodes.find(node => node.name === 'svelte:options'); const node = nodes.find(node => node.name === 'svelte:options');

@ -84,7 +84,7 @@ export default class Selector {
while (i--) { while (i--) {
const selector = block.selectors[i]; const selector = block.selectors[i];
if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') { if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') {
if (selector.name !== 'root') { if (selector.name !== 'root' && selector.name !== 'host') {
if (i === 0) code.prependRight(selector.start, attr); if (i === 0) code.prependRight(selector.start, attr);
} }
continue; continue;
@ -162,7 +162,10 @@ function apply_selector(blocks: Block[], node: Element, to_encapsulate: any[]):
if (!block) return false; if (!block) return false;
if (!node) { if (!node) {
return block.global && blocks.every(block => block.global); return (
(block.global && blocks.every(block => block.global)) ||
(block.host && blocks.length === 0)
);
} }
switch (block_might_apply_to_node(block, node)) { switch (block_might_apply_to_node(block, node)) {
@ -182,6 +185,11 @@ function apply_selector(blocks: Block[], node: Element, to_encapsulate: any[]):
continue; continue;
} }
if (ancestor_block.host) {
to_encapsulate.push({ node, block });
return true;
}
let parent = node; let parent = node;
while (parent = get_element_parent(parent)) { while (parent = get_element_parent(parent)) {
if (block_might_apply_to_node(ancestor_block, parent) !== BlockAppliesToNode.NotPossible) { if (block_might_apply_to_node(ancestor_block, parent) !== BlockAppliesToNode.NotPossible) {
@ -211,6 +219,19 @@ function apply_selector(blocks: Block[], node: Element, to_encapsulate: any[]):
} else if (block.combinator.name === '+' || block.combinator.name === '~') { } else if (block.combinator.name === '+' || block.combinator.name === '~') {
const siblings = get_possible_element_siblings(node, block.combinator.name === '+'); const siblings = get_possible_element_siblings(node, block.combinator.name === '+');
let has_match = false; let has_match = false;
// NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the
// css-tree limitation that does not parse the inner selector of :global
// so unless we are sure there will be no sibling to match, we will consider it as matched
const has_global = blocks.some(block => block.global);
if (has_global) {
if (siblings.size === 0 && get_element_parent(node) !== null) {
return false;
}
to_encapsulate.push({ node, block });
return true;
}
for (const possible_sibling of siblings.keys()) { for (const possible_sibling of siblings.keys()) {
if (apply_selector(blocks.slice(), possible_sibling, to_encapsulate)) { if (apply_selector(blocks.slice(), possible_sibling, to_encapsulate)) {
to_encapsulate.push({ node, block }); to_encapsulate.push({ node, block });
@ -236,6 +257,10 @@ function block_might_apply_to_node(block: Block, node: Element): BlockAppliesToN
const selector = block.selectors[i]; const selector = block.selectors[i];
const name = typeof selector.name === 'string' && selector.name.replace(/\\(.)/g, '$1'); const name = typeof selector.name === 'string' && selector.name.replace(/\\(.)/g, '$1');
if (selector.type === 'PseudoClassSelector' && name === 'host') {
return BlockAppliesToNode.NotPossible;
}
if (selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') { if (selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') {
continue; continue;
} }
@ -541,6 +566,7 @@ function loop_child(children: INode[], adjacent_only: boolean) {
class Block { class Block {
global: boolean; global: boolean;
host: boolean;
combinator: CssNode; combinator: CssNode;
selectors: CssNode[] selectors: CssNode[]
start: number; start: number;
@ -550,6 +576,7 @@ class Block {
constructor(combinator: CssNode) { constructor(combinator: CssNode) {
this.combinator = combinator; this.combinator = combinator;
this.global = false; this.global = false;
this.host = false;
this.selectors = []; this.selectors = [];
this.start = null; this.start = null;
@ -562,6 +589,7 @@ class Block {
if (this.selectors.length === 0) { if (this.selectors.length === 0) {
this.start = selector.start; this.start = selector.start;
this.global = selector.type === 'PseudoClassSelector' && selector.name === 'global'; this.global = selector.type === 'PseudoClassSelector' && selector.name === 'global';
this.host = selector.type === 'PseudoClassSelector' && selector.name === 'host';
} }
this.selectors.push(selector); this.selectors.push(selector);

@ -47,7 +47,7 @@ class Rule {
constructor(node: CssNode, stylesheet, parent?: Atrule) { constructor(node: CssNode, stylesheet, parent?: Atrule) {
this.node = node; this.node = node;
this.parent = parent; this.parent = parent;
this.selectors = node.selector.children.map((node: CssNode) => new Selector(node, stylesheet)); this.selectors = node.prelude.children.map((node: CssNode) => new Selector(node, stylesheet));
this.declarations = node.block.children.map((node: CssNode) => new Declaration(node)); this.declarations = node.block.children.map((node: CssNode) => new Declaration(node));
} }
@ -182,11 +182,11 @@ class Atrule {
minify(code: MagicString, dev: boolean) { minify(code: MagicString, dev: boolean) {
if (this.node.name === 'media') { if (this.node.name === 'media') {
const expression_char = code.original[this.node.expression.start]; const expression_char = code.original[this.node.prelude.start];
let c = this.node.start + (expression_char === '(' ? 6 : 7); let c = this.node.start + (expression_char === '(' ? 6 : 7);
if (this.node.expression.start > c) code.remove(c, this.node.expression.start); if (this.node.prelude.start > c) code.remove(c, this.node.prelude.start);
this.node.expression.children.forEach((query: CssNode) => { this.node.prelude.children.forEach((query: CssNode) => {
// TODO minify queries // TODO minify queries
c = query.end; c = query.end;
}); });
@ -194,17 +194,17 @@ class Atrule {
code.remove(c, this.node.block.start); code.remove(c, this.node.block.start);
} else if (this.node.name === 'supports') { } else if (this.node.name === 'supports') {
let c = this.node.start + 9; let c = this.node.start + 9;
if (this.node.expression.start - c > 1) code.overwrite(c, this.node.expression.start, ' '); if (this.node.prelude.start - c > 1) code.overwrite(c, this.node.prelude.start, ' ');
this.node.expression.children.forEach((query: CssNode) => { this.node.prelude.children.forEach((query: CssNode) => {
// TODO minify queries // TODO minify queries
c = query.end; c = query.end;
}); });
code.remove(c, this.node.block.start); code.remove(c, this.node.block.start);
} else { } else {
let c = this.node.start + this.node.name.length + 1; let c = this.node.start + this.node.name.length + 1;
if (this.node.expression) { if (this.node.prelude) {
if (this.node.expression.start - c > 1) code.overwrite(c, this.node.expression.start, ' '); if (this.node.prelude.start - c > 1) code.overwrite(c, this.node.prelude.start, ' ');
c = this.node.expression.end; c = this.node.prelude.end;
} }
if (this.node.block && this.node.block.start - c > 0) { if (this.node.block && this.node.block.start - c > 0) {
code.remove(c, this.node.block.start); code.remove(c, this.node.block.start);
@ -235,7 +235,7 @@ class Atrule {
transform(code: MagicString, id: string, keyframes: Map<string, string>, max_amount_class_specificity_increased: number) { transform(code: MagicString, id: string, keyframes: Map<string, string>, max_amount_class_specificity_increased: number) {
if (is_keyframes_node(this.node)) { if (is_keyframes_node(this.node)) {
this.node.expression.children.forEach(({ type, name, start, end }: CssNode) => { this.node.prelude.children.forEach(({ type, name, start, end }: CssNode) => {
if (type === 'Identifier') { if (type === 'Identifier') {
if (name.startsWith('-global-')) { if (name.startsWith('-global-')) {
code.remove(start, start + 8); code.remove(start, start + 8);
@ -317,7 +317,7 @@ export default class Stylesheet {
} }
if (is_keyframes_node(node)) { if (is_keyframes_node(node)) {
node.expression.children.forEach((expression: CssNode) => { node.prelude.children.forEach((expression: CssNode) => {
if (expression.type === 'Identifier' && !expression.name.startsWith('-global-')) { if (expression.type === 'Identifier' && !expression.name.startsWith('-global-')) {
this.keyframes.set(expression.name, `${this.id}-${expression.name}`); this.keyframes.set(expression.name, `${this.id}-${expression.name}`);
} }

@ -6,6 +6,7 @@ import { CompileOptions, Warning } from '../interfaces';
import Component from './Component'; import Component from './Component';
import fuzzymatch from '../utils/fuzzymatch'; import fuzzymatch from '../utils/fuzzymatch';
import get_name_from_filename from './utils/get_name_from_filename'; import get_name_from_filename from './utils/get_name_from_filename';
import { valid_namespaces } from '../utils/namespaces';
const valid_options = [ const valid_options = [
'format', 'format',
@ -22,6 +23,7 @@ const valid_options = [
'hydratable', 'hydratable',
'legacy', 'legacy',
'customElement', 'customElement',
'namespace',
'tag', 'tag',
'css', 'css',
'loopGuardTimeout', 'loopGuardTimeout',
@ -30,7 +32,7 @@ const valid_options = [
]; ];
function validate_options(options: CompileOptions, warnings: Warning[]) { function validate_options(options: CompileOptions, warnings: Warning[]) {
const { name, filename, loopGuardTimeout, dev } = options; const { name, filename, loopGuardTimeout, dev, namespace } = options;
Object.keys(options).forEach(key => { Object.keys(options).forEach(key => {
if (!valid_options.includes(key)) { if (!valid_options.includes(key)) {
@ -65,6 +67,15 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
toString: () => message toString: () => message
}); });
} }
if (namespace && valid_namespaces.indexOf(namespace) === -1) {
const match = fuzzymatch(namespace, valid_namespaces);
if (match) {
throw new Error(`Invalid namespace '${namespace}' (did you mean '${match}'?)`);
} else {
throw new Error(`Invalid namespace '${namespace}'`);
}
}
} }
export default function compile(source: string, options: CompileOptions = {}) { export default function compile(source: string, options: CompileOptions = {}) {

@ -136,6 +136,7 @@ export default class Element extends Node {
this.namespace = get_namespace(parent as Element, this, component.namespace); this.namespace = get_namespace(parent as Element, this, component.namespace);
if (this.namespace !== namespaces.foreign) {
if (this.name === 'textarea') { if (this.name === 'textarea') {
if (info.children.length > 0) { if (info.children.length > 0) {
const value_attribute = info.attributes.find(node => node.name === 'value'); const value_attribute = info.attributes.find(node => node.name === 'value');
@ -173,7 +174,7 @@ export default class Element extends Node {
}); });
} }
} }
}
const has_let = info.attributes.some(node => node.type === 'Let'); const has_let = info.attributes.some(node => node.type === 'Let');
if (has_let) { if (has_let) {
scope = scope.child(); scope = scope.child();
@ -253,65 +254,83 @@ export default class Element extends Node {
}); });
} }
if (a11y_distracting_elements.has(this.name)) { this.validate_attributes();
// no-distracting-elements this.validate_event_handlers();
this.component.warn(this, { if (this.namespace === namespaces.foreign) {
code: 'a11y-distracting-elements', this.validate_bindings_foreign();
message: `A11y: Avoid <${this.name}> elements` } else {
}); this.validate_attributes_a11y();
this.validate_special_cases();
this.validate_bindings();
this.validate_content();
} }
if (this.name === 'figcaption') {
let { parent } = this;
let is_figure_parent = false;
while (parent) {
if ((parent as Element).name === 'figure') {
is_figure_parent = true;
break;
}
if (parent.type === 'Element') {
break;
}
parent = parent.parent;
} }
if (!is_figure_parent) { validate_attributes() {
this.component.warn(this, { const { component, parent } = this;
code: 'a11y-structure',
message: 'A11y: <figcaption> must be an immediate child of <figure>' this.attributes.forEach(attribute => {
if (attribute.is_spread) return;
const name = attribute.name.toLowerCase();
// Errors
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
component.error(attribute, {
code: 'illegal-attribute',
message: `'${name}' is not a valid attribute name`
}); });
} }
if (name === 'slot') {
if (!attribute.is_static) {
component.error(attribute, {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
});
} }
if (this.name === 'figure') { if (component.slot_outlets.has(name)) {
const children = this.children.filter(node => { component.error(attribute, {
if (node.type === 'Comment') return false; code: 'duplicate-slot-attribute',
if (node.type === 'Text') return /\S/.test(node.data); message: `Duplicate '${name}' slot`
return true;
}); });
const index = children.findIndex(child => (child as Element).name === 'figcaption'); component.slot_outlets.add(name);
}
if (index !== -1 && (index !== 0 && index !== children.length - 1)) { if (!(parent.type === 'InlineComponent' || within_custom_element(parent))) {
this.component.warn(children[index], { component.error(attribute, {
code: 'a11y-structure', code: 'invalid-slotted-content',
message: 'A11y: <figcaption> must be first or last child of <figure>' message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
}); });
} }
} }
this.validate_attributes(); // Warnings
this.validate_special_cases();
this.validate_bindings(); if (this.namespace !== namespaces.foreign) {
this.validate_content(); if (name === 'is') {
this.validate_event_handlers(); component.warn(attribute, {
code: 'avoid-is',
message: 'The \'is\' attribute is not supported cross-browser and should be avoided'
});
} }
validate_attributes() { if (react_attributes.has(attribute.name)) {
const { component, parent } = this; component.warn(attribute, {
code: 'invalid-html-attribute',
message: `'${attribute.name}' is not a valid HTML attribute. Did you mean '${react_attributes.get(attribute.name)}'?`
});
}
}
});
}
const attribute_map = new Map(); validate_attributes_a11y() {
const { component } = this;
this.attributes.forEach(attribute => { this.attributes.forEach(attribute => {
if (attribute.is_spread) return; if (attribute.is_spread) return;
@ -408,60 +427,13 @@ export default class Element extends Node {
}); });
} }
} }
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
component.error(attribute, {
code: 'illegal-attribute',
message: `'${name}' is not a valid attribute name`
});
}
if (name === 'slot') {
if (!attribute.is_static) {
component.error(attribute, {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
});
}
if (component.slot_outlets.has(name)) {
component.error(attribute, {
code: 'duplicate-slot-attribute',
message: `Duplicate '${name}' slot`
}); });
component.slot_outlets.add(name);
} }
if (!(parent.type === 'InlineComponent' || within_custom_element(parent))) {
component.error(attribute, {
code: 'invalid-slotted-content',
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
});
}
}
if (name === 'is') {
component.warn(attribute, {
code: 'avoid-is',
message: 'The \'is\' attribute is not supported cross-browser and should be avoided'
});
}
if (react_attributes.has(attribute.name)) {
component.warn(attribute, {
code: 'invalid-html-attribute',
message: `'${attribute.name}' is not a valid HTML attribute. Did you mean '${react_attributes.get(attribute.name)}'?`
});
}
attribute_map.set(attribute.name, attribute);
});
}
validate_special_cases() { validate_special_cases() {
const { component, attributes, handlers } = this; const { component, attributes, handlers } = this;
const attribute_map = new Map(); const attribute_map = new Map();
const handlers_map = new Map(); const handlers_map = new Map();
@ -576,6 +548,65 @@ export default class Element extends Node {
}); });
} }
} }
if (a11y_distracting_elements.has(this.name)) {
// no-distracting-elements
component.warn(this, {
code: 'a11y-distracting-elements',
message: `A11y: Avoid <${this.name}> elements`
});
}
if (this.name === 'figcaption') {
let { parent } = this;
let is_figure_parent = false;
while (parent) {
if ((parent as Element).name === 'figure') {
is_figure_parent = true;
break;
}
if (parent.type === 'Element') {
break;
}
parent = parent.parent;
}
if (!is_figure_parent) {
component.warn(this, {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be an immediate child of <figure>'
});
}
}
if (this.name === 'figure') {
const children = this.children.filter(node => {
if (node.type === 'Comment') return false;
if (node.type === 'Text') return /\S/.test(node.data);
return true;
});
const index = children.findIndex(child => (child as Element).name === 'figcaption');
if (index !== -1 && (index !== 0 && index !== children.length - 1)) {
component.warn(children[index], {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be first or last child of <figure>'
});
}
}
}
validate_bindings_foreign() {
this.bindings.forEach(binding => {
if (binding.name !== 'this') {
this.component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding. Foreign elements only support bind:this`
});
}
});
} }
validate_bindings() { validate_bindings() {

@ -7,7 +7,7 @@ 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 { apply_preprocessor_sourcemap } from '../../utils/mapped_code';
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types'; import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
export default function dom( export default function dom(

@ -8,6 +8,7 @@ import Expression from '../../../nodes/shared/Expression';
import Text from '../../../nodes/Text'; import Text from '../../../nodes/Text';
import handle_select_value_binding from './handle_select_value_binding'; import handle_select_value_binding from './handle_select_value_binding';
import { Identifier, Node } from 'estree'; import { Identifier, Node } from 'estree';
import { namespaces } from '../../../../utils/namespaces';
export class BaseAttributeWrapper { export class BaseAttributeWrapper {
node: Attribute; node: Attribute;
@ -67,15 +68,26 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
} }
} }
if (this.parent.node.namespace == namespaces.foreign) {
// leave attribute case alone for elements in the "foreign" namespace
this.name = this.node.name;
this.metadata = this.get_metadata();
this.is_indirectly_bound_value = false;
this.property_name = null;
this.is_select_value_attribute = false;
this.is_input_value = false;
} else {
this.name = fix_attribute_casing(this.node.name); this.name = fix_attribute_casing(this.node.name);
this.metadata = this.get_metadata(); this.metadata = this.get_metadata();
this.is_indirectly_bound_value = is_indirectly_bound_value(this); this.is_indirectly_bound_value = is_indirectly_bound_value(this);
this.property_name = this.is_indirectly_bound_value this.property_name = this.is_indirectly_bound_value
? '__value' ? '__value'
: this.metadata && this.metadata.property_name; : this.metadata && this.metadata.property_name;
this.is_src = this.name === 'src'; // TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select'; this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select';
this.is_input_value = this.name === 'value' && this.parent.node.name === 'input'; this.is_input_value = this.name === 'value' && this.parent.node.name === 'input';
}
this.is_src = this.name === 'src'; // TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
this.should_cache = should_cache(this); this.should_cache = should_cache(this);
} }

@ -26,6 +26,7 @@ import Action from '../../../nodes/Action';
import MustacheTagWrapper from '../MustacheTag'; import MustacheTagWrapper from '../MustacheTag';
import RawMustacheTagWrapper from '../RawMustacheTag'; import RawMustacheTagWrapper from '../RawMustacheTag';
import create_slot_block from './create_slot_block'; import create_slot_block from './create_slot_block';
import is_dynamic from '../shared/is_dynamic';
interface BindingGroup { interface BindingGroup {
events: string[]; events: string[];
@ -898,10 +899,19 @@ export default class ElementWrapper extends Wrapper {
const all_dependencies = this.class_dependencies.concat(...dependencies); const all_dependencies = this.class_dependencies.concat(...dependencies);
const condition = block.renderer.dirty(all_dependencies); const condition = block.renderer.dirty(all_dependencies);
// If all of the dependencies are non-dynamic (don't get updated) then there is no reason
// to add an updater for this.
const any_dynamic_dependencies = all_dependencies.some((dep) => {
const variable = this.renderer.component.var_lookup.get(dep);
return !variable || is_dynamic(variable);
});
if (any_dynamic_dependencies) {
block.chunks.update.push(b` block.chunks.update.push(b`
if (${condition}) { if (${condition}) {
${updater} ${updater}
}`); }
`);
}
} }
}); });
} }

@ -51,7 +51,6 @@ export default function ssr(
return b` return b`
${component.compile_options.dev && b`@validate_store(${store_name}, '${store_name}');`} ${component.compile_options.dev && b`@validate_store(${store_name}, '${store_name}');`}
${`$$unsubscribe_${store_name}`} = @subscribe(${store_name}, #value => ${name} = #value) ${`$$unsubscribe_${store_name}`} = @subscribe(${store_name}, #value => ${name} = #value)
${store_name}.subscribe($$value => ${name} = $$value);
`; `;
}); });
const reactive_store_unsubscriptions = reactive_stores.map( const reactive_store_unsubscriptions = reactive_stores.map(

@ -124,6 +124,7 @@ export interface CompileOptions {
tag?: string; tag?: string;
css?: boolean; css?: boolean;
loopGuardTimeout?: number; loopGuardTimeout?: number;
namespace?: string;
preserveComments?: boolean; preserveComments?: boolean;
preserveWhitespace?: boolean; preserveWhitespace?: boolean;

@ -14,10 +14,13 @@ export default function read_style(parser: Parser, start: number, attributes: No
try { try {
ast = parse(styles, { ast = parse(styles, {
positions: true, positions: true,
offset: content_start offset: content_start,
onParseError(error) {
throw error;
}
}); });
} catch (err) { } catch (err) {
if (err.name === 'CssSyntaxError') { if (err.name === 'SyntaxError') {
parser.error({ parser.error({
code: 'css-syntax-error', code: 'css-syntax-error',
message: err.message message: err.message

@ -376,7 +376,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
if (type === 'Binding' && directive_name !== 'this') { if (type === 'Binding' && directive_name !== 'this') {
check_unique(directive_name); check_unique(directive_name);
} else if (type !== 'EventHandler') { } else if (type !== 'EventHandler' && type !== 'Action') {
check_unique(name); check_unique(name);
} }
@ -387,6 +387,13 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
}, start); }, start);
} }
if (type === 'Class' && directive_name === '') {
parser.error({
code: 'invalid-class-directive',
message: 'Class binding name cannot be empty'
}, start + colon_index + 1);
}
if (value[0]) { if (value[0]) {
if ((value as any[]).length > 1 || value[0].type === 'Text') { if ((value as any[]).length > 1 || value[0].type === 'Text') {
parser.error({ parser.error({

@ -0,0 +1,88 @@
import { decode as decode_mappings } from 'sourcemap-codec';
import { Processed } from './types';
/**
* Import decoded sourcemap from mozilla/source-map/SourceMapGenerator
* Forked from source-map/lib/source-map-generator.js
* from methods _serializeMappings and toJSON.
* We cannot use source-map.d.ts types, because we access hidden properties.
*/
function decoded_sourcemap_from_generator(generator: any) {
let previous_generated_line = 1;
const converted_mappings = [[]];
let result_line;
let result_segment;
let mapping;
const source_idx = generator._sources.toArray()
.reduce((acc, val, idx) => (acc[val] = idx, acc), {});
const name_idx = generator._names.toArray()
.reduce((acc, val, idx) => (acc[val] = idx, acc), {});
const mappings = generator._mappings.toArray();
result_line = converted_mappings[0];
for (let i = 0, len = mappings.length; i < len; i++) {
mapping = mappings[i];
if (mapping.generatedLine > previous_generated_line) {
while (mapping.generatedLine > previous_generated_line) {
converted_mappings.push([]);
previous_generated_line++;
}
result_line = converted_mappings[mapping.generatedLine - 1]; // line is one-based
} else if (i > 0) {
const previous_mapping = mappings[i - 1];
if (
// sorted by selectivity
mapping.generatedColumn === previous_mapping.generatedColumn &&
mapping.originalColumn === previous_mapping.originalColumn &&
mapping.name === previous_mapping.name &&
mapping.generatedLine === previous_mapping.generatedLine &&
mapping.originalLine === previous_mapping.originalLine &&
mapping.source === previous_mapping.source
) {
continue;
}
}
result_line.push([mapping.generatedColumn]);
result_segment = result_line[result_line.length - 1];
if (mapping.source != null) {
result_segment.push(...[
source_idx[mapping.source],
mapping.originalLine - 1, // line is one-based
mapping.originalColumn
]);
if (mapping.name != null) {
result_segment.push(name_idx[mapping.name]);
}
}
}
const map = {
version: generator._version,
sources: generator._sources.toArray(),
names: generator._names.toArray(),
mappings: converted_mappings
};
if (generator._file != null) {
(map as any).file = generator._file;
}
// not needed: map.sourcesContent and map.sourceRoot
return map;
}
export function decode_map(processed: Processed) {
let 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);
}
if ((decoded_map as any)._mappings && decoded_map.constructor.name === 'SourceMapGenerator') {
// import decoded sourcemap from mozilla/source-map/SourceMapGenerator
decoded_map = decoded_sourcemap_from_generator(decoded_map);
}
return decoded_map;
}

@ -1,319 +1,230 @@
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types'; import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
import { decode as decode_mappings } from 'sourcemap-codec';
import { getLocator } from 'locate-character'; import { getLocator } from 'locate-character';
import { StringWithSourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/string_with_sourcemap'; import { MappedCode, SourceLocation, parse_attached_sourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/mapped_code';
import { decode_map } from './decode_sourcemap';
export interface Processed { import { replace_in_code, slice_source } from './replace_in_code';
code: string; import { MarkupPreprocessor, Source, Preprocessor, PreprocessorGroup, Processed } from './types';
map?: string | object; // we are opaque with the type here to avoid dependency on the remapping module for our public types.
interface SourceUpdate {
string?: string;
map?: DecodedSourceMap;
dependencies?: string[]; dependencies?: string[];
} }
export interface PreprocessorGroup {
markup?: (options: {
content: string;
filename: string;
}) => Processed | Promise<Processed>;
style?: Preprocessor;
script?: Preprocessor;
}
export type Preprocessor = (options: {
content: string;
attributes: Record<string, string | boolean>;
filename?: string;
}) => Processed | Promise<Processed>;
function parse_attributes(str: string) {
const attrs = {};
str.split(/\s+/).filter(Boolean).forEach(attr => {
const p = attr.indexOf('=');
if (p === -1) {
attrs[attr] = true;
} else {
attrs[attr.slice(0, p)] = '\'"'.includes(attr[p + 1]) ?
attr.slice(p + 2, -1) :
attr.slice(p + 1);
}
});
return attrs;
}
function get_file_basename(filename: string) { function get_file_basename(filename: string) {
return filename.split(/[/\\]/).pop(); return filename.split(/[/\\]/).pop();
} }
interface Replacement {
offset: number;
length: number;
replacement: StringWithSourcemap;
}
async function replace_async(
file_basename: string,
source: string,
get_location: ReturnType<typeof getLocator>,
re: RegExp,
func: (...any) => Promise<StringWithSourcemap>
): Promise<StringWithSourcemap> {
const replacements: Array<Promise<Replacement>> = [];
source.replace(re, (...args) => {
replacements.push(
func(...args).then(
res =>
({
offset: args[args.length - 2],
length: args[0].length,
replacement: res
}) as Replacement
)
);
return '';
});
const out = new StringWithSourcemap();
let last_end = 0;
for (const { offset, length, replacement } of await Promise.all(
replacements
)) {
// content = unchanged source characters before the replaced segment
const content = StringWithSourcemap.from_source(
file_basename, source.slice(last_end, offset), get_location(last_end));
out.concat(content).concat(replacement);
last_end = offset + length;
}
// final_content = unchanged source characters after last replaced segment
const final_content = StringWithSourcemap.from_source(
file_basename, source.slice(last_end), get_location(last_end));
return out.concat(final_content);
}
/** /**
* Import decoded sourcemap from mozilla/source-map/SourceMapGenerator * Represents intermediate states of the preprocessing.
* Forked from source-map/lib/source-map-generator.js
* from methods _serializeMappings and toJSON.
* We cannot use source-map.d.ts types, because we access hidden properties.
*/ */
function decoded_sourcemap_from_generator(generator: any) { class PreprocessResult implements Source {
let previous_generated_line = 1; // sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1)
const converted_mappings = [[]]; // so we use sourcemap_list.unshift() to add new maps
let result_line; // https://github.com/ampproject/remapping#multiple-transformations-of-a-file
let result_segment; sourcemap_list: Array<DecodedSourceMap | RawSourceMap> = [];
let mapping; dependencies: string[] = [];
file_basename: string;
const source_idx = generator._sources.toArray()
.reduce((acc, val, idx) => (acc[val] = idx, acc), {});
const name_idx = generator._names.toArray()
.reduce((acc, val, idx) => (acc[val] = idx, acc), {});
const mappings = generator._mappings.toArray(); get_location: ReturnType<typeof getLocator>;
result_line = converted_mappings[0];
for (let i = 0, len = mappings.length; i < len; i++) { constructor(public source: string, public filename: string) {
mapping = mappings[i]; this.update_source({ string: source });
if (mapping.generatedLine > previous_generated_line) { // preprocess source must be relative to itself or equal null
while (mapping.generatedLine > previous_generated_line) { this.file_basename = filename == null ? null : get_file_basename(filename);
converted_mappings.push([]);
previous_generated_line++;
}
result_line = converted_mappings[mapping.generatedLine - 1]; // line is one-based
} else if (i > 0) {
const previous_mapping = mappings[i - 1];
if (
// sorted by selectivity
mapping.generatedColumn === previous_mapping.generatedColumn &&
mapping.originalColumn === previous_mapping.originalColumn &&
mapping.name === previous_mapping.name &&
mapping.generatedLine === previous_mapping.generatedLine &&
mapping.originalLine === previous_mapping.originalLine &&
mapping.source === previous_mapping.source
) {
continue;
} }
update_source({ string: source, map, dependencies }: SourceUpdate) {
if (source != null) {
this.source = source;
this.get_location = getLocator(source);
} }
result_line.push([mapping.generatedColumn]);
result_segment = result_line[result_line.length - 1]; if (map) {
this.sourcemap_list.unshift(map);
if (mapping.source != null) {
result_segment.push(...[
source_idx[mapping.source],
mapping.originalLine - 1, // line is one-based
mapping.originalColumn
]);
if (mapping.name != null) {
result_segment.push(name_idx[mapping.name]);
} }
if (dependencies) {
this.dependencies.push(...dependencies);
} }
} }
const map = { to_processed(): Processed {
version: generator._version, // Combine all the source maps for each preprocessor function into one
sources: generator._sources.toArray(), const map: RawSourceMap = combine_sourcemaps(this.file_basename, this.sourcemap_list);
names: generator._names.toArray(),
mappings: converted_mappings return {
// TODO return separated output, in future version where svelte.compile supports it:
// style: { code: styleCode, map: styleMap },
// script { code: scriptCode, map: scriptMap },
// markup { code: markupCode, map: markupMap },
code: this.source,
dependencies: [...new Set(this.dependencies)],
map: map as object,
toString: () => this.source
}; };
if (generator._file != null) {
(map as any).file = generator._file;
} }
// not needed: map.sourcesContent and map.sourceRoot
return map;
} }
/** /**
* Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap * Convert preprocessor output for the tag content into MappedCode
*/ */
function get_replacement( function processed_content_to_code(processed: Processed, location: SourceLocation, file_basename: string): MappedCode {
file_basename: string, // Convert the preprocessed code and its sourcemap to a MappedCode
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(
file_basename, prefix, get_location(offset));
const suffix_with_map = StringWithSourcemap.from_source(
file_basename, suffix, get_location(offset + prefix.length + original.length));
// Convert the preprocessed code and its sourcemap to a StringWithSourcemap
let decoded_map: DecodedSourceMap; let decoded_map: DecodedSourceMap;
if (processed.map) { if (processed.map) {
decoded_map = typeof processed.map === 'string' ? JSON.parse(processed.map) : processed.map; decoded_map = decode_map(processed);
if (typeof(decoded_map.mappings) === 'string') {
decoded_map.mappings = decode_mappings(decoded_map.mappings);
}
if ((decoded_map as any)._mappings && decoded_map.constructor.name === 'SourceMapGenerator') {
// import decoded sourcemap from mozilla/source-map/SourceMapGenerator
decoded_map = decoded_sourcemap_from_generator(decoded_map);
}
// offset only segments pointing at original component source // offset only segments pointing at original component source
const source_index = decoded_map.sources.indexOf(file_basename); const source_index = decoded_map.sources.indexOf(file_basename);
if (source_index !== -1) { if (source_index !== -1) {
sourcemap_add_offset(decoded_map, get_location(offset + prefix.length), source_index); sourcemap_add_offset(decoded_map, location, source_index);
} }
} }
const processed_with_map = StringWithSourcemap.from_processed(processed.code, decoded_map);
// Surround the processed code with the prefix and suffix, retaining valid sourcemappings return MappedCode.from_processed(processed.code, decoded_map);
return prefix_with_map.concat(processed_with_map).concat(suffix_with_map);
} }
export default async function preprocess( /**
source: string, * Given the whole tag including content, return a `MappedCode`
preprocessor: PreprocessorGroup | PreprocessorGroup[], * representing the tag content replaced with `processed`.
options?: { filename?: string } */
) { function processed_tag_to_code(
// @ts-ignore todo: doublecheck processed: Processed,
const filename = (options && options.filename) || preprocessor.filename; // legacy tag_name: 'style' | 'script',
const dependencies = []; attributes: string,
source: Source
): MappedCode {
const { file_basename, get_location } = source;
// preprocess source must be relative to itself or equal null const build_mapped_code = (code: string, offset: number) =>
const file_basename = filename == null ? null : get_file_basename(filename); MappedCode.from_source(slice_source(code, offset, source));
const preprocessors = preprocessor const tag_open = `<${tag_name}${attributes || ''}>`;
? Array.isArray(preprocessor) ? preprocessor : [preprocessor] const tag_close = `</${tag_name}>`;
: [];
const markup = preprocessors.map(p => p.markup).filter(Boolean); const tag_open_code = build_mapped_code(tag_open, 0);
const script = preprocessors.map(p => p.script).filter(Boolean); const tag_close_code = build_mapped_code(tag_close, tag_open.length + source.source.length);
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) parse_attached_sourcemap(processed, tag_name);
// 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) { const content_code = processed_content_to_code(processed, get_location(tag_open.length), file_basename);
// run markup preprocessor return tag_open_code.concat(content_code).concat(tag_close_code);
const processed = await fn({ }
content: source,
filename
});
if (!processed) continue; function parse_tag_attributes(str: string) {
// note: won't work with attribute values containing spaces.
return str
.split(/\s+/)
.filter(Boolean)
.reduce((attrs, attr) => {
const i = attr.indexOf('=');
const [key, value] = i > 0 ? [attr.slice(0, i), attr.slice(i+1)] : [attr];
const [, unquoted] = (value && value.match(/^['"](.*)['"]$/)) || [];
if (processed.dependencies) dependencies.push(...processed.dependencies); return { ...attrs, [key]: unquoted ?? value ?? true };
source = processed.code; }, {});
if (processed.map) {
sourcemap_list.unshift(
typeof(processed.map) === 'string'
? JSON.parse(processed.map)
: processed.map
);
}
} }
async function preprocess_tag_content(tag_name: 'style' | 'script', preprocessor: Preprocessor) { /**
const get_location = getLocator(source); * Calculate the updates required to process all instances of the specified tag.
const tag_regex = tag_name === 'style' */
async function process_tag(
tag_name: 'style' | 'script',
preprocessor: Preprocessor,
source: Source
): Promise<SourceUpdate> {
const { filename } = source;
const tag_regex =
tag_name === 'style'
? /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi ? /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi
: /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi; : /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
const res = await replace_async( const dependencies: string[] = [];
file_basename,
source, async function process_single_tag(
get_location, tag_with_content: string,
tag_regex, attributes = '',
async (match, attributes = '', content = '', offset) => { content = '',
const no_change = () => StringWithSourcemap.from_source( tag_offset: number
file_basename, match, get_location(offset)); ): Promise<MappedCode> {
if (!attributes && !content) { const no_change = () => MappedCode.from_source(slice_source(tag_with_content, tag_offset, source));
return no_change();
} if (!attributes && !content) return no_change();
attributes = attributes || '';
content = content || '';
// run script preprocessor
const processed = await preprocessor({ const processed = await preprocessor({
content, content: content || '',
attributes: parse_attributes(attributes), attributes: parse_tag_attributes(attributes || ''),
filename filename
}); });
if (processed && processed.dependencies) {
dependencies.push(...processed.dependencies); if (!processed) return no_change();
if (processed.dependencies) dependencies.push(...processed.dependencies);
if (!processed.map && processed.code === content) return no_change();
return processed_tag_to_code(processed, tag_name, attributes, slice_source(content, tag_offset, source));
} }
if (!processed || !processed.map && processed.code === content) {
return no_change(); const { string, map } = await replace_in_code(tag_regex, process_single_tag, source);
return { string, map, dependencies };
} }
return get_replacement(file_basename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, `</${tag_name}>`);
async function process_markup(filename: string, process: MarkupPreprocessor, source: Source) {
const processed = await process({
content: source.source,
filename
});
if (processed) {
return {
string: processed.code,
map: processed.map
? // TODO: can we use decode_sourcemap?
typeof processed.map === 'string'
? JSON.parse(processed.map)
: processed.map
: undefined,
dependencies: processed.dependencies
};
} else {
return {};
} }
);
source = res.string;
sourcemap_list.unshift(res.map);
} }
for (const fn of script) { export default async function preprocess(
await preprocess_tag_content('script', fn); source: string,
} preprocessor: PreprocessorGroup | PreprocessorGroup[],
options?: { filename?: string }
): Promise<Processed> {
// @ts-ignore todo: doublecheck
const filename = (options && options.filename) || preprocessor.filename; // legacy
for (const fn of style) { const preprocessors = preprocessor ? (Array.isArray(preprocessor) ? preprocessor : [preprocessor]) : [];
await preprocess_tag_content('style', fn);
}
// Combine all the source maps for each preprocessor function into one const markup = preprocessors.map(p => p.markup).filter(Boolean);
const map: RawSourceMap = combine_sourcemaps( const script = preprocessors.map(p => p.script).filter(Boolean);
file_basename, const style = preprocessors.map(p => p.style).filter(Boolean);
sourcemap_list
);
return { const result = new PreprocessResult(source, filename);
// TODO return separated output, in future version where svelte.compile supports it:
// style: { code: styleCode, map: styleMap }, // TODO keep track: what preprocessor generated what sourcemap?
// script { code: scriptCode, map: scriptMap }, // to make debugging easier = detect low-resolution sourcemaps in fn combine_mappings
// markup { code: markupCode, map: markupMap },
code: source, for (const process of markup) {
dependencies: [...new Set(dependencies)], result.update_source(await process_markup(filename, process, result));
map: (map as object),
toString() {
return source;
} }
};
for (const process of script) {
result.update_source(await process_tag('script', process, result));
}
for (const preprocess of style) {
result.update_source(await process_tag('style', preprocess, result));
}
return result.to_processed();
} }

@ -0,0 +1,75 @@
import { MappedCode } from '../utils/mapped_code';
import { Source } from './types';
interface Replacement {
offset: number;
length: number;
replacement: MappedCode;
}
export function slice_source(
code_slice: string,
offset: number,
{ file_basename, filename, get_location }: Source
): Source {
return {
source: code_slice,
get_location: (index: number) => get_location(index + offset),
file_basename,
filename
};
}
function calculate_replacements(
re: RegExp,
get_replacement: (...match: any[]) => Promise<MappedCode>,
source: string
) {
const replacements: Array<Promise<Replacement>> = [];
source.replace(re, (...match) => {
replacements.push(
get_replacement(...match).then(
replacement => {
const matched_string = match[0];
const offset = match[match.length-2];
return ({ offset, length: matched_string.length, replacement });
}
)
);
return '';
});
return Promise.all(replacements);
}
function perform_replacements(
replacements: Replacement[],
source: Source
): MappedCode {
const out = new MappedCode();
let last_end = 0;
for (const { offset, length, replacement } of replacements) {
const unchanged_prefix = MappedCode.from_source(
slice_source(source.source.slice(last_end, offset), last_end, source)
);
out.concat(unchanged_prefix).concat(replacement);
last_end = offset + length;
}
const unchanged_suffix = MappedCode.from_source(slice_source(source.source.slice(last_end), last_end, source));
return out.concat(unchanged_suffix);
}
export async function replace_in_code(
regex: RegExp,
get_replacement: (...match: any[]) => Promise<MappedCode>,
location: Source
): Promise<MappedCode> {
const replacements = await calculate_replacements(regex, get_replacement, location.source);
return perform_replacements(replacements, location);
}

@ -0,0 +1,32 @@
import { Location } from 'locate-character';
export interface Source {
source: string;
get_location: (search: number) => Location;
file_basename: string;
filename: string;
}
export interface Processed {
code: string;
map?: string | object; // we are opaque with the type here to avoid dependency on the remapping module for our public types.
dependencies?: string[];
toString?: () => string;
}
export type MarkupPreprocessor = (options: {
content: string;
filename: string;
}) => Processed | Promise<Processed>;
export type Preprocessor = (options: {
content: string;
attributes: Record<string, string | boolean>;
filename?: string;
}) => Processed | Promise<Processed>;
export interface PreprocessorGroup {
markup?: MarkupPreprocessor;
style?: Preprocessor;
script?: Preprocessor;
}

@ -1,8 +1,9 @@
import { DecodedSourceMap, RawSourceMap, SourceMapLoader } from '@ampproject/remapping/dist/types/types'; import { DecodedSourceMap, RawSourceMap, SourceMapLoader } from '@ampproject/remapping/dist/types/types';
import remapping from '@ampproject/remapping'; import remapping from '@ampproject/remapping';
import { SourceMap } from 'magic-string'; import { SourceMap } from 'magic-string';
import { Source, Processed } from '../preprocess/types';
type SourceLocation = { export type SourceLocation = {
line: number; line: number;
column: number; column: number;
}; };
@ -67,7 +68,7 @@ function pushArray<T>(_this: T[], other: T[]) {
} }
} }
export class StringWithSourcemap { export class MappedCode {
string: string; string: string;
map: DecodedSourceMap; map: DecodedSourceMap;
@ -89,7 +90,7 @@ export class StringWithSourcemap {
* concat in-place (mutable), return this (chainable) * concat in-place (mutable), return this (chainable)
* will also mutate the `other` object * will also mutate the `other` object
*/ */
concat(other: StringWithSourcemap): StringWithSourcemap { concat(other: MappedCode): MappedCode {
// noop: if one is empty, return the other // noop: if one is empty, return the other
if (other.string == '') return this; if (other.string == '') return this;
if (this.string == '') { if (this.string == '') {
@ -166,7 +167,7 @@ export class StringWithSourcemap {
return this; return this;
} }
static from_processed(string: string, map?: DecodedSourceMap): StringWithSourcemap { static from_processed(string: string, map?: DecodedSourceMap): MappedCode {
const line_count = string.split('\n').length; const line_count = string.split('\n').length;
if (map) { if (map) {
@ -177,23 +178,23 @@ export class StringWithSourcemap {
for (let i = 0; i < missing_lines; i++) { for (let i = 0; i < missing_lines; i++) {
map.mappings.push([]); map.mappings.push([]);
} }
return new StringWithSourcemap(string, map); return new MappedCode(string, map);
} }
if (string == '') return new StringWithSourcemap(); if (string == '') return new MappedCode();
map = { version: 3, names: [], sources: [], mappings: [] }; map = { version: 3, names: [], sources: [], mappings: [] };
// add empty SourceMapSegment[] for every line // add empty SourceMapSegment[] for every line
for (let i = 0; i < line_count; i++) map.mappings.push([]); for (let i = 0; i < line_count; i++) map.mappings.push([]);
return new StringWithSourcemap(string, map); return new MappedCode(string, map);
} }
static from_source( static from_source({ source, file_basename, get_location }: Source): MappedCode {
source_file: string, source: string, offset?: SourceLocation let offset: SourceLocation = get_location(0);
): StringWithSourcemap {
if (!offset) offset = { line: 0, column: 0 }; if (!offset) offset = { line: 0, column: 0 };
const map: DecodedSourceMap = { version: 3, names: [], sources: [source_file], mappings: [] }; const map: DecodedSourceMap = { version: 3, names: [], sources: [file_basename], mappings: [] };
if (source == '') return new StringWithSourcemap(source, map); if (source == '') return new MappedCode(source, map);
// we create a high resolution identity map here, // we create a high resolution identity map here,
// we know that it will eventually be merged with svelte's map, // we know that it will eventually be merged with svelte's map,
@ -213,7 +214,7 @@ export class StringWithSourcemap {
for (let segment = 0; segment < segment_list.length; segment++) { for (let segment = 0; segment < segment_list.length; segment++) {
segment_list[segment][3] += offset.column; segment_list[segment][3] += offset.column;
} }
return new StringWithSourcemap(source, map); return new MappedCode(source, map);
} }
} }
@ -255,6 +256,7 @@ export function combine_sourcemaps(
// browser vs node.js // browser vs node.js
const b64enc = typeof btoa == 'function' ? btoa : b => Buffer.from(b).toString('base64'); const b64enc = typeof btoa == 'function' ? btoa : b => Buffer.from(b).toString('base64');
const b64dec = typeof atob == 'function' ? atob : a => Buffer.from(a, 'base64').toString();
export function apply_preprocessor_sourcemap(filename: string, svelte_map: SourceMap, preprocessor_map_input: string | DecodedSourceMap | RawSourceMap): SourceMap { 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; if (!svelte_map || !preprocessor_map_input) return svelte_map;
@ -288,3 +290,39 @@ export function apply_preprocessor_sourcemap(filename: string, svelte_map: Sourc
return result_map as SourceMap; return result_map as SourceMap;
} }
// parse attached sourcemap in processed.code
export function parse_attached_sourcemap(processed: Processed, tag_name: 'script' | 'style'): void {
const r_in = '[#@]\\s*sourceMappingURL\\s*=\\s*(\\S*)';
const regex = (tag_name == 'script')
? new RegExp('(?://'+r_in+')|(?:/\\*'+r_in+'\\s*\\*/)$')
: new RegExp('/\\*'+r_in+'\\s*\\*/$');
function log_warning(message) {
// code_start: help to find preprocessor
const code_start = processed.code.length < 100 ? processed.code : (processed.code.slice(0, 100) + ' [...]');
console.warn(`warning: ${message}. processed.code = ${JSON.stringify(code_start)}`);
}
processed.code = processed.code.replace(regex, (_, match1, match2) => {
const map_url = (tag_name == 'script') ? (match1 || match2) : match1;
const map_data = (map_url.match(/data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/) || [])[1];
if (map_data) {
// sourceMappingURL is data URL
if (processed.map) {
log_warning('Not implemented. ' +
'Found sourcemap in both processed.code and processed.map. ' +
'Please update your preprocessor to return only one sourcemap.');
// ignore attached sourcemap
return '';
}
processed.map = b64dec(map_data); // use attached sourcemap
return ''; // remove from processed.code
}
// sourceMappingURL is path or URL
if (!processed.map) {
log_warning(`Found sourcemap path ${JSON.stringify(map_url)} in processed.code, but no sourcemap data. ` +
'Please update your preprocessor to return sourcemap data directly.');
}
// ignore sourcemap path
return ''; // remove from processed.code
});
}

@ -1,3 +1,6 @@
// The `foreign` namespace covers all DOM implementations that aren't HTML5.
// It opts out of HTML5-specific a11y checks and case-insensitive attribute names.
export const foreign = 'https://svelte.dev/docs#svelte_options';
export const html = 'http://www.w3.org/1999/xhtml'; export const html = 'http://www.w3.org/1999/xhtml';
export const mathml = 'http://www.w3.org/1998/Math/MathML'; export const mathml = 'http://www.w3.org/1998/Math/MathML';
export const svg = 'http://www.w3.org/2000/svg'; export const svg = 'http://www.w3.org/2000/svg';
@ -6,12 +9,14 @@ export const xml = 'http://www.w3.org/XML/1998/namespace';
export const xmlns = 'http://www.w3.org/2000/xmlns'; export const xmlns = 'http://www.w3.org/2000/xmlns';
export const valid_namespaces = [ export const valid_namespaces = [
'foreign',
'html', 'html',
'mathml', 'mathml',
'svg', 'svg',
'xlink', 'xlink',
'xml', 'xml',
'xmlns', 'xmlns',
foreign,
html, html,
mathml, mathml,
svg, svg,
@ -20,4 +25,4 @@ export const valid_namespaces = [
xmlns xmlns
]; ];
export const namespaces: Record<string, string> = { html, mathml, svg, xlink, xml, xmlns }; export const namespaces: Record<string, string> = { foreign, html, mathml, svg, xlink, xml, xmlns };

@ -16,7 +16,7 @@ interface FlipParams {
easing?: (t: number) => number; easing?: (t: number) => number;
} }
export function flip(node: Element, animation: { from: DOMRect; to: DOMRect }, params: FlipParams): AnimationConfig { export function flip(node: Element, animation: { from: DOMRect; to: DOMRect }, params: FlipParams = {}): AnimationConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
const scaleX = animation.from.width / node.clientWidth; const scaleX = animation.from.width / node.clientWidth;

@ -100,8 +100,6 @@ export function init(component, options, instance, create_fragment, not_equal, p
const parent_component = current_component; const parent_component = current_component;
set_current_component(component); set_current_component(component);
const prop_values = options.props || {};
const $$: T$$ = component.$$ = { const $$: T$$ = component.$$ = {
fragment: null, fragment: null,
ctx: null, ctx: null,
@ -128,7 +126,7 @@ export function init(component, options, instance, create_fragment, not_equal, p
let ready = false; let ready = false;
$$.ctx = instance $$.ctx = instance
? instance(component, prop_values, (i, ret, ...rest) => { ? instance(component, options.props || {}, (i, ret, ...rest) => {
const value = rest.length ? rest[0] : ret; const value = rest.length ? rest[0] : ret;
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) { if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value); if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);

@ -115,6 +115,20 @@ export class SvelteComponentDev extends SvelteComponent {
* ### DO NOT USE! * ### DO NOT USE!
*/ */
$$prop_def: Props; $$prop_def: Props;
/**
* @private
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
*/
$$events_def: any;
/**
* @private
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
*/
$$slot_def: any;
constructor(options: { constructor(options: {
target: Element; target: Element;

@ -125,10 +125,12 @@ type StoresValues<T> = T extends Readable<infer U> ? U :
* *
* @param stores - input stores * @param stores - input stores
* @param fn - function callback that aggregates the values * @param fn - function callback that aggregates the values
* @param initial_value - when used asynchronously
*/ */
export function derived<S extends Stores, T>( export function derived<S extends Stores, T>(
stores: S, stores: S,
fn: (values: StoresValues<S>) => T fn: (values: StoresValues<S>, set: (value: T) => void) => Unsubscriber | void,
initial_value?: T
): Readable<T>; ): Readable<T>;
/** /**
@ -137,12 +139,10 @@ export function derived<S extends Stores, T>(
* *
* @param stores - input stores * @param stores - input stores
* @param fn - function callback that aggregates the values * @param fn - function callback that aggregates the values
* @param initial_value - when used asynchronously
*/ */
export function derived<S extends Stores, T>( export function derived<S extends Stores, T>(
stores: S, stores: S,
fn: (values: StoresValues<S>, set: (value: T) => void) => Unsubscriber | void, fn: (values: StoresValues<S>) => T
initial_value?: T
): Readable<T>; ): Readable<T>;
export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Readable<T> { export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Readable<T> {

@ -25,7 +25,7 @@ export function blur(node: Element, {
easing = cubicInOut, easing = cubicInOut,
amount = 5, amount = 5,
opacity = 0 opacity = 0
}: BlurParams): TransitionConfig { }: BlurParams = {}): TransitionConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const target_opacity = +style.opacity; const target_opacity = +style.opacity;
const f = style.filter === 'none' ? '' : style.filter; const f = style.filter === 'none' ? '' : style.filter;
@ -50,7 +50,7 @@ export function fade(node: Element, {
delay = 0, delay = 0,
duration = 400, duration = 400,
easing = linear easing = linear
}: FadeParams): TransitionConfig { }: FadeParams = {}): TransitionConfig {
const o = +getComputedStyle(node).opacity; const o = +getComputedStyle(node).opacity;
return { return {
@ -77,7 +77,7 @@ export function fly(node: Element, {
x = 0, x = 0,
y = 0, y = 0,
opacity = 0 opacity = 0
}: FlyParams): TransitionConfig { }: FlyParams = {}): TransitionConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const target_opacity = +style.opacity; const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
@ -104,7 +104,7 @@ export function slide(node: Element, {
delay = 0, delay = 0,
duration = 400, duration = 400,
easing = cubicOut easing = cubicOut
}: SlideParams): TransitionConfig { }: SlideParams = {}): TransitionConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const opacity = +style.opacity; const opacity = +style.opacity;
const height = parseFloat(style.height); const height = parseFloat(style.height);
@ -146,7 +146,7 @@ export function scale(node: Element, {
easing = cubicOut, easing = cubicOut,
start = 0, start = 0,
opacity = 0 opacity = 0
}: ScaleParams): TransitionConfig { }: ScaleParams = {}): TransitionConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const target_opacity = +style.opacity; const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
@ -177,7 +177,7 @@ export function draw(node: SVGElement & { getTotalLength(): number }, {
speed, speed,
duration, duration,
easing = cubicInOut easing = cubicInOut
}: DrawParams): TransitionConfig { }: DrawParams = {}): TransitionConfig {
const len = node.getTotalLength(); const len = node.getTotalLength();
if (duration === undefined) { if (duration === undefined) {

@ -0,0 +1,27 @@
export default {
warnings: [
{
code: 'css-unused-selector',
message: 'Unused CSS selector ":host > span"',
pos: 147,
start: {
character: 147,
column: 1,
line: 18
},
end: {
character: 159,
column: 13,
line: 18
},
frame: `
16: }
17:
18: :host > span {
^
19: color: red;
20: }
`
}
]
};

@ -0,0 +1 @@
:host h1.svelte-xyz{color:red}:host>h1.svelte-xyz{color:red}:host>.svelte-xyz{color:red}:host span.svelte-xyz{color:red}

@ -0,0 +1,27 @@
<style>
:host h1 {
color: red;
}
:host > h1 {
color: red;
}
:host > * {
color: red;
}
:host span {
color: red;
}
:host > span {
color: red;
}
</style>
<h1>Hello!</h1>
<div>
<span>World!</span>
</div>

@ -0,0 +1,50 @@
export default {
warnings: [
{
code: 'css-unused-selector',
message: 'Unused CSS selector ":global(input) + span"',
pos: 239,
start: {
character: 239,
column: 2,
line: 9
},
end: {
character: 260,
column: 23,
line: 9
},
frame: `
7: :global(input) ~ p { color: red; }
8:
9: :global(input) + span { color: red; }
^
10: :global(input) ~ span { color: red; }
11: </style>
`
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ":global(input) ~ span"',
pos: 279,
start: {
character: 279,
column: 2,
line: 10
},
end: {
character: 300,
column: 23,
line: 10
},
frame: `
8:
9: :global(input) + span { color: red; }
10: :global(input) ~ span { color: red; }
^
11: </style>
12:
`
}
]
};

@ -0,0 +1 @@
input+div.svelte-xyz{color:red}input~div.svelte-xyz{color:red}input+h1.svelte-xyz{color:red}input~h1.svelte-xyz{color:red}input+p.svelte-xyz{color:red}input~p.svelte-xyz{color:red}

@ -0,0 +1,21 @@
<style>
:global(input) + div { color: red; }
:global(input) ~ div { color: red; }
:global(input) + h1 { color: red; }
:global(input) ~ h1 { color: red; }
:global(input) + p { color: red; }
:global(input) ~ p { color: red; }
:global(input) + span { color: red; }
:global(input) ~ span { color: red; }
</style>
<h1>Hello!</h1>
<div>
<span>World!</span>
</div>
{#each [] as _}
<p />
{/each}

@ -0,0 +1,162 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
component_subscribe,
detach,
element,
init,
insert,
noop,
safe_not_equal,
space,
subscribe,
toggle_class
} from "svelte/internal";
import { reactiveStoreVal, unreactiveExport } from "./store";
function create_fragment(ctx) {
let div0;
let t0;
let div1;
let t1;
let div2;
let t2;
let div3;
let t3;
let div4;
let t4;
let div5;
let t5;
let div6;
let t6;
let div7;
let t7;
let div8;
return {
c() {
div0 = element("div");
t0 = space();
div1 = element("div");
t1 = space();
div2 = element("div");
t2 = space();
div3 = element("div");
t3 = space();
div4 = element("div");
t4 = space();
div5 = element("div");
t5 = space();
div6 = element("div");
t6 = space();
div7 = element("div");
t7 = space();
div8 = element("div");
toggle_class(div0, "update1", reactiveModuleVar);
toggle_class(div1, "update2", /*reactiveConst*/ ctx[0].x);
toggle_class(div2, "update3", nonReactiveGlobal && /*reactiveConst*/ ctx[0].x);
toggle_class(div3, "update4", /*$reactiveStoreVal*/ ctx[2]);
toggle_class(div4, "update5", /*$reactiveDeclaration*/ ctx[3]);
toggle_class(div5, "static1", nonReactiveModuleVar);
toggle_class(div6, "static2", nonReactiveGlobal);
toggle_class(div7, "static3", nonReactiveModuleVar && nonReactiveGlobal);
toggle_class(div8, "static4", unreactiveExport);
},
m(target, anchor) {
insert(target, div0, anchor);
insert(target, t0, anchor);
insert(target, div1, anchor);
insert(target, t1, anchor);
insert(target, div2, anchor);
insert(target, t2, anchor);
insert(target, div3, anchor);
insert(target, t3, anchor);
insert(target, div4, anchor);
insert(target, t4, anchor);
insert(target, div5, anchor);
insert(target, t5, anchor);
insert(target, div6, anchor);
insert(target, t6, anchor);
insert(target, div7, anchor);
insert(target, t7, anchor);
insert(target, div8, anchor);
},
p(ctx, [dirty]) {
if (dirty & /*reactiveModuleVar*/ 0) {
toggle_class(div0, "update1", reactiveModuleVar);
}
if (dirty & /*reactiveConst*/ 1) {
toggle_class(div1, "update2", /*reactiveConst*/ ctx[0].x);
}
if (dirty & /*nonReactiveGlobal, reactiveConst*/ 1) {
toggle_class(div2, "update3", nonReactiveGlobal && /*reactiveConst*/ ctx[0].x);
}
if (dirty & /*$reactiveStoreVal*/ 4) {
toggle_class(div3, "update4", /*$reactiveStoreVal*/ ctx[2]);
}
if (dirty & /*$reactiveDeclaration*/ 8) {
toggle_class(div4, "update5", /*$reactiveDeclaration*/ ctx[3]);
}
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(div0);
if (detaching) detach(t0);
if (detaching) detach(div1);
if (detaching) detach(t1);
if (detaching) detach(div2);
if (detaching) detach(t2);
if (detaching) detach(div3);
if (detaching) detach(t3);
if (detaching) detach(div4);
if (detaching) detach(t4);
if (detaching) detach(div5);
if (detaching) detach(t5);
if (detaching) detach(div6);
if (detaching) detach(t6);
if (detaching) detach(div7);
if (detaching) detach(t7);
if (detaching) detach(div8);
}
};
}
let nonReactiveModuleVar = Math.random();
let reactiveModuleVar = Math.random();
function instance($$self, $$props, $$invalidate) {
let reactiveDeclaration;
let $reactiveStoreVal;
let $reactiveDeclaration,
$$unsubscribe_reactiveDeclaration = noop,
$$subscribe_reactiveDeclaration = () => ($$unsubscribe_reactiveDeclaration(), $$unsubscribe_reactiveDeclaration = subscribe(reactiveDeclaration, $$value => $$invalidate(3, $reactiveDeclaration = $$value)), reactiveDeclaration);
component_subscribe($$self, reactiveStoreVal, $$value => $$invalidate(2, $reactiveStoreVal = $$value));
$$self.$$.on_destroy.push(() => $$unsubscribe_reactiveDeclaration());
nonReactiveGlobal = Math.random();
const reactiveConst = { x: Math.random() };
reactiveModuleVar += 1;
if (Math.random()) {
reactiveConst.x += 1;
}
$: $$subscribe_reactiveDeclaration($$invalidate(1, reactiveDeclaration = reactiveModuleVar * 2));
return [reactiveConst, reactiveDeclaration, $reactiveStoreVal, $reactiveDeclaration];
}
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
export default Component;

@ -0,0 +1,31 @@
<script context="module">
let nonReactiveModuleVar = Math.random();
let reactiveModuleVar = Math.random();
</script>
<script>
import { reactiveStoreVal, unreactiveExport } from './store';
nonReactiveGlobal = Math.random();
const reactiveConst = {x: Math.random()};
$: reactiveDeclaration = reactiveModuleVar * 2;
reactiveModuleVar += 1;
if (Math.random()) {
reactiveConst.x += 1;
}
</script>
<!--These should all get updaters because they have at least one reactive dependency-->
<div class:update1={reactiveModuleVar}></div>
<div class:update2={reactiveConst.x}></div>
<div class:update3={nonReactiveGlobal && reactiveConst.x}></div>
<div class:update4={$reactiveStoreVal}></div>
<div class:update5={$reactiveDeclaration}></div>
<!--These shouldn't get updates because they're purely non-reactive-->
<div class:static1={nonReactiveModuleVar}></div>
<div class:static2={nonReactiveGlobal}></div>
<div class:static3={nonReactiveModuleVar && nonReactiveGlobal}></div>
<div class:static4={unreactiveExport}></div>

@ -0,0 +1,4 @@
import { writable } from '../../../../store';
export const reactiveStoreVal = writable(0);
export const unreactiveExport = true;

@ -0,0 +1 @@
<input use:autofocus use:autofocus>

@ -0,0 +1,34 @@
{
"html": {
"start": 0,
"end": 35,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 35,
"type": "Element",
"name": "input",
"attributes": [
{
"start": 7,
"end": 20,
"type": "Action",
"name": "autofocus",
"modifiers": [],
"expression": null
},
{
"start": 21,
"end": 34,
"type": "Action",
"name": "autofocus",
"modifiers": [],
"expression": null
}
],
"children": []
}
]
}
}

@ -37,7 +37,7 @@
"children": [ "children": [
{ {
"type": "Rule", "type": "Rule",
"selector": { "prelude": {
"type": "SelectorList", "type": "SelectorList",
"children": [ "children": [
{ {
@ -74,7 +74,7 @@
"end": 43 "end": 43
} }
], ],
"start": 39, "start": 40,
"end": 43 "end": 43
}, },
"start": 33, "start": 33,

@ -0,0 +1,10 @@
{
"code": "invalid-class-directive",
"message": "Class binding name cannot be empty",
"start": {
"line": 1,
"column": 10,
"character": 10
},
"pos": 10
}

@ -0,0 +1,20 @@
// Test support for the `foreign` namespace preserving attribute case.
export default {
html: `
<page horizontalAlignment="center">
<button textWrap="true" text="button">
</page>
`,
options: {
hydrate: false // Hydration test will fail as case sensitivity is only handled for svg elements.
},
compileOptions: {
namespace: 'foreign'
},
test({ assert, target }) {
const attr = sel => target.querySelector(sel).attributes[0].name;
assert.equal(attr('page'), 'horizontalAlignment');
assert.equal(attr('button'), 'textWrap');
}
};

@ -0,0 +1,3 @@
<page horizontalAlignment="center">
<button textWrap="true" text="button">
</page>

@ -0,0 +1,18 @@
// Test support for the `foreign` namespace preserving attribute case.
export default {
html: `
<page horizontalAlignment="center">
<button textWrap="true" text="button">
</page>
`,
options: {
hydrate: false // Hydration test will fail as case sensitivity is only handled for svg elements.
},
test({ assert, target }) {
const attr = sel => target.querySelector(sel).attributes[0].name;
assert.equal(attr('page'), 'horizontalAlignment');
assert.equal(attr('button'), 'textWrap');
}
};

@ -0,0 +1,4 @@
<svelte:options namespace="foreign" />
<page horizontalAlignment="center">
<button textWrap="true" text="button">
</page>

@ -0,0 +1,20 @@
import { store } from './store.js';
export default {
html: '<h1>0</h1>',
before_test() {
store.reset();
},
async test({ assert, target, component }) {
store.set(42);
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<h1>42</h1>');
assert.equal(store.numberOfTimesSubscribeCalled(), 1);
},
test_ssr({ assert }) {
assert.equal(store.numberOfTimesSubscribeCalled(), 1);
}
};

@ -0,0 +1,5 @@
<script>
import { store } from './store';
</script>
<h1>{$store}</h1>

@ -0,0 +1,18 @@
import { writable } from '../../../../store';
const _store = writable(0);
let count = 0;
export const store = {
..._store,
subscribe(fn) {
count++;
return _store.subscribe(fn);
},
reset() {
count = 0;
_store.set(0);
},
numberOfTimesSubscribeCalled() {
return count;
}
};

@ -0,0 +1,18 @@
export default {
html: `
a: moduleA
b: moduleB
moduleA: moduleA
moduleB: moduleB
`,
async test({ assert, target, component }) {
await component.updateModuleA();
assert.htmlEqual(target.innerHTML, `
a: moduleA
b: moduleB
moduleA: moduleA
moduleB: moduleB
`);
}
};

@ -0,0 +1,15 @@
<script context="module">
let moduleA = 'moduleA';
let moduleB = 'moduleB';
</script>
<script>
export function updateModuleA() {
moduleA = 'something else';
}
$: a = moduleA;
$: b = moduleB;
</script>
a: {a}
b: {b}
moduleA: {moduleA}
moduleB: {moduleB}

@ -33,6 +33,10 @@ describe('ssr', () => {
return setupHtmlEqual(); return setupHtmlEqual();
}); });
let saved_window;
before(() => saved_window = global.window);
after(() => global.window = saved_window);
fs.readdirSync(`${__dirname}/samples`).forEach(dir => { fs.readdirSync(`${__dirname}/samples`).forEach(dir => {
if (dir[0] === '.') return; if (dir[0] === '.') return;
@ -197,6 +201,10 @@ describe('ssr', () => {
assert.htmlEqual(html, config.html); assert.htmlEqual(html, config.html);
} }
if (config.test_ssr) {
config.test_ssr({ assert });
}
if (config.after_test) config.after_test(); if (config.after_test) config.after_test();
if (config.show) { if (config.show) {

@ -31,7 +31,8 @@ describe('sourcemaps', () => {
const inputCode = fs.readFileSync(inputFile, 'utf-8'); const inputCode = fs.readFileSync(inputFile, 'utf-8');
const input = { const input = {
code: inputCode, code: inputCode,
locate: getLocator(inputCode) locate: getLocator(inputCode),
locate_1: getLocator(inputCode, { offsetLine: 1 })
}; };
const preprocessed = await svelte.preprocess( const preprocessed = await svelte.preprocess(
@ -86,12 +87,14 @@ describe('sourcemaps', () => {
assert.deepEqual( assert.deepEqual(
js.map.sources.slice().sort(), js.map.sources.slice().sort(),
(config.js_map_sources || ['input.svelte']).sort() (config.js_map_sources || ['input.svelte']).sort(),
'js.map.sources is wrong'
); );
if (css.map) { if (css.map) {
assert.deepEqual( assert.deepEqual(
css.map.sources.slice().sort(), css.map.sources.slice().sort(),
(config.css_map_sources || ['input.svelte']).sort() (config.css_map_sources || ['input.svelte']).sort(),
'css.map.sources is wrong'
); );
} }

@ -0,0 +1,44 @@
import MagicString from 'magic-string';
let indent_size = 4;
let comment_multi = true;
function get_processor(tag_name, search, replace) {
return {
[tag_name]: ({ content, filename }) => {
let code = content.slice();
const ms = new MagicString(code);
const idx = ms.original.indexOf(search);
if (idx == -1) throw new Error('search not found in src');
ms.overwrite(idx, idx + search.length, replace, { storeName: true });
// change line + column
const indent = Array.from({ length: indent_size }).join(' ');
ms.prependLeft(idx, '\n'+indent);
const map_opts = { source: filename, hires: true, includeContent: false };
const map = ms.generateMap(map_opts);
const attach_line = (tag_name == 'style' || comment_multi)
? `\n/*# sourceMappingURL=${map.toUrl()} */`
: `\n//# sourceMappingURL=${map.toUrl()}` // only in script
;
code = ms.toString() + attach_line;
indent_size += 2;
if (tag_name == 'script') comment_multi = !comment_multi;
return { code };
}
};
}
export default {
preprocess: [
get_processor('script', 'replace_me_script', 'done_replace_script_1'),
get_processor('script', 'done_replace_script_1', 'done_replace_script_2'),
get_processor('style', '.replace_me_style', '.done_replace_style_1'),
get_processor('style', '.done_replace_style_1', '.done_replace_style_2')
]
};

@ -0,0 +1,11 @@
<style>
.replace_me_style {
color: red;
}
</style>
<script>
let
replace_me_script = 'hello'
;
</script>
<h1 class="done_replace_style_2">{done_replace_script_2}</h1>

@ -0,0 +1,45 @@
import * as assert from 'assert';
const get_line_column = obj => ({ line: obj.line, column: obj.column });
export function test({ input, css, js }) {
let out_obj, loc_output, actual, loc_input, expected;
out_obj = js;
// we need the second occurence of 'done_replace_script_2' in output.js
// the first occurence is mapped back to markup '{done_replace_script_2}'
loc_output = out_obj.locate_1('done_replace_script_2');
loc_output = out_obj.locate_1('done_replace_script_2', loc_output.character + 1);
actual = out_obj.mapConsumer.originalPositionFor(loc_output);
loc_input = input.locate_1('replace_me_script');
expected = {
source: 'input.svelte',
name: 'replace_me_script',
...get_line_column(loc_input)
};
assert.deepEqual(actual, expected);
out_obj = css;
loc_output = out_obj.locate_1('.done_replace_style_2');
actual = out_obj.mapConsumer.originalPositionFor(loc_output);
loc_input = input.locate_1('.replace_me_style');
expected = {
source: 'input.svelte',
name: '.replace_me_style',
...get_line_column(loc_input)
};
assert.deepEqual(actual, expected);
assert.equal(
js.code.indexOf('\n/*# sourceMappingURL=data:application/json;base64,'),
-1,
'magic-comment attachments were NOT removed'
);
assert.equal(
css.code.indexOf('\n/*# sourceMappingURL=data:application/json;base64,'),
-1,
'magic-comment attachments were NOT removed'
);
}

@ -101,4 +101,35 @@ describe('validate', () => {
assert.deepEqual(warnings, []); assert.deepEqual(warnings, []);
}); });
it('errors if namespace is provided but unrecognised', () => {
assert.throws(() => {
svelte.compile('<div></div>', {
name: 'test',
namespace: 'svefefe'
});
}, /Invalid namespace 'svefefe'/);
});
it('errors with a hint if namespace is provided but unrecognised but close', () => {
assert.throws(() => {
svelte.compile('<div></div>', {
name: 'test',
namespace: 'foriegn'
});
}, /Invalid namespace 'foriegn' \(did you mean 'foreign'\?\)/);
});
it('does not throw error if \'this\' is bound for foreign element', () => {
assert.doesNotThrow(() => {
svelte.compile(`
<script>
let whatever;
</script>
<div bind:this={whatever} />`, {
name: 'test',
namespace: 'foreign'
});
});
});
}); });

@ -0,0 +1,7 @@
<svelte:options namespace="foreign" />
<page>
<a>not actually a link</a>
<label>This isn't a html label</label>
<figure>This is maybe a QT figure</figure>
</page>

@ -0,0 +1,15 @@
[{
"code": "invalid-binding",
"message": "'value' is not a valid binding. Foreign elements only support bind:this",
"pos": 81,
"start": {
"line": 6,
"column": 7,
"character": 81
},
"end": {
"line": 6,
"column": 28,
"character": 102
}
}]

@ -0,0 +1,6 @@
<svelte:options namespace="foreign" />
<script>
let whatever;
</script>
<input bind:value={whatever} />

@ -0,0 +1,6 @@
<script context="module">
let foo;
</script>
<script>
$: bar = foo;
</script>

@ -0,0 +1,17 @@
[
{
"code": "module-script-reactive-declaration",
"message": "\"foo\" is declared in a module script and will not be reactive",
"pos": 65,
"start": {
"character": 65,
"column": 10,
"line": 5
},
"end": {
"character": 68,
"column": 13,
"line": 5
}
}
]
Loading…
Cancel
Save