Merge branch 'master' into @feature/compile-error-mode

pull/6194/head
Simon H 5 years ago committed by GitHub
commit f4598e5463
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,14 +2,18 @@
## Unreleased
* Expose `svelte/ssr` which exported lifecycle methods as noop ([#6416](https://github.com/sveltejs/svelte/pull/6416))
* Add `trusted` event modifier ([#6137](https://github.com/sveltejs/svelte/issues/6137))
* Fix erroneous `unknown prop` warning when using slot on a component ([#6065](https://github.com/sveltejs/svelte/pull/6065))
* Throw compiler error when passing empty directive names ([#6299](https://github.com/sveltejs/svelte/issues/6299))
* New a11y warning `a11y-mouse-events-have-key-events` which checks that `mouseover`/`mouseout` are accompanied by `focus`/`blur` event handlers ([5938](https://github.com/sveltejs/svelte/pull/5938))
* Fix `preserveComments` in SSR mode ([4730](https://github.com/sveltejs/svelte/issues/4730))
* Add `varsReport` compiler option ([#6192](https://github.com/sveltejs/svelte/pull/6192))
* Expose `svelte/ssr` which exported lifecycle methods as noop ([#6416](https://github.com/sveltejs/svelte/pull/6416))
* Throw compiler error when passing empty directive names ([#6299](https://github.com/sveltejs/svelte/issues/6299))
* Update `periscopic` to allow for export of anonymous function or class ([#3275](https://github.com/sveltejs/svelte/issues/3275))
* Fix `preserveComments` in SSR mode ([#4730](https://github.com/sveltejs/svelte/issues/4730))
* Fix compiler error when using `:where()` inside `:global()` ([#6434](https://github.com/sveltejs/svelte/issues/6434))
* Fix erroneous `unknown prop` warning when using slot on a component ([#6065](https://github.com/sveltejs/svelte/pull/6065))
* Fix :global() with pseudo element should be considered as global ([#6470](https://github.com/sveltejs/svelte/pull/6470))
* New a11y warning `a11y-mouse-events-have-key-events` which checks that `mouseover`/`mouseout` are accompanied by `focus`/`blur` event handlers ([#5938](https://github.com/sveltejs/svelte/pull/5938))
* Remove deprecated `a11y-no-onchange warning` ([#6457](https://github.com/sveltejs/svelte/issues/6457))
* Remove `a11y-media-has-caption` from `audio` elements ([#6054](https://github.com/sveltejs/svelte/issues/6054))
## 3.38.3

@ -9,6 +9,7 @@
"compiler.*",
"register.js",
"index.*",
"ssr.*",
"internal",
"store",
"animate",

@ -54,9 +54,9 @@ To build the website, run `npm run build`. The output can be found in `__sapper_
Tests can be run using `npm run test`.
## Linking `@sveltejs/site-kit` and `@sveltejs/site-repl`
## Linking `@sveltejs/site-kit` and `@sveltejs/svelte-repl`
This site depends on `@sveltejs/site-kit`, a collection of styles, components and icons used in common by *.svelte.dev websites, and `@sveltejs/site-repl`.
This site depends on `@sveltejs/site-kit` (a collection of styles, components and icons used in common by *.svelte.dev websites), and `@sveltejs/svelte-repl`.
In order to work on features that depend on those packages, you need to [link](https://docs.npmjs.com/cli/link) their repositories:
@ -70,7 +70,7 @@ In order to work on features that depend on those packages, you need to [link](h
- `cd <svelte-repo>/site`
- `npm link @sveltejs/site-kit`
- `npm link @sveltejs/svelte-repl`
## Translating the API docs

@ -0,0 +1,65 @@
---
title: What's new in Svelte: July 2021
description: Keeping cool with fixes, TypeScript tooling and tonnes of new features
author: Daniel Sandoval
authorURL: https://desandoval.net
---
As the northern hemisphere heats up, Svelte has stayed cool with lots of performance and bug fixes, better TypeScript support, and lots of new components & tools from around the ecosystem. Let's take a peek 👀
## New in SvelteKit
- `adapter-node` now precompresses assets using gzip & brotli ([#1693](https://github.com/sveltejs/kit/pull/1693))
- Support for TypeScript transpilation has been added to the `svelte-kit package` tooling ([#1633](https://github.com/sveltejs/kit/pull/1633))
- Improved caching defaults in `adapter-node` ([#1416](https://github.com/sveltejs/kit/pull/1416))
- Allow configuring Rollup output options ([#1572](https://github.com/sveltejs/kit/pull/1572))
- Fixed usage of SSL with HMR ([#1517](https://github.com/sveltejs/kit/pull/1517))
## Features & bug fixes from around svelte/*
- [Svelte 3.38.3](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md#3383) (released June 22) includes a bunch of performance and bug fixes - including hydration optimizations, `this` preservation in bubbled events, and more!
- The latest language tools releases added support for prop renaming from outside of a component, PostCSS syntax grammar, and a `.d.ts` output target in `svelte2tsx` which can be used to create type definitions from Svelte files.
- Also in language tools, some long-awaited experimental features for enhanced TypeScript support were added - including explicitly typing all possible component events or slots, and using generics. Have a look at [the RFC](https://github.com/sveltejs/rfcs/pull/38) for more details and leave feedback in [this issue](https://github.com/sveltejs/language-tools/issues/442) if you are using it.
- `svelte-scroller` got some quality-of-life fixes in 2.0.7 - fixing an initial width bug and updating its `index` more conservatively
## Coming soon to Svelte
- Constants in markup ([RFC](https://github.com/sveltejs/rfcs/blob/master/text/0000-markup-constants.md)): Adds a new `{@const ...}` tag that defines a local constant ([PR](https://github.com/sveltejs/svelte/pull/6413))
---
## Community Showcase
**Apps & Sites**
- [SvelteThemes](https://sveltethemes.dev/) is a curated list of Svelte themes and templates built using svelte, sveltekit, elderjs, routify etc.
- [Beatbump](https://github.com/snuffyDev/Beatbump) is an alternative frontend for YouTube Music created using Svelte/SvelteKit.
- [Sveltuir](https://github.com/webspaceadam/sveltuir) is an app help you memorize the guitar fretboard
**Educational Content**
- [Svelte Radio: A Jolly Good Svelte Summer](https://share.transistor.fm/s/60880542) is a conversation about what's new in Svelte and a celebration of Svelte Radio's 1-year anniversary
- [Class properties in Svelte](https://navillus.dev/blog/svelte-class-props) is a refresher on the power of `class` for developers switching over to Svelte from React
- [Sveltekit Tutorial for Beginners](https://www.youtube.com/playlist?list=PLm_Qt4aKpfKjf77S8UD79Ockhwp_699Ms) is a video playlist for learning SvelteKit by WebJeda
- [How To Cache Dynamic Pages On Demand With A Service Worker In SvelteKit](https://jochemvogel.medium.com/how-to-cache-dynamic-pages-on-demand-with-a-service-worker-in-sveltekit-4b4a7652583d) walks through the power of service workers when used within SvelteKit for on-demand caching
- [Vue vs Svelte: Comparing Framework Internals](https://www.vuemastery.com/blog/vue-vs-svelte-comparing-framework-internals/) dives deep into the differences between Vue and Svelte from the inside out
- [Setting up a development environment for SvelteKit with Docker and Docker Compose](https://jenyus.web.app/blog/2021-05-30-setting-up-a-development-environment-for-sveltekit-with-docker-and-compose) walks through how to use Docker to create reusable development environments, no matter what kind of device you run your code on
- Scalable Scripts released three videos this month documenting how to deploy dockerized Svelte Apps to [AWS](https://youtu.be/VOs2Od5jYOc), [Azure](https://youtu.be/gdg4ne_uDm8) and [Google Cloud](https://youtu.be/_-uBb61Tikw)
- [Render Katex with Svelte from zero to hero](https://www.youtube.com/watch?v=euowJs9CblA) demonstrates how to implement Katex in a Svelte project
- [Using Custom Elements in Svelte](https://css-tricks.com/using-custom-elements-in-svelte/) shows some of the quirks to look out for when using custom elements in a Svelte site
**Libraries, Tools & Components**
- [svelte-pipeline](https://github.com/novacbn/svelte-pipeline) provides custom Javascript contexts and the Svelte Compiler as Svelte Stores, for REPLs, Editors, etc.
- [Sveltotron](https://github.com/Salemmous/sveltotron) is an Electron-based app made to inspect your Svelte app
- [svelte-qr-reader-writer](https://github.com/pleasemarkdarkly/svelte-qr-reader-writer) is a Svelte component that helps read and write data from QR codes
- [svelte-stack-router](https://www.npmjs.com/package/svelte-stack-router) Aims to make Svelte apps feel more native by routing with Stacks
- [svelte-typed-context](https://www.npmjs.com/package/svelte-typed-context) provides an interface which, when provided to `getContext` or `setContext`, allows for stricter types
- [svelte-modals](https://svelte-modals.mattjennings.io/) is a simple, flexible, zero-dependency modal manager for Svelte
**Want to contribute a component? Interested in helping make Svelte's presence on the web better?** Submit a Component to the Svelte Society site by making [a PR to this file](https://github.com/svelte-society/sveltesociety-2021/blob/main/src/routes/components/components.json) or check out [the list of open issues](https://github.com/svelte-society/sveltesociety-2021/issues) if you'd like to contribute to the Svelte Society rewrite in SvelteKit.
## See you next month!
Want more updates? Join us on [Reddit](https://www.reddit.com/r/sveltejs/) or [Discord](https://discord.com/invite/yy75DKs)!

@ -2,15 +2,15 @@
title: Accessibility warnings
---
Accessibility (shortened to a11y) isn't always easy to get right, but Svelte will help by warning you if you write inaccessible markup.
Accessibility (shortened to a11y) isn't always easy to get right, but Svelte will help by warning you at compile time if you write inaccessible markup. However, keep in mind that many accessibility issues can only be identified at runtime using other automated tools and by manually testing your application.
Here is a list of accessibility checks Svelte will do it for you.
Here is a list of accessibility checks Svelte will do for you.
---
### `a11y-accesskey`
Enforce no `accesskey` on element. Access keys are HTML attributes that allow web developers to assign keyboard shortcuts to elements. Inconsistencies between keyboard shortcuts and keyboard commands used by screenreader and keyboard only users create accessibility complications so to avoid complications, access keys should not be used.
Enforce no `accesskey` on element. Access keys are HTML attributes that allow web developers to assign keyboard shortcuts to elements. Inconsistencies between keyboard shortcuts and keyboard commands used by screen reader and keyboard-only users create accessibility complications. To avoid complications, access keys should not be used.
```sv
<!-- A11y: Avoid using accesskey -->
@ -32,7 +32,7 @@ Certain reserved DOM elements do not support ARIA roles, states and properties.
### `a11y-autofocus`
Enforce that `autofocus` is not used on elements. Autofocusing elements can cause usability issues for sighted and non-sighted users, alike.
Enforce that `autofocus` is not used on elements. Autofocusing elements can cause usability issues for sighted and non-sighted users alike.
```sv
<!-- A11y: Avoid using autofocus -->
@ -56,32 +56,32 @@ The following elements are visually distracting: `<marquee>` and `<blink>`.
### `a11y-hidden`
Certain DOM elements are useful for screen readers navigation and should not be hidden.
Certain DOM elements are useful for screen reader navigation and should not be hidden.
```sv
<!-- A11y: <h2> element should not be hidden -->
<h2 aria-hidden>invisible header</h2>
<h2 aria-hidden="true">invisible header</h2>
```
---
### `a11y-img-redundant-alt`
Enforce img alt attribute does not contain the word image, picture, or photo. Screenreaders already announce `img` elements as an image. There is no need to use words such as _image_, _photo_, and/or _picture_.
Enforce img alt attribute does not contain the word image, picture, or photo. Screen readers already announce `img` elements as an image. There is no need to use words such as _image_, _photo_, and/or _picture_.
```sv
<img src="foo" alt="Foo eating a sandwich." />
<!-- aria-hidden, won't be announced by screen reader -->
<img src="bar" aria-hidden alt="Picture of me taking a photo of an image" />
<img src="bar" aria-hidden="true" alt="Picture of me taking a photo of an image" />
<!-- A11y: Screenreaders already announce <img> elements as an image. -->
<!-- A11y: Screen readers already announce <img> elements as an image. -->
<img src="foo" alt="Photo of foo being weird." />
<!-- A11y: Screenreaders already announce <img> elements as an image. -->
<!-- A11y: Screen readers already announce <img> elements as an image. -->
<img src="bar" alt="Image of me at a bar!" />
<!-- A11y: Screenreaders already announce <img> elements as an image. -->
<!-- A11y: Screen readers already announce <img> elements as an image. -->
<img src="foo" alt="Picture of baz fixing a bug." />
```
@ -89,7 +89,7 @@ Enforce img alt attribute does not contain the word image, picture, or photo. Sc
### `a11y-invalid-attribute`
Enforce that accessibility attribute should have valid value
Enforce that attributes important for accessibility have a valid value. For example, `href` should not be empty, `'#'`, or `javascript:`.
```sv
<!-- A11y: '' is not a valid href attribute -->
@ -105,7 +105,7 @@ Enforce that a label tag has a text label and an associated control.
There are two supported ways to associate a label with a control:
- Wrapping a control in a label tag.
- Adding `for` to a label and assigning it a DOM ID string that indicates an input on the page.
- Adding `for` to a label and assigning it the ID of an input on the page.
```sv
<label for="id">B</label>
@ -151,18 +151,26 @@ Certain reserved DOM elements do not support ARIA roles, states and properties.
### `a11y-misplaced-scope`
The scope scope should be used only on `<th>` elements.
The scope attribute should only be used on `<th>` elements.
```sv
<!-- A11y: The scope attribute should only be used with <th> elements -->
<div scope/>
<div scope="row" />
```
---
### `a11y-missing-attribute`
Enforce that element should have required accessibility attribute
Enforce that attributes required for accessibility are present on an element. This includes the following checks:
- `<a>` should have an href (unless it's a [fragment-defining tag](https://github.com/sveltejs/svelte/issues/4697))
- `<area>` should have alt, aria-label, or aria-labelledby
- `<html>` should have lang
- `<iframe>` should have title
- `<img>` should have alt
- `<object>` should have title, aria-label, or aria-labelledby
- `<input type="image">` should have alt, aria-label, or aria-labelledby
```sv
<!-- A11y: <input type=\"image\"> element should have an alt, aria-label or aria-labelledby attribute -->
@ -191,9 +199,23 @@ Enforce that heading elements (`h1`, `h2`, etc.) and anchors have content and th
---
### `a11y-mouse-events-have-key-events`
Enforce that `on:mouseover` and `on:mouseout` are accompanied by `on:focus` and `on:blur`, respectively. This helps to ensure that any functionality triggered by these mouse events is also accessible to keyboard users.
```sv
<!-- A11y: on:mouseover must be accompanied by on:focus -->
<div on:mouseover={handleMouseover} />
<!-- A11y: on:mouseout must be accompanied by on:blur -->
<div on:mouseout={handleMouseout} />
```
---
### `a11y-positive-tabindex`
Avoid positive `tabIndex` property values to synchronize the flow of the page with keyboard tab order.
Avoid positive `tabindex` property values. This will move elements out of the expected tab order, creating a confusing experience for keyboard users.
```sv
<!-- A11y: avoid tabindex values above zero -->
@ -204,7 +226,7 @@ Avoid positive `tabIndex` property values to synchronize the flow of the page wi
### `a11y-structure`
Warns when accessibility related elements are not in a right structure.
Enforce that certain DOM elements have the correct structure.
```sv
<!-- A11y: <figcaption> must be an immediate child of <figure> -->
@ -217,7 +239,7 @@ Warns when accessibility related elements are not in a right structure.
### `a11y-unknown-aria-attribute`
Invalid aria attribute. Enforces valid `aria-*` property based on [WAI-ARIA States and Properties spec](https://www.w3.org/WAI/PF/aria-1.1/states_and_properties)
Enforce that only known ARIA attributes are used. This is based on the [WAI-ARIA States and Properties spec](https://www.w3.org/WAI/PF/aria-1.1/states_and_properties).
```sv
<!-- A11y: Unknown aria attribute 'aria-labeledby' (did you mean 'labelledby'?) -->

@ -7,37 +7,35 @@
let showControls = true;
let showControlsTimeout;
function handleMousemove(e) {
// Used to track time of last mouse down event
let lastMouseDown;
function handleMove(e) {
// Make the controls visible, but fade out after
// 2.5 seconds of inactivity
clearTimeout(showControlsTimeout);
showControlsTimeout = setTimeout(() => showControls = false, 2500);
showControls = true;
if (!(e.buttons & 1)) return; // mouse not down
if (!duration) return; // video not loaded yet
if (e.type !== 'touchmove' && !(e.buttons & 1)) return; // mouse not down
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
const { left, right } = this.getBoundingClientRect();
time = duration * (e.clientX - left) / (right - left);
time = duration * (clientX - left) / (right - left);
}
// we can't rely on the built-in click event, because it fires
// after a drag — we have to listen for clicks ourselves
function handleMousedown(e) {
// we can't rely on the built-in click event, because it fires
// after a drag — we have to listen for clicks ourselves
lastMouseDown = new Date();
}
function handleMouseup() {
function handleMouseup(e) {
if (new Date() - lastMouseDown < 300) {
if (paused) e.target.play();
else e.target.pause();
cancel();
}
function cancel() {
e.target.removeEventListener('mouseup', handleMouseup);
}
e.target.addEventListener('mouseup', handleMouseup);
setTimeout(cancel, 200);
}
function format(seconds) {
@ -58,8 +56,10 @@
<video
poster="https://sveltejs.github.io/assets/caminandes-llamigos.jpg"
src="https://sveltejs.github.io/assets/caminandes-llamigos.mp4"
on:mousemove={handleMousemove}
on:mousemove={handleMove}
on:touchmove|preventDefault={handleMove}
on:mousedown={handleMousedown}
on:mouseup={handleMouseup}
bind:currentTime={time}
bind:duration
bind:paused>
@ -128,4 +128,4 @@
video {
width: 100%;
}
</style>
</style>

@ -1,6 +1,7 @@
<script>
import File from './File.svelte';
import {slide} from 'svelte/transition'
export let expanded = false;
export let name;
export let files;
@ -13,7 +14,7 @@
<span class:expanded on:click={toggle}>{name}</span>
{#if expanded}
<ul>
<ul transition:slide={{duration:300}}>
{#each files as file}
<li>
{#if file.type === 'folder'}
@ -49,4 +50,4 @@
li {
padding: 0.2em 0;
}
</style>
</style>

@ -1,11 +1,11 @@
<script>
let count = 0;
function handleClick() {
function incrementCount() {
// event handler code goes here
}
</script>
<button>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
</button>

@ -7,37 +7,35 @@
let showControls = true;
let showControlsTimeout;
function handleMousemove(e) {
// Used to track time of last mouse down event
let lastMouseDown;
function handleMove(e) {
// Make the controls visible, but fade out after
// 2.5 seconds of inactivity
clearTimeout(showControlsTimeout);
showControlsTimeout = setTimeout(() => showControls = false, 2500);
showControls = true;
if (!(e.buttons & 1)) return; // mouse not down
if (!duration) return; // video not loaded yet
if (e.type !== 'touchmove' && !(e.buttons & 1)) return; // mouse not down
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
const { left, right } = this.getBoundingClientRect();
time = duration * (e.clientX - left) / (right - left);
time = duration * (clientX - left) / (right - left);
}
// we can't rely on the built-in click event, because it fires
// after a drag — we have to listen for clicks ourselves
function handleMousedown(e) {
// we can't rely on the built-in click event, because it fires
// after a drag — we have to listen for clicks ourselves
lastMouseDown = new Date();
}
function handleMouseup() {
function handleMouseup(e) {
if (new Date() - lastMouseDown < 300) {
if (paused) e.target.play();
else e.target.pause();
cancel();
}
function cancel() {
e.target.removeEventListener('mouseup', handleMouseup);
}
e.target.addEventListener('mouseup', handleMouseup);
setTimeout(cancel, 200);
}
function format(seconds) {
@ -58,8 +56,10 @@
<video
poster="https://sveltejs.github.io/assets/caminandes-llamigos.jpg"
src="https://sveltejs.github.io/assets/caminandes-llamigos.mp4"
on:mousemove={handleMousemove}
on:mousedown={handleMousedown}>
on:mousemove={handleMove}
on:touchmove|preventDefault={handleMove}
on:mousedown={handleMousedown}
on:mouseup={handleMouseup}>
<track kind="captions">
</video>
@ -125,4 +125,4 @@
video {
width: 100%;
}
</style>
</style>

@ -7,37 +7,35 @@
let showControls = true;
let showControlsTimeout;
function handleMousemove(e) {
// Used to track time of last mouse down event
let lastMouseDown;
function handleMove(e) {
// Make the controls visible, but fade out after
// 2.5 seconds of inactivity
clearTimeout(showControlsTimeout);
showControlsTimeout = setTimeout(() => showControls = false, 2500);
showControls = true;
if (!(e.buttons & 1)) return; // mouse not down
if (!duration) return; // video not loaded yet
if (e.type !== 'touchmove' && !(e.buttons & 1)) return; // mouse not down
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
const { left, right } = this.getBoundingClientRect();
time = duration * (e.clientX - left) / (right - left);
time = duration * (clientX - left) / (right - left);
}
// we can't rely on the built-in click event, because it fires
// after a drag — we have to listen for clicks ourselves
function handleMousedown(e) {
// we can't rely on the built-in click event, because it fires
// after a drag — we have to listen for clicks ourselves
lastMouseDown = new Date();
}
function handleMouseup() {
function handleMouseup(e) {
if (new Date() - lastMouseDown < 300) {
if (paused) e.target.play();
else e.target.pause();
cancel();
}
function cancel() {
e.target.removeEventListener('mouseup', handleMouseup);
}
e.target.addEventListener('mouseup', handleMouseup);
setTimeout(cancel, 200);
}
function format(seconds) {
@ -58,8 +56,10 @@
<video
poster="https://sveltejs.github.io/assets/caminandes-llamigos.jpg"
src="https://sveltejs.github.io/assets/caminandes-llamigos.mp4"
on:mousemove={handleMousemove}
on:mousemove={handleMove}
on:touchmove|preventDefault={handleMove}
on:mousedown={handleMousedown}
on:mouseup={handleMouseup}
bind:currentTime={time}
bind:duration
bind:paused>
@ -128,4 +128,4 @@
video {
width: 100%;
}
</style>
</style>

@ -10,12 +10,15 @@ On line 58, add `currentTime={time}`, `duration` and `paused` bindings:
<video
poster="https://sveltejs.github.io/assets/caminandes-llamigos.jpg"
src="https://sveltejs.github.io/assets/caminandes-llamigos.mp4"
on:mousemove={handleMousemove}
on:mousemove={handleMove}
on:touchmove|preventDefault={handleMove}
on:mousedown={handleMousedown}
on:mouseup={handleMouseup}
bind:currentTime={time}
bind:duration
bind:paused
></video>
bind:paused>
<track kind="captions">
</video>
```
> `bind:duration` is equivalent to `bind:duration={duration}`

@ -11,6 +11,7 @@ const unsubscribe = count.subscribe(value => {
count_value = value;
});
```
> Calling a `subscribe` method returns an `unsubscribe` function.
You now declared `unsubscribe`, but it still needs to be called, for example through the `onDestroy` [lifecycle hook](tutorial/ondestroy):

@ -1427,9 +1427,9 @@
}
},
"@sveltejs/site-kit": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@sveltejs/site-kit/-/site-kit-1.3.0.tgz",
"integrity": "sha512-xGZiB0t7LXiThffL3LH5Kg+zjxrykBR1SnXw07gVM+FgbTngmCEOho7SQm+fPq2n9mU4XnisoOkl9KCxMSM9hg==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@sveltejs/site-kit/-/site-kit-1.4.0.tgz",
"integrity": "sha512-QoIo8KC6G9gInwFyk4+KCzVOiLNdj2aw7T+wGDED+A/SuvICSCyS7W/Ft0WcoAGyQGtgIFRVCcXacubduzA7bg==",
"dev": true,
"requires": {
"@sindresorhus/slugify": "^0.9.1",
@ -3651,9 +3651,9 @@
}
},
"svelte": {
"version": "3.31.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.31.0.tgz",
"integrity": "sha512-r+n8UJkDqoQm1b+3tA3Lh6mHXKpcfOSOuEuIo5gE2W9wQYi64RYX/qE6CZBDDsP/H4M+N426JwY7XGH4xASvGQ==",
"version": "3.37.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.37.0.tgz",
"integrity": "sha512-TRF30F4W4+d+Jr2KzUUL1j8Mrpns/WM/WacxYlo5MMb2E5Qy2Pk1Guj6GylxsW9OnKQl1tnF8q3hG/hQ3h6VUA==",
"dev": true
},
"svelte-json-tree": {

@ -41,7 +41,7 @@
"@rollup/plugin-node-resolve": "^11.0.0",
"@rollup/plugin-replace": "^2.2.0",
"@sindresorhus/slugify": "^0.9.1",
"@sveltejs/site-kit": "^1.3.0",
"@sveltejs/site-kit": "^1.4.0",
"@sveltejs/svelte-repl": "^0.2.1",
"degit": "^2.1.4",
"dotenv": "^8.1.0",
@ -56,7 +56,7 @@
"rollup-plugin-terser": "^7.0.0",
"sapper": "^0.28.10",
"shelljs": "^0.8.3",
"svelte": "^3.12.0"
"svelte": "^3.37.0"
},
"engines": {
"node": ">=10.0.0"

@ -34,6 +34,8 @@ import { apply_preprocessor_sourcemap } from '../utils/mapped_code';
import Element from './nodes/Element';
import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types';
import { clone } from '../utils/clone';
import compiler_warnings from './compiler_warnings';
import compiler_errors from './compiler_errors';
interface ComponentOptions {
namespace?: string;
@ -161,10 +163,7 @@ export default class Component {
const svelteOptions = ast.html.children.find(
child => child.name === 'svelte:options'
) || { start: 0, end: 0 };
this.warn(svelteOptions, {
code: 'custom-element-no-tag',
message: 'No custom element \'tag\' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag="my-thing"/>. To hide this warning, use <svelte:options tag={null}/>'
});
this.warn(svelteOptions, compiler_warnings.custom_element_no_tag);
}
this.tag = this.component_options.tag || compile_options.tag;
} else {
@ -482,18 +481,12 @@ export default class Component {
extract_exports(node) {
if (node.type === 'ExportDefaultDeclaration') {
return this.error(node, {
code: 'default-export',
message: 'A component cannot have a default export'
});
return this.error(node, compiler_errors.default_export);
}
if (node.type === 'ExportNamedDeclaration') {
if (node.source) {
return this.error(node, {
code: 'not-implemented',
message: 'A component currently cannot have an export ... from'
});
return this.error(node, compiler_errors.not_implemented);
}
if (node.declaration) {
if (node.declaration.type === 'VariableDeclaration') {
@ -502,10 +495,7 @@ export default class Component {
const variable = this.var_lookup.get(name);
variable.export_name = name;
if (variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) {
this.warn(declarator, {
code: 'unused-export-let',
message: `${this.name.name} has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``
});
this.warn(declarator, compiler_warnings.unused_export_let(this.name.name, name));
}
});
});
@ -525,10 +515,7 @@ export default class Component {
variable.export_name = specifier.exported.name;
if (variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) {
this.warn(specifier, {
code: 'unused-export-let',
message: `${this.name.name} has unused export property '${specifier.exported.name}'. If it is for external reference only, please consider using \`export const ${specifier.exported.name}\``
});
this.warn(specifier, compiler_warnings.unused_export_let(this.name.name, specifier.exported.name));
}
}
});
@ -559,10 +546,7 @@ export default class Component {
walk(script.content, {
enter(node: Node) {
if (node.type === 'LabeledStatement' && node.label.name === '$') {
component.warn(node as any, {
code: 'module-script-reactive-declaration',
message: '$: has no effect in a module script'
});
component.warn(node as any, compiler_warnings.module_script_reactive_declaration);
}
}
});
@ -572,10 +556,7 @@ export default class Component {
scope.declarations.forEach((node, name) => {
if (name[0] === '$') {
return this.error(node as any, {
code: 'illegal-declaration',
message: 'The $ prefix is reserved, and cannot be used for variable and import names'
});
return this.error(node as any, compiler_errors.illegal_declaration);
}
const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let');
@ -590,10 +571,7 @@ export default class Component {
globals.forEach((node, name) => {
if (name[0] === '$') {
return this.error(node as any, {
code: 'illegal-subscription',
message: 'Cannot reference store value inside <script context="module">'
});
return this.error(node as any, compiler_errors.illegal_subscription);
} else {
this.add_var({
name,
@ -651,10 +629,7 @@ export default class Component {
instance_scope.declarations.forEach((node, name) => {
if (name[0] === '$') {
return this.error(node as any, {
code: 'illegal-declaration',
message: 'The $ prefix is reserved, and cannot be used for variable and import names'
});
return this.error(node as any, compiler_errors.illegal_declaration);
}
const writable = node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let');
@ -688,10 +663,7 @@ export default class Component {
});
} else if (name[0] === '$') {
if (name === '$' || name[1] === '$') {
return this.error(node as any, {
code: 'illegal-global',
message: `${name} is an illegal variable name`
});
return this.error(node as any, compiler_errors.illegal_global(name));
}
this.add_var({
@ -874,10 +846,7 @@ export default class Component {
node.label.name === '$' &&
parent.type !== 'Program'
) {
this.warn(node as any, {
code: 'non-top-level-reactive-declaration',
message: '$: has no effect outside of the top-level'
});
this.warn(node as any, compiler_warnings.non_top_level_reactive_declaration);
}
if (is_reference(node, parent)) {
@ -891,10 +860,7 @@ export default class Component {
if (name[1] !== '$' && scope.has(name.slice(1)) && scope.find_owner(name.slice(1)) !== this.instance_scope) {
if (!((/Function/.test(parent.type) && prop === 'params') || (parent.type === 'VariableDeclarator' && prop === 'id'))) {
return this.error(node as any, {
code: 'contextual-store',
message: 'Stores must be declared at the top level of the component (this may change in a future version of Svelte)'
});
return this.error(node as any, compiler_errors.contextual_store);
}
}
}
@ -959,10 +925,7 @@ export default class Component {
if (variable.export_name) {
// TODO is this still true post-#3539?
return component.error(declarator as any, {
code: 'destructured-prop',
message: 'Cannot declare props in destructured declaration'
});
return component.error(declarator as any, compiler_errors.destructured_prop);
}
if (variable.subscribable) {
@ -1252,10 +1215,7 @@ export default class Component {
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`
});
component.warn(node as any, compiler_warnings.module_script_variable_reactive_declaration(name));
}
}
const is_writable_or_mutated =
@ -1320,10 +1280,7 @@ export default class Component {
if (cycle && cycle.length) {
const declarationList = lookup.get(cycle[0]);
const declaration = declarationList[0];
return this.error(declaration.node, {
code: 'cyclical-reactive-declaration',
message: `Cyclical dependency detected: ${cycle.join(' → ')}`
});
return this.error(declaration.node, compiler_errors.cyclical_reactive_declaration(cycle));
}
const add_declaration = declaration => {
@ -1346,10 +1303,7 @@ export default class Component {
warn_if_undefined(name: string, node, template_scope: TemplateScope) {
if (name[0] === '$') {
if (name === '$' || name[1] === '$' && !is_reserved_keyword(name)) {
return this.error(node, {
code: 'illegal-global',
message: `${name} is an illegal variable name`
});
return this.error(node, compiler_errors.illegal_global(name));
}
this.has_reactive_assignments = true; // TODO does this belong here?
@ -1363,15 +1317,7 @@ export default class Component {
if (template_scope && template_scope.names.has(name)) return;
if (globals.has(name) && node.type !== 'InlineComponent') return;
let message = `'${name}' is not defined`;
if (!this.ast.instance) {
message += `. Consider adding a <script> block with 'export let ${name}' to declare a prop`;
}
this.warn(node, {
code: 'missing-declaration',
message
});
this.warn(node, compiler_warnings.missing_declaration(name, !!this.ast.instance));
}
push_ignores(ignores) {
@ -1399,7 +1345,7 @@ function process_component_options(component: Component, nodes) {
const node = nodes.find(node => node.name === 'svelte:options');
function get_value(attribute, code, message) {
function get_value(attribute, {code, message}) {
const { value } = attribute;
const chunk = value[0];
@ -1425,26 +1371,18 @@ function process_component_options(component: Component, nodes) {
switch (name) {
case 'tag': {
const code = 'invalid-tag-attribute';
const message = "'tag' must be a string literal";
const tag = get_value(attribute, code, message);
const tag = get_value(attribute, compiler_errors.invalid_tag_attribute);
if (typeof tag !== 'string' && tag !== null) {
return component.error(attribute, { code, message });
return component.error(attribute, compiler_errors.invalid_tag_attribute);
}
if (tag && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
return component.error(attribute, {
code: 'invalid-tag-property',
message: "tag name must be two or more words joined by the '-' character"
});
return component.error(attribute, compiler_errors.invalid_tag_property);
}
if (tag && !component.compile_options.customElement) {
component.warn(attribute, {
code: 'missing-custom-element-compile-options',
message: "The 'tag' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?"
});
component.warn(attribute, compiler_warnings.missing_custom_element_compile_options);
}
component_options.tag = tag;
@ -1452,27 +1390,15 @@ function process_component_options(component: Component, nodes) {
}
case 'namespace': {
const code = 'invalid-namespace-attribute';
const message = "The 'namespace' attribute must be a string literal representing a valid namespace";
const ns = get_value(attribute, code, message);
const ns = get_value(attribute, compiler_errors.invalid_namespace_attribute);
if (typeof ns !== 'string') {
return component.error(attribute, { code, message });
return component.error(attribute, compiler_errors.invalid_namespace_attribute);
}
if (valid_namespaces.indexOf(ns) === -1) {
const match = fuzzymatch(ns, valid_namespaces);
if (match) {
return component.error(attribute, {
code: 'invalid-namespace-property',
message: `Invalid namespace '${ns}' (did you mean '${match}'?)`
});
} else {
return component.error(attribute, {
code: 'invalid-namespace-property',
message: `Invalid namespace '${ns}'`
});
}
return component.error(attribute, compiler_errors.invalid_namespace_property(ns, match));
}
component_options.namespace = ns;
@ -1482,12 +1408,10 @@ function process_component_options(component: Component, nodes) {
case 'accessors':
case 'immutable':
case 'preserveWhitespace': {
const code = `invalid-${name}-value`;
const message = `${name} attribute must be true or false`;
const value = get_value(attribute, code, message);
const value = get_value(attribute, compiler_errors.invalid_attribute_value(name));
if (typeof value !== 'boolean') {
return component.error(attribute, { code, message });
return component.error(attribute, compiler_errors.invalid_attribute_value(name));
}
component_options[name] = value;
@ -1495,16 +1419,10 @@ function process_component_options(component: Component, nodes) {
}
default:
return component.error(attribute, {
code: 'invalid-options-attribute',
message: '<svelte:options> unknown attribute'
});
return component.error(attribute, compiler_errors.invalid_options_attribute_unknown);
}
} else {
return component.error(attribute, {
code: 'invalid-options-attribute',
message: "<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes"
});
return component.error(attribute, compiler_errors.invalid_options_attribute);
}
});
}

@ -0,0 +1,253 @@
// All compiler errors should be listed and accessed from here
/**
* @internal
*/
export default {
invalid_binding_elements: (element: string, binding: string) => ({
code: 'invalid-binding',
message: `'${binding}' is not a valid binding on <${element}> elements`
}),
invalid_binding_element_with: (elements: string, binding: string) => ({
code: 'invalid-binding',
message: `'${binding}' binding can only be used with ${elements}`
}),
invalid_binding_on: (binding: string, element: string, post?: string) => ({
code: 'invalid-binding',
message: `'${binding}' is not a valid binding on ${element}` + (post || '')
}),
invalid_binding_foreign: (binding: string) => ({
code: 'invalid-binding',
message: `'${binding}' is not a valid binding. Foreign elements only support bind:this`
}),
invalid_binding_no_checkbox: (binding: string, is_radio: boolean) => ({
code: 'invalid-binding',
message: `'${binding}' binding can only be used with <input type="checkbox">` + (is_radio ? ' — for <input type="radio">, use \'group\' binding' : '')
}),
invalid_binding: (binding: string) => ({
code: 'invalid-binding',
message: `'${binding}' is not a valid binding`
}),
invalid_binding_window: (parts: string[]) => ({
code: 'invalid-binding',
message: `Bindings on <svelte:window> must be to top-level properties, e.g. '${parts[parts.length - 1]}' rather than '${parts.join('.')}'`
}),
invalid_binding_let: {
code: 'invalid-binding',
message: 'Cannot bind to a variable declared with the let: directive'
},
invalid_binding_await: {
code: 'invalid-binding',
message: 'Cannot bind to a variable declared with {#await ... then} or {:catch} blocks'
},
invalid_binding_writibale: {
code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable'
},
binding_undeclared: (name: string) => ({
code: 'binding-undeclared',
message: `${name} is not declared`
}),
invalid_type: {
code: 'invalid-type',
message: '\'type\' attribute cannot be dynamic if input uses two-way binding'
},
missing_type: {
code: 'missing-type',
message: '\'type\' attribute must be specified'
},
dynamic_multiple_attribute: {
code: 'dynamic-multiple-attribute',
message: '\'multiple\' attribute cannot be dynamic if select uses two-way binding'
},
missing_contenteditable_attribute: {
code: 'missing-contenteditable-attribute',
message: '\'contenteditable\' attribute is required for textContent and innerHTML two-way bindings'
},
dynamic_contenteditable_attribute: {
code: 'dynamic-contenteditable-attribute',
message: '\'contenteditable\' attribute cannot be dynamic if element uses two-way binding'
},
invalid_event_modifier_combination: (modifier1: string, modifier2: string) => ({
code: 'invalid-event-modifier',
message: `The '${modifier1}' and '${modifier2}' modifiers cannot be used together`
}),
invalid_event_modifier_legacy: (modifier: string) => ({
code: 'invalid-event-modifier',
message: `The '${modifier}' modifier cannot be used in legacy mode`
}),
invalid_event_modifier: (valid: string) => ({
code: 'invalid-event-modifier',
message: `Valid event modifiers are ${valid}`
}),
invalid_event_modifier_component: {
code: 'invalid-event-modifier',
message: "Event modifiers other than 'once' can only be used on DOM elements"
},
textarea_duplicate_value: {
code: 'textarea-duplicate-value',
message: 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
},
illegal_attribute: (name: string) => ({
code: 'illegal-attribute',
message: `'${name}' is not a valid attribute name`
}),
invalid_slot_attribute: {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
},
duplicate_slot_attribute: (name: string) => ({
code: 'duplicate-slot-attribute',
message: `Duplicate '${name}' slot`
}),
invalid_slotted_content: {
code: 'invalid-slotted-content',
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
},
invalid_attribute_head: {
code: 'invalid-attribute',
message: '<svelte:head> should not have any attributes or directives'
},
invalid_action: {
code: 'invalid-action',
message: 'Actions can only be applied to DOM elements, not components'
},
invalid_class: {
code: 'invalid-class',
message: 'Classes can only be applied to DOM elements, not components'
},
invalid_transition: {
code: 'invalid-transition',
message: 'Transitions can only be applied to DOM elements, not components'
},
invalid_let: {
code: 'invalid-let',
message: 'let directive value must be an identifier or an object/array pattern'
},
invalid_slot_directive: {
code: 'invalid-slot-directive',
message: '<slot> cannot have directives'
},
dynamic_slot_name: {
code: 'dynamic-slot-name',
message: '<slot> name cannot be dynamic'
},
invalid_slot_name: {
code: 'invalid-slot-name',
message: 'default is a reserved word — it cannot be used as a slot name'
},
invalid_slot_attribute_value_missing: {
code: 'invalid-slot-attribute',
message: 'slot attribute value is missing'
},
invalid_slotted_content_fragment: {
code: 'invalid-slotted-content',
message: '<svelte:fragment> must be a child of a component'
},
illegal_attribute_title: {
code: 'illegal-attribute',
message: '<title> cannot have attributes'
},
illegal_structure_title: {
code: 'illegal-structure',
message: '<title> can only contain text and {tags}'
},
duplicate_transition: (directive: string, parent_directive: string) => {
function describe(_directive: string) {
return _directive === 'transition'
? "a 'transition'"
: `an '${_directive}'`;
}
const message = directive === parent_directive
? `An element can only have one '${directive}' directive`
: `An element cannot have both ${describe(parent_directive)} directive and ${describe(directive)} directive`;
return {
code: 'duplicate-transition',
message
};
},
contextual_store: {
code: 'contextual-store',
message: 'Stores must be declared at the top level of the component (this may change in a future version of Svelte)'
},
default_export: {
code: 'default-export',
message: 'A component cannot have a default export'
},
not_implemented: {
code: 'not-implemented',
message: 'A component currently cannot have an export ... from'
},
illegal_declaration: {
code: 'illegal-declaration',
message: 'The $ prefix is reserved, and cannot be used for variable and import names'
},
illegal_subscription: {
code: 'illegal-subscription',
message: 'Cannot reference store value inside <script context="module">'
},
illegal_global: (name: string) => ({
code: 'illegal-global',
message: `${name} is an illegal variable name`
}),
destructured_prop: {
code: 'destructured-prop',
message: 'Cannot declare props in destructured declaration'
},
cyclical_reactive_declaration: (cycle: string[]) => ({
code: 'cyclical-reactive-declaration',
message: `Cyclical dependency detected: ${cycle.join(' → ')}`
}),
invalid_tag_property: {
code: 'invalid-tag-property',
message: "tag name must be two or more words joined by the '-' character"
},
invalid_tag_attribute: {
code: 'invalid-tag-attribute',
message: "'tag' must be a string literal"
},
invalid_namespace_property: (namespace: string, suggestion?: string) => ({
code: 'invalid-namespace-property',
message: `Invalid namespace '${namespace}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '')
}),
invalid_namespace_attribute: {
code: 'invalid-namespace-attribute',
message: "The 'namespace' attribute must be a string literal representing a valid namespace"
},
invalid_attribute_value: (name: string) => ({
code: `invalid-${name}-value`,
message: `${name} attribute must be true or false`
}),
invalid_options_attribute_unknown: {
code: 'invalid-options-attribute',
message: '<svelte:options> unknown attribute'
},
invalid_options_attribute: {
code: 'invalid-options-attribute',
message: "<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes"
},
css_invalid_global: {
code: 'css-invalid-global',
message: ':global(...) can be at the start or end of a selector sequence, but not in the middle'
},
css_invalid_global_selector: {
code: 'css-invalid-global-selector',
message: ':global(...) must contain a single selector'
},
duplicate_animation: {
code: 'duplicate-animation',
message: "An element can only have one 'animate' directive"
},
invalid_animation_immediate: {
code: 'invalid-animation',
message: 'An element that uses the animate directive must be the immediate child of a keyed each block'
},
invalid_animation_sole: {
code: 'invalid-animation',
message: 'An element that uses the animate directive must be the sole child of a keyed each block'
},
invalid_directive_value: {
code: 'invalid-directive-value',
message: 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
}
};

@ -0,0 +1,143 @@
// All compiler warnings should be listed and accessed from here
/**
* @internal
*/
export default {
custom_element_no_tag: {
code: 'custom-element-no-tag',
message: 'No custom element \'tag\' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag="my-thing"/>. To hide this warning, use <svelte:options tag={null}/>'
},
unused_export_let: (component: string, property: string) => ({
code: 'unused-export-let',
message: `${component} has unused export property '${property}'. If it is for external reference only, please consider using \`export const ${property}\``
}),
module_script_reactive_declaration: {
code: 'module-script-reactive-declaration',
message: '$: has no effect in a module script'
},
non_top_level_reactive_declaration: {
code: 'non-top-level-reactive-declaration',
message: '$: has no effect outside of the top-level'
},
module_script_variable_reactive_declaration: (name: string) => ({
code: 'module-script-reactive-declaration',
message: `"${name}" is declared in a module script and will not be reactive`
}),
missing_declaration: (name: string, has_script: boolean) => ({
code: 'missing-declaration',
message: `'${name}' is not defined` + (has_script ? '' : `. Consider adding a <script> block with 'export let ${name}' to declare a prop`)
}),
missing_custom_element_compile_options: {
code: 'missing-custom-element-compile-options',
message: "The 'tag' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?"
},
css_unused_selector: (selector: string) => ({
code: 'css-unused-selector',
message: `Unused CSS selector "${selector}"`
}),
empty_block: {
code: 'empty-block',
message: 'Empty block'
},
reactive_component: (name: string) => ({
code: 'reactive-component',
message: `<${name}/> will not be reactive if ${name} changes. Use <svelte:component this={${name}}/> if you want this reactivity.`
}),
component_name_lowercase: (name: string) => ({
code: 'component-name-lowercase',
message: `<${name}> will be treated as an HTML element unless it begins with a capital letter`
}),
avoid_is: {
code: 'avoid-is',
message: 'The \'is\' attribute is not supported cross-browser and should be avoided'
},
invalid_html_attribute: (name: string, suggestion: string) => ({
code: 'invalid-html-attribute',
message: `'${name}' is not a valid HTML attribute. Did you mean '${suggestion}'?`
}),
a11y_aria_attributes: (name: string) => ({
code: 'a11y-aria-attributes',
message: `A11y: <${name}> should not have aria-* attributes`
}),
a11y_unknown_aria_attribute: (attribute: string, suggestion?: string) => ({
code: 'a11y-unknown-aria-attribute',
message: `A11y: Unknown aria attribute 'aria-${attribute}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '')
}),
a11y_hidden: (name: string) => ({
code: 'a11y-hidden',
message: `A11y: <${name}> element should not be hidden`
}),
a11y_misplaced_role: (name: string) => ({
code: 'a11y-misplaced-role',
message: `A11y: <${name}> should not have role attribute`
}),
a11y_unknown_role: (role: string | boolean, suggestion?: string) => ({
code: 'a11y-unknown-role',
message: `A11y: Unknown role '${role}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '')
}),
a11y_accesskey: {
code: 'a11y-accesskey',
message: 'A11y: Avoid using accesskey'
},
a11y_autofocus: {
code: 'a11y-autofocus',
message: 'A11y: Avoid using autofocus'
},
a11y_misplaced_scope: {
code: 'a11y-misplaced-scope',
message: 'A11y: The scope attribute should only be used with <th> elements'
},
a11y_positive_tabindex: {
code: 'a11y-positive-tabindex',
message: 'A11y: avoid tabindex values above zero'
},
a11y_invalid_attribute: (href_attribute: string, href_value: string) => ({
code: 'a11y-invalid-attribute',
message: `A11y: '${href_value}' is not a valid ${href_attribute} attribute`
}),
a11y_missing_attribute: (name: string, article: string, sequence: string) => ({
code: 'a11y-missing-attribute',
message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
}),
a11y_img_redundant_alt: {
code: 'a11y-img-redundant-alt',
message: 'A11y: Screenreaders already announce <img> elements as an image.'
},
a11y_label_has_associated_control: {
code: 'a11y-label-has-associated-control',
message: 'A11y: A form label must be associated with a control.'
},
a11y_media_has_caption: {
code: 'a11y-media-has-caption',
message: 'A11y: <video> elements must have a <track kind="captions">'
},
a11y_distracting_elements: (name: string) => ({
code: 'a11y-distracting-elements',
message: `A11y: Avoid <${name}> elements`
}),
a11y_structure_immediate: {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be an immediate child of <figure>'
},
a11y_structure_first_or_last: {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be first or last child of <figure>'
},
a11y_mouse_events_have_key_events: (event: string, accompanied_by: string) => ({
code: 'a11y-mouse-events-have-key-events',
message: `A11y: on:${event} must be accompanied by on:${accompanied_by}`
}),
a11y_missing_content: (name: string) => ({
code: 'a11y-missing-content',
message: `A11y: <${name}> element should have child content`
}),
redundant_event_modifier_for_touch: {
code: 'redundant-event-modifier',
message: 'Touch event handlers that don\'t use the \'event\' object are passive by default'
},
redundant_event_modifier_passive: {
code: 'redundant-event-modifier',
message: 'The passive modifier only works with wheel and touch events'
}
};

@ -8,6 +8,7 @@ import { INode } from '../nodes/interfaces';
import EachBlock from '../nodes/EachBlock';
import IfBlock from '../nodes/IfBlock';
import AwaitBlock from '../nodes/AwaitBlock';
import compiler_errors from '../compiler_errors';
enum BlockAppliesToNode {
NotPossible,
@ -137,10 +138,7 @@ export default class Selector {
for (let i = start; i < end; i += 1) {
if (this.blocks[i].global) {
return component.error(this.blocks[i].selectors[0], {
code: 'css-invalid-global',
message: ':global(...) can be at the start or end of a selector sequence, but not in the middle'
});
return component.error(this.blocks[i].selectors[0], compiler_errors.css_invalid_global);
}
}
@ -148,10 +146,7 @@ export default class Selector {
for (const selector of block.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
if (/[^\\],(?!([^([]+[^\\]|[^([\\])[)\]])/.test(selector.children[0].value)) {
component.error(selector, {
code: 'css-invalid-global-selector',
message: ':global(...) must contain a single selector'
});
component.error(selector, compiler_errors.css_invalid_global_selector);
}
}
}

@ -6,6 +6,7 @@ import { Ast, CssHashGetter } from '../../interfaces';
import Component from '../Component';
import { CssNode } from './interfaces';
import hash from '../utils/hash';
import compiler_warnings from '../compiler_warnings';
function remove_css_prefix(name: string): string {
return name.replace(/^-((webkit)|(moz)|(o)|(ms))-/, '');
@ -448,10 +449,7 @@ export default class Stylesheet {
warn_on_unused_selectors(component: Component) {
this.children.forEach(child => {
child.warn_on_unused_selector((selector: Selector) => {
component.warn(selector.node, {
code: 'css-unused-selector',
message: `Unused CSS selector "${this.source.slice(selector.node.start, selector.node.end)}"`
});
component.warn(selector.node, compiler_warnings.css_unused_selector(this.source.slice(selector.node.start, selector.node.end)));
});
});
}

@ -5,6 +5,7 @@ import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Element from './Element';
import EachBlock from './EachBlock';
import compiler_errors from '../compiler_errors';
export default class Animation extends Node {
type: 'Animation';
@ -20,21 +21,13 @@ export default class Animation extends Node {
component.add_reference(info.name.split('.')[0]);
if (parent.animation) {
component.error(this, {
code: 'duplicate-animation',
message: "An element can only have one 'animate' directive"
});
return;
return component.error(this, compiler_errors.duplicate_animation);
}
const block = parent.parent;
if (!block || block.type !== 'EachBlock' || !block.key) {
// TODO can we relax the 'immediate child' rule?
component.error(this, {
code: 'invalid-animation',
message: 'An element that uses the animate directive must be the immediate child of a keyed each block'
});
return;
return component.error(this, compiler_errors.invalid_animation_immediate);
}
(block as EachBlock).has_animation = true;

@ -10,6 +10,7 @@ import Element from './Element';
import InlineComponent from './InlineComponent';
import Window from './Window';
import { clone } from '../../utils/clone';
import compiler_errors from '../compiler_errors';
// TODO this should live in a specific binding
const read_only_media_attributes = new Set([
@ -35,11 +36,7 @@ export default class Binding extends Node {
super(component, parent, scope, info);
if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') {
component.error(info, {
code: 'invalid-directive-value',
message: 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
});
return;
return component.error(info, compiler_errors.invalid_directive_value);
}
this.name = info.name;
@ -52,18 +49,10 @@ export default class Binding extends Node {
// make sure we track this as a mutable ref
if (scope.is_let(name)) {
component.error(this, {
code: 'invalid-binding',
message: 'Cannot bind to a variable declared with the let: directive'
});
return;
return component.error(this, compiler_errors.invalid_binding_let);
} else if (scope.names.has(name)) {
if (scope.is_await(name)) {
component.error(this, {
code: 'invalid-binding',
message: 'Cannot bind to a variable declared with {#await ... then} or {:catch} blocks'
});
return;
return component.error(this, compiler_errors.invalid_binding_await);
}
scope.dependencies_for_name.get(name).forEach(name => {
@ -76,21 +65,13 @@ export default class Binding extends Node {
const variable = component.var_lookup.get(name);
if (!variable || variable.global) {
component.error(this.expression.node as any, {
code: 'binding-undeclared',
message: `${name} is not declared`
});
return;
return component.error(this.expression.node as any, compiler_errors.binding_undeclared(name));
}
variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
if (info.expression.type === 'Identifier' && !variable.writable) {
component.error(this.expression.node as any, {
code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable'
});
return;
return component.error(this.expression.node as any, compiler_errors.invalid_binding_writibale);
}
}

@ -8,6 +8,7 @@ import { Context, unpack_destructuring } from './shared/Context';
import { Node } from 'estree';
import Component from '../Component';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
export default class EachBlock extends AbstractBlock {
type: 'EachBlock';
@ -61,11 +62,7 @@ export default class EachBlock extends AbstractBlock {
if (this.has_animation) {
if (this.children.length !== 1) {
const child = this.children.find(child => !!(child as Element).animation);
component.error((child as Element).animation, {
code: 'invalid-animation',
message: 'An element that uses the animate directive must be the sole child of a keyed each block'
});
return;
return component.error((child as Element).animation, compiler_errors.invalid_animation_sole);
}
}

@ -17,6 +17,8 @@ import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import Component from '../Component';
import compiler_warnings from '../compiler_warnings';
import compiler_errors from '../compiler_errors';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|svg|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;
@ -139,11 +141,7 @@ export default class Element extends Node {
if (info.children.length > 0) {
const value_attribute = info.attributes.find(node => node.name === 'value');
if (value_attribute) {
component.error(value_attribute, {
code: 'textarea-duplicate-value',
message: 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
});
return;
return component.error(value_attribute, compiler_errors.textarea_duplicate_value);
}
// this is an egregious hack, but it's the easiest way to get <textarea>
@ -247,10 +245,7 @@ export default class Element extends Node {
validate() {
if (this.component.var_lookup.has(this.name) && this.component.var_lookup.get(this.name).imported) {
this.component.warn(this, {
code: 'component-name-lowercase',
message: `<${this.name}> will be treated as an HTML element unless it begins with a capital letter`
});
this.component.warn(this, compiler_warnings.component_name_lowercase(this.name));
}
this.validate_attributes();
@ -277,35 +272,23 @@ export default class Element extends Node {
// Errors
if (/(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/.test(name)) {
return component.error(attribute, {
code: 'illegal-attribute',
message: `'${name}' is not a valid attribute name`
});
return component.error(attribute, compiler_errors.illegal_attribute(name));
}
if (name === 'slot') {
if (!attribute.is_static) {
return component.error(attribute, {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
});
return component.error(attribute, compiler_errors.invalid_slot_attribute);
}
if (component.slot_outlets.has(name)) {
return component.error(attribute, {
code: 'duplicate-slot-attribute',
message: `Duplicate '${name}' slot`
});
return component.error(attribute, compiler_errors.duplicate_slot_attribute(name));
// this code was unreachable. Still needed?
// component.slot_outlets.add(name);
}
if (!(parent.type === 'SlotTemplate' || within_custom_element(parent))) {
return 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'
});
return component.error(attribute, compiler_errors.invalid_slotted_content);
}
}
@ -313,17 +296,11 @@ export default class Element extends Node {
if (this.namespace !== namespaces.foreign) {
if (name === 'is') {
component.warn(attribute, {
code: 'avoid-is',
message: 'The \'is\' attribute is not supported cross-browser and should be avoided'
});
component.warn(attribute, compiler_warnings.avoid_is);
}
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)}'?`
});
component.warn(attribute, compiler_warnings.invalid_html_attribute(attribute.name, react_attributes.get(attribute.name)));
}
}
});
@ -341,29 +318,17 @@ export default class Element extends Node {
if (name.startsWith('aria-')) {
if (invisible_elements.has(this.name)) {
// aria-unsupported-elements
component.warn(attribute, {
code: 'a11y-aria-attributes',
message: `A11y: <${this.name}> should not have aria-* attributes`
});
component.warn(attribute, compiler_warnings.a11y_aria_attributes(this.name));
}
const type = name.slice(5);
if (!aria_attribute_set.has(type)) {
const match = fuzzymatch(type, aria_attributes);
let message = `A11y: Unknown aria attribute 'aria-${type}'`;
if (match) message += ` (did you mean '${match}'?)`;
component.warn(attribute, {
code: 'a11y-unknown-aria-attribute',
message
});
component.warn(attribute, compiler_warnings.a11y_unknown_aria_attribute(type, match));
}
if (name === 'aria-hidden' && /^h[1-6]$/.test(this.name)) {
component.warn(attribute, {
code: 'a11y-hidden',
message: `A11y: <${this.name}> element should not be hidden`
});
component.warn(attribute, compiler_warnings.a11y_hidden(this.name));
}
}
@ -371,10 +336,7 @@ export default class Element extends Node {
if (name === 'role') {
if (invisible_elements.has(this.name)) {
// aria-unsupported-elements
component.warn(attribute, {
code: 'a11y-misplaced-role',
message: `A11y: <${this.name}> should not have role attribute`
});
component.warn(attribute, compiler_warnings.a11y_misplaced_role(this.name));
}
const value = attribute.get_static_value();
@ -382,38 +344,23 @@ export default class Element extends Node {
if (value && !aria_role_set.has(value)) {
// @ts-ignore
const match = fuzzymatch(value, aria_roles);
let message = `A11y: Unknown role '${value}'`;
if (match) message += ` (did you mean '${match}'?)`;
component.warn(attribute, {
code: 'a11y-unknown-role',
message
});
component.warn(attribute, compiler_warnings.a11y_unknown_role(value, match));
}
}
// no-access-key
if (name === 'accesskey') {
component.warn(attribute, {
code: 'a11y-accesskey',
message: 'A11y: Avoid using accesskey'
});
component.warn(attribute, compiler_warnings.a11y_accesskey);
}
// no-autofocus
if (name === 'autofocus') {
component.warn(attribute, {
code: 'a11y-autofocus',
message: 'A11y: Avoid using autofocus'
});
component.warn(attribute, compiler_warnings.a11y_autofocus);
}
// scope
if (name === 'scope' && this.name !== 'th') {
component.warn(attribute, {
code: 'a11y-misplaced-scope',
message: 'A11y: The scope attribute should only be used with <th> elements'
});
component.warn(attribute, compiler_warnings.a11y_misplaced_scope);
}
// tabindex-no-positive
@ -421,10 +368,7 @@ export default class Element extends Node {
const value = attribute.get_static_value();
// @ts-ignore todo is tabindex=true correct case?
if (!isNaN(value) && +value > 0) {
component.warn(attribute, {
code: 'a11y-positive-tabindex',
message: 'A11y: avoid tabindex values above zero'
});
component.warn(attribute, compiler_warnings.a11y_positive_tabindex);
}
}
});
@ -454,20 +398,14 @@ export default class Element extends Node {
const href_value = href_attribute.get_static_value();
if (href_value === '' || href_value === '#' || /^\W*javascript:/i.test(href_value)) {
component.warn(href_attribute, {
code: 'a11y-invalid-attribute',
message: `A11y: '${href_value}' is not a valid ${href_attribute.name} attribute`
});
component.warn(href_attribute, compiler_warnings.a11y_invalid_attribute(href_attribute.name, href_value));
}
} else {
const id_attribute_valid = id_attribute && id_attribute.get_static_value() !== '';
const name_attribute_valid = name_attribute && name_attribute.get_static_value() !== '';
if (!id_attribute_valid && !name_attribute_valid) {
component.warn(this, {
code: 'a11y-missing-attribute',
message: 'A11y: <a> element should have an href attribute'
});
component.warn(this, compiler_warnings.a11y_missing_attribute('a', 'an', 'href'));
}
}
} else {
@ -503,10 +441,7 @@ export default class Element extends Node {
const alt_value = alt_attribute.get_static_value();
if (/\b(image|picture|photo)\b/i.test(alt_value)) {
component.warn(this, {
code: 'a11y-img-redundant-alt',
message: 'A11y: Screenreaders already announce <img> elements as an image.'
});
component.warn(this, compiler_warnings.a11y_img_redundant_alt);
}
}
}
@ -514,14 +449,11 @@ export default class Element extends Node {
if (this.name === 'label') {
const has_input_child = this.children.some(i => (i instanceof Element && a11y_labelable.has(i.name) ));
if (!attribute_map.has('for') && !has_input_child) {
component.warn(this, {
code: 'a11y-label-has-associated-control',
message: 'A11y: A form label must be associated with a control.'
});
component.warn(this, compiler_warnings.a11y_label_has_associated_control);
}
}
if (this.is_media_node()) {
if (this.name === 'video') {
if (attribute_map.has('muted')) {
return;
}
@ -533,19 +465,13 @@ export default class Element extends Node {
}
if (!has_caption) {
component.warn(this, {
code: 'a11y-media-has-caption',
message: 'A11y: Media elements must have a <track kind="captions">'
});
component.warn(this, compiler_warnings.a11y_media_has_caption);
}
}
if (a11y_distracting_elements.has(this.name)) {
// no-distracting-elements
component.warn(this, {
code: 'a11y-distracting-elements',
message: `A11y: Avoid <${this.name}> elements`
});
component.warn(this, compiler_warnings.a11y_distracting_elements(this.name));
}
if (this.name === 'figcaption') {
@ -564,10 +490,7 @@ export default class Element extends Node {
}
if (!is_figure_parent) {
component.warn(this, {
code: 'a11y-structure',
message: 'A11y: <figcaption> must be an immediate child of <figure>'
});
component.warn(this, compiler_warnings.a11y_structure_immediate);
}
}
@ -581,35 +504,23 @@ export default class Element extends Node {
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>'
});
component.warn(children[index], compiler_warnings.a11y_structure_first_or_last);
}
}
if (handlers_map.has('mouseover') && !handlers_map.has('focus')) {
component.warn(this, {
code: 'a11y-mouse-events-have-key-events',
message: 'A11y: on:mouseover must be accompanied by on:focus'
});
component.warn(this, compiler_warnings.a11y_mouse_events_have_key_events('mouseover', 'focus'));
}
if (handlers_map.has('mouseout') && !handlers_map.has('blur')) {
component.warn(this, {
code: 'a11y-mouse-events-have-key-events',
message: 'A11y: on:mouseout must be accompanied by on:blur'
});
component.warn(this, compiler_warnings.a11y_mouse_events_have_key_events('mouseout', 'blur'));
}
}
validate_bindings_foreign() {
this.bindings.forEach(binding => {
if (binding.name !== 'this') {
return this.component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding. Foreign elements only support bind:this`
});
return this.component.error(binding, compiler_errors.invalid_binding_foreign(binding.name));
}
});
}
@ -625,19 +536,13 @@ export default class Element extends Node {
if (!attribute) return null;
if (!attribute.is_static) {
return component.error(attribute, {
code: 'invalid-type',
message: '\'type\' attribute cannot be dynamic if input uses two-way binding'
});
return component.error(attribute, compiler_errors.invalid_type);
}
const value = attribute.get_static_value();
if (value === true) {
return component.error(attribute, {
code: 'missing-type',
message: '\'type\' attribute must be specified'
});
return component.error(attribute, compiler_errors.missing_type);
}
return value;
@ -652,10 +557,7 @@ export default class Element extends Node {
this.name !== 'textarea' &&
this.name !== 'select'
) {
return component.error(binding, {
code: 'invalid-binding',
message: `'value' is not a valid binding on <${this.name}> elements`
});
return component.error(binding, compiler_errors.invalid_binding_elements(this.name, 'value'));
}
if (this.name === 'select') {
@ -664,68 +566,45 @@ export default class Element extends Node {
);
if (attribute && !attribute.is_static) {
return component.error(attribute, {
code: 'dynamic-multiple-attribute',
message: '\'multiple\' attribute cannot be dynamic if select uses two-way binding'
});
return component.error(attribute, compiler_errors.dynamic_multiple_attribute);
}
} else {
check_type_attribute();
}
} else if (name === 'checked' || name === 'indeterminate') {
if (this.name !== 'input') {
return component.error(binding, {
code: 'invalid-binding',
message: `'${name}' is not a valid binding on <${this.name}> elements`
});
return component.error(binding, compiler_errors.invalid_binding_elements(this.name, name));
}
const type = check_type_attribute();
if (type !== 'checkbox') {
let message = `'${name}' binding can only be used with <input type="checkbox">`;
if (type === 'radio') message += ' — for <input type="radio">, use \'group\' binding';
return component.error(binding, { code: 'invalid-binding', message });
return component.error(binding, compiler_errors.invalid_binding_no_checkbox(name, type === 'radio'));
}
} else if (name === 'group') {
if (this.name !== 'input') {
return component.error(binding, {
code: 'invalid-binding',
message: `'group' is not a valid binding on <${this.name}> elements`
});
return component.error(binding, compiler_errors.invalid_binding_elements(this.name, 'group'));
}
const type = check_type_attribute();
if (type !== 'checkbox' && type !== 'radio') {
return component.error(binding, {
code: 'invalid-binding',
message: '\'group\' binding can only be used with <input type="checkbox"> or <input type="radio">'
});
return component.error(binding, compiler_errors.invalid_binding_element_with('<input type="checkbox"> or <input type="radio">', 'group'));
}
} else if (name === 'files') {
if (this.name !== 'input') {
return component.error(binding, {
code: 'invalid-binding',
message: `'files' is not a valid binding on <${this.name}> elements`
});
return component.error(binding, compiler_errors.invalid_binding_elements(this.name, 'files'));
}
const type = check_type_attribute();
if (type !== 'file') {
return component.error(binding, {
code: 'invalid-binding',
message: '\'files\' binding can only be used with <input type="file">'
});
return component.error(binding, compiler_errors.invalid_binding_element_with('<input type="file">', 'files'));
}
} else if (name === 'open') {
if (this.name !== 'details') {
return component.error(binding, {
code: 'invalid-binding',
message: `'${name}' binding can only be used with <details>`
});
return component.error(binding, compiler_errors.invalid_binding_element_with('<details>', name));
}
} else if (
name === 'currentTime' ||
@ -741,37 +620,22 @@ export default class Element extends Node {
name === 'ended'
) {
if (this.name !== 'audio' && this.name !== 'video') {
return component.error(binding, {
code: 'invalid-binding',
message: `'${name}' binding can only be used with <audio> or <video>`
});
return component.error(binding, compiler_errors.invalid_binding_element_with('audio> or <video>', name));
}
} else if (
name === 'videoHeight' ||
name === 'videoWidth'
) {
if (this.name !== 'video') {
return component.error(binding, {
code: 'invalid-binding',
message: `'${name}' binding can only be used with <video>`
});
return component.error(binding, compiler_errors.invalid_binding_element_with('<video>', name));
}
} else if (dimensions.test(name)) {
if (this.name === 'svg' && (name === 'offsetWidth' || name === 'offsetHeight')) {
return component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on <svg>. Use '${name.replace('offset', 'client')}' instead`
});
return component.error(binding, compiler_errors.invalid_binding_on(binding.name, `<svg>. Use '${name.replace('offset', 'client')}' instead`));
} else if (svg.test(this.name)) {
return component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on SVG elements`
});
return component.error(binding, compiler_errors.invalid_binding_on(binding.name, 'SVG elements'));
} else if (is_void(this.name)) {
return component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on void elements like <${this.name}>. Use a wrapper element instead`
});
return component.error(binding, compiler_errors.invalid_binding_on(binding.name, `void elements like <${this.name}>. Use a wrapper element instead`));
}
} else if (
name === 'textContent' ||
@ -782,21 +646,12 @@ export default class Element extends Node {
);
if (!contenteditable) {
return component.error(binding, {
code: 'missing-contenteditable-attribute',
message: '\'contenteditable\' attribute is required for textContent and innerHTML two-way bindings'
});
return component.error(binding, compiler_errors.missing_contenteditable_attribute);
} else if (contenteditable && !contenteditable.is_static) {
return component.error(contenteditable, {
code: 'dynamic-contenteditable-attribute',
message: '\'contenteditable\' attribute cannot be dynamic if element uses two-way binding'
});
return component.error(contenteditable, compiler_errors.dynamic_contenteditable_attribute);
}
} else if (name !== 'this') {
return component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding`
});
return component.error(binding, compiler_errors.invalid_binding(binding.name));
}
});
}
@ -809,10 +664,7 @@ export default class Element extends Node {
) return;
if (this.children.length === 0) {
this.component.warn(this, {
code: 'a11y-missing-content',
message: `A11y: <${this.name}> element should have child content`
});
this.component.warn(this, compiler_warnings.a11y_missing_content(this.name));
}
}
@ -821,50 +673,32 @@ export default class Element extends Node {
this.handlers.forEach(handler => {
if (handler.modifiers.has('passive') && handler.modifiers.has('preventDefault')) {
return component.error(handler, {
code: 'invalid-event-modifier',
message: 'The \'passive\' and \'preventDefault\' modifiers cannot be used together'
});
return component.error(handler, compiler_errors.invalid_event_modifier_combination('passive', 'preventDefault'));
}
if (handler.modifiers.has('passive') && handler.modifiers.has('nonpassive')) {
return component.error(handler, {
code: 'invalid-event-modifier',
message: 'The \'passive\' and \'nonpassive\' modifiers cannot be used together'
});
return component.error(handler, compiler_errors.invalid_event_modifier_combination('passive', 'nonpassive'));
}
handler.modifiers.forEach(modifier => {
if (!valid_modifiers.has(modifier)) {
return component.error(handler, {
code: 'invalid-event-modifier',
message: `Valid event modifiers are ${list(Array.from(valid_modifiers))}`
});
return component.error(handler, compiler_errors.invalid_event_modifier(list(Array.from(valid_modifiers))));
}
if (modifier === 'passive') {
if (passive_events.has(handler.name)) {
if (handler.can_make_passive) {
component.warn(handler, {
code: 'redundant-event-modifier',
message: 'Touch event handlers that don\'t use the \'event\' object are passive by default'
});
component.warn(handler, compiler_warnings.redundant_event_modifier_for_touch);
}
} else {
component.warn(handler, {
code: 'redundant-event-modifier',
message: 'The passive modifier only works with wheel and touch events'
});
component.warn(handler, compiler_warnings.redundant_event_modifier_passive);
}
}
if (component.compile_options.legacy && (modifier === 'once' || modifier === 'passive')) {
// TODO this could be supported, but it would need a few changes to
// how event listeners work
return component.error(handler, {
code: 'invalid-event-modifier',
message: `The '${modifier}' modifier cannot be used in legacy mode`
});
return component.error(handler, compiler_errors.invalid_event_modifier_legacy(modifier));
}
});
@ -927,10 +761,7 @@ function should_have_attribute(
attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` :
attributes[0];
node.component.warn(node, {
code: 'a11y-missing-attribute',
message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
});
node.component.warn(node, compiler_warnings.a11y_missing_attribute(name, article, sequence));
}
function within_custom_element(parent: INode) {

@ -4,6 +4,7 @@ import hash from '../utils/hash';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
export default class Head extends Node {
type: 'Head';
@ -14,11 +15,7 @@ export default class Head extends Node {
super(component, parent, scope, info);
if (info.attributes.length) {
component.error(info.attributes[0], {
code: 'invalid-attribute',
message: '<svelte:head> should not have any attributes or directives'
});
return;
return component.error(info.attributes[0], compiler_errors.invalid_attribute_head);
}
this.children = map_children(component, parent, scope, info.children.filter(child => {

@ -9,6 +9,7 @@ import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
export default class InlineComponent extends Node {
type: 'InlineComponent';
@ -41,10 +42,7 @@ export default class InlineComponent extends Node {
/* eslint-disable no-fallthrough */
switch (node.type) {
case 'Action':
return component.error(node, {
code: 'invalid-action',
message: 'Actions can only be applied to DOM elements, not components'
});
return component.error(node, compiler_errors.invalid_action);
case 'Attribute':
if (node.name.startsWith('--')) {
@ -61,10 +59,7 @@ export default class InlineComponent extends Node {
break;
case 'Class':
return component.error(node, {
code: 'invalid-class',
message: 'Classes can only be applied to DOM elements, not components'
});
return component.error(node, compiler_errors.invalid_class);
case 'EventHandler':
this.handlers.push(new EventHandler(component, this, scope, node));
@ -75,10 +70,7 @@ export default class InlineComponent extends Node {
break;
case 'Transition':
return component.error(node, {
code: 'invalid-transition',
message: 'Transitions can only be applied to DOM elements, not components'
});
return component.error(node, compiler_errors.invalid_transition);
default:
throw new Error(`Not implemented: ${node.type}`);
@ -103,10 +95,7 @@ export default class InlineComponent extends Node {
this.handlers.forEach(handler => {
handler.modifiers.forEach(modifier => {
if (modifier !== 'once') {
return component.error(handler, {
code: 'invalid-event-modifier',
message: "Event modifiers other than 'once' can only be used on DOM elements"
});
return component.error(handler, compiler_errors.invalid_event_modifier_component);
}
});
});

@ -4,6 +4,7 @@ import { walk } from 'estree-walker';
import { BasePattern, Identifier } from 'estree';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
const applicable = new Set(['Identifier', 'ObjectExpression', 'ArrayExpression', 'Property']);
@ -26,10 +27,7 @@ export default class Let extends Node {
walk(info.expression, {
enter(node: Identifier|BasePattern) {
if (!applicable.has(node.type)) {
return component.error(node as any, {
code: 'invalid-let',
message: 'let directive value must be an identifier or an object/array pattern'
});
return component.error(node as any, compiler_errors.invalid_let);
}
if (node.type === 'Identifier') {

@ -4,6 +4,7 @@ import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
export default class Slot extends Element {
type: 'Element';
@ -17,26 +18,17 @@ export default class Slot extends Element {
info.attributes.forEach(attr => {
if (attr.type !== 'Attribute' && attr.type !== 'Spread') {
return component.error(attr, {
code: 'invalid-slot-directive',
message: '<slot> cannot have directives'
});
return component.error(attr, compiler_errors.invalid_slot_directive);
}
if (attr.name === 'name') {
if (attr.value.length !== 1 || attr.value[0].type !== 'Text') {
return component.error(attr, {
code: 'dynamic-slot-name',
message: '<slot> name cannot be dynamic'
});
return component.error(attr, compiler_errors.dynamic_slot_name);
}
this.slot_name = attr.value[0].data;
if (this.slot_name === 'default') {
return component.error(attr, {
code: 'invalid-slot-name',
message: 'default is a reserved word — it cannot be used as a slot name'
});
return component.error(attr, compiler_errors.invalid_slot_name);
}
}

@ -5,6 +5,7 @@ import Node from './shared/Node';
import Let from './Let';
import Attribute from './Attribute';
import { INode } from './interfaces';
import compiler_errors from '../compiler_errors';
export default class SlotTemplate extends Node {
type: 'SlotTemplate';
@ -45,17 +46,11 @@ export default class SlotTemplate extends Node {
if (node.name === 'slot') {
this.slot_attribute = new Attribute(component, this, scope, node);
if (!this.slot_attribute.is_static) {
return component.error(node, {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
});
return component.error(node, compiler_errors.invalid_slot_attribute);
}
const value = this.slot_attribute.get_static_value();
if (typeof value === 'boolean') {
return component.error(node, {
code: 'invalid-slot-attribute',
message: 'slot attribute value is missing'
});
return component.error(node, compiler_errors.invalid_slot_attribute_value_missing);
}
this.slot_template_name = value as string;
break;
@ -73,10 +68,7 @@ export default class SlotTemplate extends Node {
validate_slot_template_placement() {
if (this.parent.type !== 'InlineComponent') {
return this.component.error(this, {
code: 'invalid-slotted-content',
message: '<svelte:fragment> must be a child of a component'
});
return this.component.error(this, compiler_errors.invalid_slotted_content_fragment);
}
}
}

@ -3,6 +3,7 @@ import map_children, { Children } from './shared/map_children';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
export default class Title extends Node {
type: 'Title';
@ -14,19 +15,12 @@ export default class Title extends Node {
this.children = map_children(component, parent, scope, info.children);
if (info.attributes.length > 0) {
component.error(info.attributes[0], {
code: 'illegal-attribute',
message: '<title> cannot have attributes'
});
return;
return component.error(info.attributes[0], compiler_errors.illegal_attribute_title);
}
info.children.forEach(child => {
if (child.type !== 'Text' && child.type !== 'MustacheTag') {
return component.error(child, {
code: 'illegal-structure',
message: '<title> can only contain text and {tags}'
});
return component.error(child, compiler_errors.illegal_structure_title);
}
});

@ -4,6 +4,7 @@ import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Element from './Element';
import compiler_errors from '../compiler_errors';
export default class Transition extends Node {
type: 'Transition';
@ -25,16 +26,7 @@ export default class Transition extends Node {
if ((info.intro && parent.intro) || (info.outro && parent.outro)) {
const parent_transition = (parent.intro || parent.outro);
const message = this.directive === parent_transition.directive
? `An element can only have one '${this.directive}' directive`
: `An element cannot have both ${describe(parent_transition)} directive and ${describe(this)} directive`;
component.error(info, {
code: 'duplicate-transition',
message
});
return;
return component.error(info, compiler_errors.duplicate_transition(this.directive, parent_transition.directive));
}
this.expression = info.expression
@ -42,9 +34,3 @@ export default class Transition extends Node {
: null;
}
}
function describe(transition: Transition) {
return transition.directive === 'transition'
? "a 'transition'"
: `an '${transition.directive}'`;
}

@ -8,6 +8,7 @@ import Action from './Action';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
const valid_bindings = [
'innerWidth',
@ -36,10 +37,7 @@ export default class Window extends Node {
const { parts } = flatten_reference(node.expression);
// TODO is this constraint necessary?
return component.error(node.expression, {
code: 'invalid-binding',
message: `Bindings on <svelte:window> must be to top-level properties, e.g. '${parts[parts.length - 1]}' rather than '${parts.join('.')}'`
});
return component.error(node.expression, compiler_errors.invalid_binding_window(parts));
}
if (!~valid_bindings.indexOf(node.name)) {
@ -49,18 +47,10 @@ export default class Window extends Node {
fuzzymatch(node.name, valid_bindings)
);
const message = `'${node.name}' is not a valid binding on <svelte:window>`;
if (match) {
return component.error(node, {
code: 'invalid-binding',
message: `${message} (did you mean '${match}'?)`
});
return component.error(node, compiler_errors.invalid_binding_on(node.name, '<svelte:window>', ` (did you mean '${match}'?)`));
} else {
return component.error(node, {
code: 'invalid-binding',
message: `${message} — valid bindings are ${list(valid_bindings)}`
});
return component.error(node, compiler_errors.invalid_binding_on(node.name, '<svelte:window>', ` — valid bindings are ${list(valid_bindings)}`));
}
}

@ -2,6 +2,7 @@ import Block from '../../render_dom/Block';
import Component from '../../Component';
import Node from './Node';
import { INode } from '../interfaces';
import compiler_warnings from '../../compiler_warnings';
export default class AbstractBlock extends Node {
block: Block;
@ -17,10 +18,7 @@ export default class AbstractBlock extends Node {
const child = this.children[0];
if (!child || (child.type === 'Text' && !/[^ \r\n\f\v\t]/.test(child.data))) {
this.component.warn(this, {
code: 'empty-block',
message: 'Empty block'
});
this.component.warn(this, compiler_warnings.empty_block);
}
}
}

@ -18,6 +18,7 @@ import is_contextual from './is_contextual';
import EachBlock from '../EachBlock';
import { clone } from '../../../utils/clone';
import { Node as PeriscopicNode } from 'periscopic';
import compiler_errors from '../../compiler_errors';
type Owner = INode;
@ -85,10 +86,7 @@ export default class Expression {
if (name[0] === '$') {
const store_name = name.slice(1);
if (template_scope.names.has(store_name) || scope.has(store_name)) {
return component.error(node, {
code: 'contextual-store',
message: 'Stores must be declared at the top level of the component (this may change in a future version of Svelte)'
});
return component.error(node, compiler_errors.contextual_store);
}
}

@ -19,6 +19,7 @@ import mark_each_block_bindings from '../shared/mark_each_block_bindings';
import { string_to_member_expression } from '../../../utils/string_to_member_expression';
import SlotTemplate from '../../../nodes/SlotTemplate';
import { is_head } from '../shared/is_head';
import compiler_warnings from '../../../compiler_warnings';
type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node };
@ -108,10 +109,7 @@ export default class InlineComponentWrapper extends Wrapper {
}
if (variable.reassigned || variable.export_name || variable.is_reactive_dependency) {
this.renderer.component.warn(this.node, {
code: 'reactive-component',
message: `<${name}/> will not be reactive if ${name} changes. Use <svelte:component this={${name}}/> if you want this reactivity.`
});
this.renderer.component.warn(this.node, compiler_warnings.reactive_component(name));
}
}

@ -1,4 +1,4 @@
<video><track kind="captions"/></video>
<video></video>
<video><track /></video>
<audio muted></audio>
<audio></audio>

@ -6,7 +6,7 @@
"column": 15,
"line": 2
},
"message": "A11y: Media elements must have a <track kind=\"captions\">",
"message": "A11y: <video> elements must have a <track kind=\"captions\">",
"pos": 40,
"start": {
"character": 40,
@ -21,7 +21,7 @@
"column": 24,
"line": 3
},
"message": "A11y: Media elements must have a <track kind=\"captions\">",
"message": "A11y: <video> elements must have a <track kind=\"captions\">",
"pos": 56,
"start": {
"character": 56,

@ -21,6 +21,9 @@
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
// Hides exports flagged with @internal from the d.ts output
"stripInternal": true,
// TODO: error all the things
//"strict": true,
"noImplicitThis": true,

Loading…
Cancel
Save