Merge branch 'version-4' into fix/pr-6794

pull/6800/head
Simon Holthausen 3 years ago
commit e0d2b8b0b6

@ -10,5 +10,8 @@ module.exports = {
'estree' 'estree'
], ],
'svelte3/compiler': require('./compiler') 'svelte3/compiler': require('./compiler')
},
rules: {
'@typescript-eslint/no-non-null-assertion': 'off'
} }
}; };

@ -22,14 +22,20 @@ jobs:
os: ubuntu-latest os: ubuntu-latest
- node-version: 18 - node-version: 18
os: ubuntu-latest os: ubuntu-latest
- node-version: 20
os: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2.2.4
with:
version: ${{ matrix.node-version == 14 && 7 || 8 }}
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm cache: pnpm
- run: npm install - run: pnpm install --frozen-lockfile
- run: npm run test:integration - run: node node_modules/puppeteer/install.js
- run: pnpm test:integration
env: env:
CI: true CI: true
Lint: Lint:
@ -37,10 +43,11 @@ jobs:
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2.2.4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: npm cache: pnpm
- run: 'npm i && npm run lint' - run: 'pnpm i && pnpm lint'
Unit: Unit:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 10 timeout-minutes: 10
@ -57,12 +64,14 @@ jobs:
os: ubuntu-latest os: ubuntu-latest
- node-version: 18 - node-version: 18
os: ubuntu-latest os: ubuntu-latest
- node-version: 20
os: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2.2.4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: npm node-version: ${{ matrix.node-version }}
- run: npm install cache: pnpm
env: - run: pnpm install
SKIP_PREPARE: true - run: pnpm test:unit
- run: npm run test:unit

@ -1,9 +1,12 @@
const is_unit_test = process.env.UNIT_TEST; const is_unit_test = process.env.UNIT_TEST;
module.exports = { module.exports = {
file: is_unit_test ? [] : ['test/test.ts'], file: is_unit_test ? [] : ['test/test.js'],
require: [ require: [
'sucrase/register' 'sucrase/register'
],
"node-option": [
"experimental-modules"
] ]
}; };

@ -3,13 +3,24 @@
## Unreleased (4.0) ## Unreleased (4.0)
* **breaking** Minimum supported Node version is now Node 14 * **breaking** Minimum supported Node version is now Node 14
* **breaking** Minimum supported TypeScript version is now 5 (it will likely work with lower versions, but we make no guarantess about that) * **breaking** Minimum supported webpack version is now webpack 5
* **breaking** Minimum supported TypeScript version is now TypeScript 5 (it will likely work with lower versions, but we make no guarantees about that)
* **breaking** Stricter types for `createEventDispatcher` (see PR for migration instructions) ([#7224](https://github.com/sveltejs/svelte/pull/7224)) * **breaking** Stricter types for `createEventDispatcher` (see PR for migration instructions) ([#7224](https://github.com/sveltejs/svelte/pull/7224))
* **breaking** Stricter types for `Action` and `ActionReturn` (see PR for migration instructions) ([#7224](https://github.com/sveltejs/svelte/pull/7224)) * **breaking** Stricter types for `Action` and `ActionReturn` (see PR for migration instructions) ([#7224](https://github.com/sveltejs/svelte/pull/7224))
* **breaking** Stricter types for `onMount` - now throws a type error when returning a function asynchronously to catch potential mistakes around callback functions (see PR for migration instructions) ([#8136](https://github.com/sveltejs/svelte/pull/8136)) * **breaking** Stricter types for `onMount` - now throws a type error when returning a function asynchronously to catch potential mistakes around callback functions (see PR for migration instructions) ([#8136](https://github.com/sveltejs/svelte/pull/8136))
* **breaking** Overhaul and drastically improve creating custom elements with Svelte (see PR for list of changes and migration instructions) ([#8457](https://github.com/sveltejs/svelte/pull/8457))
* **breaking** Deprecate `SvelteComponentTyped`, use `SvelteComponent` instead ([#8512](https://github.com/sveltejs/svelte/pull/8512))
* **breaking** Error on falsy values instead of stores passed to `derived` ([#7947](https://github.com/sveltejs/svelte/pull/7947))
* **breaking** Custom store implementers now need to pass an `update` function additionally to the `set` function ([#6750](https://github.com/sveltejs/svelte/pull/6750))
* Improve hydration speed by adding `data-svelte-h` attribute to detect unchanged HTML elements ([#7426](https://github.com/sveltejs/svelte/pull/7426))
* Add `a11y no-noninteractive-element-interactions` rule ([#8391](https://github.com/sveltejs/svelte/pull/8391)) * Add `a11y no-noninteractive-element-interactions` rule ([#8391](https://github.com/sveltejs/svelte/pull/8391))
* Add `a11y-no-static-element-interactions`rule ([#8251](https://github.com/sveltejs/svelte/pull/8251)) * Add `a11y-no-static-element-interactions`rule ([#8251](https://github.com/sveltejs/svelte/pull/8251))
* Improve duplicate key error for keyed `each` blocks ([#8411](https://github.com/sveltejs/svelte/pull/8411))
* Bind `null` option and input values consistently ([#8312](https://github.com/sveltejs/svelte/issues/8312)) * Bind `null` option and input values consistently ([#8312](https://github.com/sveltejs/svelte/issues/8312))
* Allow `$store` to be used with changing values including nullish values ([#7555](https://github.com/sveltejs/svelte/issues/7555))
* Initialize stylesheet with `/* empty */` to enable setting CSP directive that also works in Safari ([#7800](https://github.com/sveltejs/svelte/pull/7800))
* Treat slots as if they don't exist when using CSS adjacent and general sibling combinators ([#8284](https://github.com/sveltejs/svelte/issues/8284))
* Fix transitions so that they don't require a `style-src 'unsafe-inline'` Content Security Policy (CSP) ([#6662](https://github.com/sveltejs/svelte/issues/6662)).
## Unreleased (3.0) ## Unreleased (3.0)

@ -72,9 +72,9 @@ Small pull requests are much easier to review and more likely to get merged.
### Installation ### Installation
1. Ensure you have [npm](https://www.npmjs.com/get-npm) installed. 1. Ensure you have [pnpm](https://pnpm.io/installation) installed.
1. After cloning the repository, run `npm install` in the root of the repository. 1. After cloning the repository, run `pnpm install` in the root of the repository.
1. To start a development server, run `npm run dev`. 1. To compile in watch mode, run `pnpm dev`.
### Creating a branch ### Creating a branch
@ -94,8 +94,8 @@ Test samples are kept in `/test/xxx/samples` folder.
#### Running tests #### Running tests
1. To run test, run `npm run test`. 1. To run test, run `pnpm test`.
1. To run test for a specific feature, you can use the `-g` (aka `--grep`) option. For example, to only run test involving transitions, run `npm run test -- -g transition`. 1. To run test for a specific feature, you can use the `-g` (aka `--grep`) option. For example, to only run test involving transitions, run `pnpm test -- -g transition`.
##### Running solo test ##### Running solo test
@ -106,11 +106,11 @@ Test samples are kept in `/test/xxx/samples` folder.
##### Updating `.expected` files ##### Updating `.expected` files
1. Tests suites like `css`, `js`, `server-side-rendering` asserts that the generated output has to match the content in the `.expected` file. For example, in the `js` test suites, the generated js code is compared against the content in `expected.js`. 1. Tests suites like `css`, `js`, `server-side-rendering` asserts that the generated output has to match the content in the `.expected` file. For example, in the `js` test suites, the generated js code is compared against the content in `expected.js`.
1. To update the content of the `.expected` file, run the test with `--update` flag. (`npm run test --update`) 1. To update the content of the `.expected` file, run the test with `--update` flag. (`pnpm test --update`)
### Style guide ### Style guide
[Eslint](https://eslint.org) will catch most styling issues that may exist in your code. You can check the status of your code styling by simply running `npm run lint`. [Eslint](https://eslint.org) will catch most styling issues that may exist in your code. You can check the status of your code styling by simply running `pnpm lint`.
#### Code conventions #### Code conventions
@ -122,8 +122,8 @@ Test samples are kept in `/test/xxx/samples` folder.
Please make sure the following is done when submitting a pull request: Please make sure the following is done when submitting a pull request:
1. Describe your **test plan** in your pull request description. Make sure to test your changes. 1. Describe your **test plan** in your pull request description. Make sure to test your changes.
1. Make sure your code lints (`npm run lint`). 1. Make sure your code lints (`pnpm lint`).
1. Make sure your tests pass (`npm run test`). 1. Make sure your tests pass (`pnpm test`).
All pull requests should be opened against the `master` branch. Make sure the PR does only one thing, otherwise please split it. All pull requests should be opened against the `master` branch. Make sure the PR does only one thing, otherwise please split it.

@ -34,7 +34,7 @@ To install and work on Svelte locally:
```bash ```bash
git clone https://github.com/sveltejs/svelte.git git clone https://github.com/sveltejs/svelte.git
cd svelte cd svelte
npm install pnpm 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.
@ -42,13 +42,13 @@ npm install
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 pnpm build
``` ```
To watch for changes and continually rebuild the package (this is useful if you're using [npm link](https://docs.npmjs.com/cli/link.html) to test out changes in a project locally): To watch for changes and continually rebuild the package (this is useful if you're using [`pnpm link`](https://pnpm.io/cli/link) to test out changes in a project locally):
```bash ```bash
npm run dev pnpm 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.
@ -57,13 +57,13 @@ The compiler is written in [TypeScript](https://www.typescriptlang.org/), but do
### Running Tests ### Running Tests
```bash ```bash
npm run test pnpm test
``` ```
To filter tests, use `-g` (aka `--grep`). For example, to only run tests involving transitions: To filter tests, use `-g` (aka `--grep`). For example, to only run tests involving transitions:
```bash ```bash
npm run test -- -g transition pnpm test -- -g transition
``` ```

@ -84,9 +84,9 @@ export interface DOMAttributes<T extends EventTarget> {
'on:beforeinput'?: EventHandler<InputEvent, T> | undefined | null; 'on:beforeinput'?: EventHandler<InputEvent, T> | undefined | null;
'on:input'?: FormEventHandler<T> | undefined | null; 'on:input'?: FormEventHandler<T> | undefined | null;
'on:reset'?: FormEventHandler<T> | undefined | null; 'on:reset'?: FormEventHandler<T> | undefined | null;
'on:submit'?: EventHandler<Event & { readonly submitter: HTMLElement | null; }, T> | undefined | null; // TODO make this SubmitEvent once we require TS>=4.4 'on:submit'?: EventHandler<SubmitEvent, T> | undefined | null;
'on:invalid'?: EventHandler<Event, T> | undefined | null; 'on:invalid'?: EventHandler<Event, T> | undefined | null;
'on:formdata'?: EventHandler<Event & { readonly formData: FormData; }, T> | undefined | null; // TODO make this FormDataEvent once we require TS>=4.4 'on:formdata'?: EventHandler<FormDataEvent, T> | undefined | null;
// Image Events // Image Events
'on:load'?: EventHandler | undefined | null; 'on:load'?: EventHandler | undefined | null;
@ -547,9 +547,9 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
'bind:innerText'?: string | undefined | null; 'bind:innerText'?: string | undefined | null;
readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null; readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null;
readonly 'bind:contentBoxSize'?: Array<{ blockSize: number; inlineSize: number }> | undefined | null; // TODO make this ResizeObserverSize once we require TS>=4.4 readonly 'bind:contentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:borderBoxSize'?: Array<{ blockSize: number; inlineSize: number }> | undefined | null; // TODO make this ResizeObserverSize once we require TS>=4.4 readonly 'bind:borderBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:devicePixelContentBoxSize'?: Array<{ blockSize: number; inlineSize: number }> | undefined | null; // TODO make this ResizeObserverSize once we require TS>=4.4 readonly 'bind:devicePixelContentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
// SvelteKit // SvelteKit
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null; 'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;
@ -558,6 +558,9 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
'data-sveltekit-preload-data'?: true | '' | 'hover' | 'tap' | 'off' | undefined | null; 'data-sveltekit-preload-data'?: true | '' | 'hover' | 'tap' | 'off' | undefined | null;
'data-sveltekit-reload'?: true | '' | 'off' | undefined | null; 'data-sveltekit-reload'?: true | '' | 'off' | undefined | null;
'data-sveltekit-replacestate'?: true | '' | 'off' | undefined | null; 'data-sveltekit-replacestate'?: true | '' | 'off' | undefined | null;
// allow any data- attribute
[key: `data-${string}`]: any;
} }
export type HTMLAttributeAnchorTarget = export type HTMLAttributeAnchorTarget =
@ -1594,7 +1597,17 @@ export interface SvelteHTMLElements {
'svelte:document': HTMLAttributes<Document>; 'svelte:document': HTMLAttributes<Document>;
'svelte:body': HTMLAttributes<HTMLElement>; 'svelte:body': HTMLAttributes<HTMLElement>;
'svelte:fragment': { slot?: string }; 'svelte:fragment': { slot?: string };
'svelte:options': { [name: string]: any }; 'svelte:options': {
customElement?: string | undefined | {
tag: string;
shadow?: 'open' | 'none' | undefined;
props?: Record<string, { attribute?: string; reflect?: boolean; type?: 'String' | 'Boolean' | 'Number' | 'Array' | 'Object' }> | undefined;
};
immutable?: boolean | undefined;
accessors?: boolean | undefined;
namespace?: string | undefined;
[name: string]: any
};
'svelte:head': { [name: string]: any }; 'svelte:head': { [name: string]: any };
[name: string]: { [name: string]: any }; [name: string]: { [name: string]: any };

9435
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -130,6 +130,7 @@
"@rollup/plugin-virtual": "^3.0.1", "@rollup/plugin-virtual": "^3.0.1",
"@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.8.0", "@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.8.0",
"@types/aria-query": "^5.0.1", "@types/aria-query": "^5.0.1",
"@types/estree": "^1.0.0",
"@types/mocha": "^10.0.1", "@types/mocha": "^10.0.1",
"@types/node": "^14.14.31", "@types/node": "^14.14.31",
"@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/eslint-plugin": "^5.58.0",
@ -159,5 +160,9 @@
"tslib": "^2.5.0", "tslib": "^2.5.0",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"util": "^0.12.5" "util": "^0.12.5"
},
"packageManager": "pnpm@7.32.0",
"engines": {
"pnpm": ">=7.0.0"
} }
} }

File diff suppressed because it is too large Load Diff

@ -15,9 +15,12 @@ const is_publish = !!process.env.PUBLISH;
const ts_plugin = is_publish const ts_plugin = is_publish
? typescript({ ? typescript({
typescript: require('typescript'), typescript: require('typescript'),
paths: {
'svelte/*': ['./src/runtime/*']
}
}) })
: sucrase({ : sucrase({
transforms: ['typescript'], transforms: ['typescript']
}); });
fs.writeFileSync( fs.writeFileSync(
@ -89,19 +92,6 @@ export default [
} }
} }
fs.writeFileSync(
`${dir}/package.json`,
JSON.stringify(
{
main: './index.js',
module: './index.mjs',
types: './index.d.ts',
},
null,
' '
)
);
fs.writeFileSync( fs.writeFileSync(
`${dir}/index.d.ts`, `${dir}/index.d.ts`,
`export * from '../types/runtime/${dir}/index.js';` `export * from '../types/runtime/${dir}/index.js';`

@ -1825,10 +1825,10 @@ The `<svelte:options>` element provides a place to specify per-component compile
* `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"; use the "foreign" namespace to opt out of case-insensitive attribute names and HTML-specific warnings * `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 * `customElement="..."` — the name to use when compiling this component as a custom element
```sv ```sv
<svelte:options tag="my-custom-element"/> <svelte:options customElement="my-custom-element"/>
``` ```
### `<svelte:fragment>` ### `<svelte:fragment>`

@ -289,7 +289,7 @@ This makes it possible to wrap almost any other reactive state handling library
store = writable(value?: any) store = writable(value?: any)
``` ```
```js ```js
store = writable(value?: any, start?: (set: (value: any) => void) => () => void) store = writable(value?: any, start?: (set: (value: any) => void, update: (fn: any => any) => void) => () => void)
``` ```
--- ---
@ -316,7 +316,7 @@ count.update(n => n + 1); // logs '2'
--- ---
If a function is passed as the second argument, it will be called when the number of subscribers goes from zero to one (but not from one to two, etc). That function will be passed a `set` function which changes the value of the store. It must return a `stop` function that is called when the subscriber count goes from one to zero. If a function is passed as the second argument, it will be called when the number of subscribers goes from zero to one (but not from one to two, etc). That function will be passed a `set` function which changes the value of the store, and an `update` function which works like the `update` method on the store, taking a callback to calculate the store's new value from its old value. It must return a `stop` function that is called when the subscriber count goes from one to zero.
```js ```js
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
@ -340,7 +340,7 @@ Note that the value of a `writable` is lost when it is destroyed, for example wh
#### `readable` #### `readable`
```js ```js
store = readable(value?: any, start?: (set: (value: any) => void) => () => void) store = readable(value?: any, start?: (set: (value: any) => void, update: (fn: any => any) => void) => () => void)
``` ```
--- ---
@ -359,6 +359,16 @@ const time = readable(null, set => {
return () => clearInterval(interval); return () => clearInterval(interval);
}); });
const ticktock = readable(null, (set, update) => {
set('tick');
const interval = setInterval(() => {
update(sound => sound === 'tick' ? 'tock' : 'tick');
}, 1000);
return () => clearInterval(interval);
});
``` ```
#### `derived` #### `derived`
@ -367,13 +377,13 @@ const time = readable(null, set => {
store = derived(a, callback: (a: any) => any) store = derived(a, callback: (a: any) => any)
``` ```
```js ```js
store = derived(a, callback: (a: any, set: (value: any) => void) => void | () => void, initial_value: any) store = derived(a, callback: (a: any, set: (value: any) => void, update: (fn: any => any) => void) => void | () => void, initial_value: any)
``` ```
```js ```js
store = derived([a, ...b], callback: ([a: any, ...b: any[]]) => any) store = derived([a, ...b], callback: ([a: any, ...b: any[]]) => any)
``` ```
```js ```js
store = derived([a, ...b], callback: ([a: any, ...b: any[]], set: (value: any) => void) => void | () => void, initial_value: any) store = derived([a, ...b], callback: ([a: any, ...b: any[]], set: (value: any) => void, update: (fn: any => any) => void) => void | () => void, initial_value: any)
``` ```
--- ---
@ -390,9 +400,9 @@ const doubled = derived(a, $a => $a * 2);
--- ---
The callback can set a value asynchronously by accepting a second argument, `set`, and calling it when appropriate. The callback can set a value asynchronously by accepting a second argument, `set`, and an optional third argument, `update`, calling either or both of them when appropriate.
In this case, you can also pass a third argument to `derived` — the initial value of the derived store before `set` is first called. In this case, you can also pass a third argument to `derived` — the initial value of the derived store before `set` or `update` is first called. If no initial value is specified, the store's initial value will be `undefined`.
```js ```js
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
@ -400,6 +410,13 @@ import { derived } from 'svelte/store';
const delayed = derived(a, ($a, set) => { const delayed = derived(a, ($a, set) => {
setTimeout(() => set($a), 1000); setTimeout(() => set($a), 1000);
}, 'one moment...'); }, 'one moment...');
const delayedIncrement = derived(a, ($a, set, update) => {
set($a);
setTimeout(() => update(x => x + 1), 1000);
// every time $a produces a value, this produces two
// values, $a immediately and then $a + 1 a second later
});
``` ```
--- ---
@ -1118,7 +1135,7 @@ app.count += 1;
Svelte components can also be compiled to custom elements (aka web components) using the `customElement: true` compiler option. You should specify a tag name for the component using the `<svelte:options>` [element](/docs#template-syntax-svelte-options). Svelte components can also be compiled to custom elements (aka web components) using the `customElement: true` compiler option. You should specify a tag name for the component using the `<svelte:options>` [element](/docs#template-syntax-svelte-options).
```sv ```sv
<svelte:options tag="my-element" /> <svelte:options customElement="my-element" />
<script> <script>
export let name = 'world'; export let name = 'world';
@ -1130,12 +1147,12 @@ Svelte components can also be compiled to custom elements (aka web components) u
--- ---
Alternatively, use `tag={null}` to indicate that the consumer of the custom element should name it. You can leave out the tag name for any of your inner components which you don't want to expose and use them like regular Svelte components. Consumers of the component can still name it afterwards if needed, using the static `element` property which contains the custom element constructor and which is available when the `customElement` compiler option is `true`.
```js ```js
import MyElement from './MyElement.svelte'; import MyElement from './MyElement.svelte';
customElements.define('my-element', MyElement); customElements.define('my-element', MyElement.element);
``` ```
--- ---
@ -1166,15 +1183,42 @@ console.log(el.name);
el.name = 'everybody'; el.name = 'everybody';
``` ```
---
When constructing a custom element, you can tailor several aspects by defining `customElement` as an object within `<svelte:options>`. This object comprises a mandatory `tag` property for the custom element's name, an optional `shadow` property that can be set to `"none"` to forgo shadow root creation, and a `props` option, which offers the following settings:
- `attribute: string`: To update a custom element's prop, you have two alternatives: either set the property on the custom element's reference as illustrated above or use an HTML attribute. For the latter, the default attribute name is the lowercase property name. Modify this by assigning `attribute: "<desired name>"`.
- `reflect: boolean`: By default, updated prop values do not reflect back to the DOM. To enable this behavior, set `reflect: true`.
- `type: 'String' | 'Boolean' | 'Number' | 'Array' | 'Object'`: While converting an attribute value to a prop value and reflecting it back, the prop value is assumed to be a `String` by default. This may not always be accurate. For instance, for a number type, define it using `type: "Number"`
```svelte
<svelte:options
customElement={{
tag: "custom-element",
shadow: "none",
props: {
name: { reflect: true, type: "Number", attribute: "element-index" },
},
}}
/>
<script>
export let elementIndex;
</script>
...
```
Custom elements can be a useful way to package components for consumption in a non-Svelte app, as they will work with vanilla HTML and JavaScript as well as [most frameworks](https://custom-elements-everywhere.com/). There are, however, some important differences to be aware of: Custom elements can be a useful way to package components for consumption in a non-Svelte app, as they will work with vanilla HTML and JavaScript as well as [most frameworks](https://custom-elements-everywhere.com/). There are, however, some important differences to be aware of:
* Styles are *encapsulated*, rather than merely *scoped*. This means that any non-component styles (such as you might have in a `global.css` file) will not apply to the custom element, including styles with the `:global(...)` modifier * Styles are *encapsulated*, rather than merely *scoped* (unless you set `shadow: "none"`). This means that any non-component styles (such as you might have in a `global.css` file) will not apply to the custom element, including styles with the `:global(...)` modifier
* Instead of being extracted out as a separate .css file, styles are inlined into the component as a JavaScript string * Instead of being extracted out as a separate .css file, styles are inlined into the component as a JavaScript string
* Custom elements are not generally suitable for server-side rendering, as the shadow DOM is invisible until JavaScript loads * Custom elements are not generally suitable for server-side rendering, as the shadow DOM is invisible until JavaScript loads
* In Svelte, slotted content renders *lazily*. In the DOM, it renders *eagerly*. In other words, it will always be created even if the component's `<slot>` element is inside an `{#if ...}` block. Similarly, including a `<slot>` in an `{#each ...}` block will not cause the slotted content to be rendered multiple times * In Svelte, slotted content renders *lazily*. In the DOM, it renders *eagerly*. In other words, it will always be created even if the component's `<slot>` element is inside an `{#if ...}` block. Similarly, including a `<slot>` in an `{#each ...}` block will not cause the slotted content to be rendered multiple times
* The `let:` directive has no effect * The `let:` directive has no effect, because custom elements do not have a way to pass data to the parent component that fills the slot
* Polyfills are required to support older browsers * Polyfills are required to support older browsers
When a custom element written with Svelte is created or updated, the shadow dom will reflect the value in the next tick, not immediately. This way updates can be batched, and DOM moves which temporarily (but synchronously) detach the element from the DOM don't lead to unmounting the inner component.
### Server-side component API ### Server-side component API

@ -25,6 +25,6 @@ The options that can be set here are:
* `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"`
* `tag="..."` — the name to use when compiling this component as a custom element * `customElement="..."` — the name to use when compiling this component as a custom element
Consult the [API reference](/docs) for more information on these options. Consult the [API reference](/docs) for more information on these options.

@ -16,7 +16,7 @@ import Stylesheet from './css/Stylesheet';
import { test } from '../config'; import { test } from '../config';
import Fragment from './nodes/Fragment'; import Fragment from './nodes/Fragment';
import internal_exports from './internal_exports'; import internal_exports from './internal_exports';
import { Ast, CompileOptions, Var, Warning, CssResult } from '../interfaces'; import { Ast, CompileOptions, Var, Warning, CssResult, Attribute } from '../interfaces';
import error from '../utils/error'; import error from '../utils/error';
import get_code_frame from '../utils/get_code_frame'; import get_code_frame from '../utils/get_code_frame';
import flatten_reference from './utils/flatten_reference'; import flatten_reference from './utils/flatten_reference';
@ -26,7 +26,7 @@ import TemplateScope from './nodes/shared/TemplateScope';
import fuzzymatch from '../utils/fuzzymatch'; import fuzzymatch from '../utils/fuzzymatch';
import get_object from './utils/get_object'; import get_object from './utils/get_object';
import Slot from './nodes/Slot'; import Slot from './nodes/Slot';
import { Node, ImportDeclaration, ExportNamedDeclaration, Identifier, ExpressionStatement, AssignmentExpression, Literal, Property, RestElement, ExportDefaultDeclaration, ExportAllDeclaration, FunctionDeclaration, FunctionExpression, VariableDeclarator } from 'estree'; import { Node, ImportDeclaration, ExportNamedDeclaration, Identifier, ExpressionStatement, AssignmentExpression, Literal, Property, RestElement, ExportDefaultDeclaration, ExportAllDeclaration, FunctionDeclaration, FunctionExpression, VariableDeclarator, ObjectExpression } from 'estree';
import add_to_set from './utils/add_to_set'; 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, b } from 'code-red'; import { print, b } from 'code-red';
@ -38,13 +38,18 @@ import compiler_warnings from './compiler_warnings';
import compiler_errors from './compiler_errors'; import compiler_errors from './compiler_errors';
import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore'; import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore';
import check_enable_sourcemap from './utils/check_enable_sourcemap'; import check_enable_sourcemap from './utils/check_enable_sourcemap';
import Tag from './nodes/shared/Tag';
interface ComponentOptions { interface ComponentOptions {
namespace?: string; namespace?: string;
tag?: string;
immutable?: boolean; immutable?: boolean;
accessors?: boolean; accessors?: boolean;
preserveWhitespace?: boolean; preserveWhitespace?: boolean;
customElement?: {
tag: string | null;
shadow?: 'open' | 'none';
props?: Record<string, { attribute?: string; reflect?: boolean; type?: 'String' | 'Boolean' | 'Number' | 'Array' | 'Object' }>;
};
} }
const regex_leading_directory_separator = /^[/\\]/; const regex_leading_directory_separator = /^[/\\]/;
@ -110,6 +115,8 @@ export default class Component {
slots: Map<string, Slot> = new Map(); slots: Map<string, Slot> = new Map();
slot_outlets: Set<string> = new Set(); slot_outlets: Set<string> = new Set();
tags: Tag[] = [];
constructor( constructor(
ast: Ast, ast: Ast,
source: string, source: string,
@ -164,16 +171,7 @@ export default class Component {
this.component_options.namespace; this.component_options.namespace;
if (compile_options.customElement) { if (compile_options.customElement) {
if ( this.tag = this.component_options.customElement?.tag || compile_options.tag || this.name.name;
this.component_options.tag === undefined &&
compile_options.tag === undefined
) {
const svelteOptions = ast.html.children.find(
child => child.name === 'svelte:options'
) || { start: 0, end: 0 };
this.warn(svelteOptions, compiler_warnings.custom_element_no_tag);
}
this.tag = this.component_options.tag || compile_options.tag;
} else { } else {
this.tag = this.name.name; this.tag = this.name.name;
} }
@ -192,7 +190,7 @@ export default class Component {
this.pop_ignores(); this.pop_ignores();
this.elements.forEach(element => this.stylesheet.apply(element)); this.elements.forEach(element => this.stylesheet.apply(element));
if (!compile_options.customElement) this.stylesheet.reify(); this.stylesheet.reify();
this.stylesheet.warn_on_unused_selectors(this); this.stylesheet.warn_on_unused_selectors(this);
} }
@ -544,6 +542,9 @@ export default class Component {
extract_names(declarator.id).forEach(name => { extract_names(declarator.id).forEach(name => {
const variable = this.var_lookup.get(name); const variable = this.var_lookup.get(name);
variable.export_name = name; variable.export_name = name;
if (declarator.init?.type === 'Literal' && typeof declarator.init.value === 'boolean') {
variable.is_boolean = true;
}
if (!module_script && variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) { if (!module_script && variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) {
this.warn(declarator as any, compiler_warnings.unused_export_let(this.name.name, name)); this.warn(declarator as any, compiler_warnings.unused_export_let(this.name.name, name));
} }
@ -761,6 +762,7 @@ export default class Component {
this.hoist_instance_declarations(); this.hoist_instance_declarations();
this.extract_reactive_declarations(); this.extract_reactive_declarations();
this.check_if_tags_content_dynamic();
} }
post_template_walk() { post_template_walk() {
@ -1492,6 +1494,12 @@ export default class Component {
unsorted_reactive_declarations.forEach(add_declaration); unsorted_reactive_declarations.forEach(add_declaration);
} }
check_if_tags_content_dynamic() {
this.tags.forEach(tag => {
tag.check_if_content_dynamic();
});
}
warn_if_undefined(name: string, node, template_scope: TemplateScope) { warn_if_undefined(name: string, node, template_scope: TemplateScope) {
if (name[0] === '$') { if (name[0] === '$') {
if (name === '$' || name[1] === '$' && !is_reserved_keyword(name)) { if (name === '$' || name[1] === '$' && !is_reserved_keyword(name)) {
@ -1563,10 +1571,7 @@ function process_component_options(component: Component, nodes) {
if (attribute.type === 'Attribute') { if (attribute.type === 'Attribute') {
const { name } = attribute; const { name } = attribute;
switch (name) { function parse_tag(attribute: Attribute, tag: string) {
case 'tag': {
const tag = get_value(attribute, compiler_errors.invalid_tag_attribute);
if (typeof tag !== 'string' && tag !== null) { if (typeof tag !== 'string' && tag !== null) {
return component.error(attribute, compiler_errors.invalid_tag_attribute); return component.error(attribute, compiler_errors.invalid_tag_attribute);
} }
@ -1579,7 +1584,86 @@ function process_component_options(component: Component, nodes) {
component.warn(attribute, compiler_warnings.missing_custom_element_compile_options); component.warn(attribute, compiler_warnings.missing_custom_element_compile_options);
} }
component_options.tag = tag; component_options.customElement = component_options.customElement || {} as any;
component_options.customElement.tag = tag;
}
switch (name) {
case 'tag': {
component.warn(attribute, compiler_warnings.tag_option_deprecated);
parse_tag(attribute, get_value(attribute, compiler_errors.invalid_tag_attribute));
break;
}
case 'customElement': {
component_options.customElement = component_options.customElement || {} as any;
const { value } = attribute;
if (value[0].type === 'MustacheTag' && value[0].expression?.value === null) {
component_options.customElement.tag = null;
break;
} else if (value[0].type === 'Text') {
parse_tag(attribute, get_value(attribute, compiler_errors.invalid_tag_attribute));
break;
} else if (value[0].expression.type !== 'ObjectExpression') {
return component.error(attribute, compiler_errors.invalid_customElement_attribute);
}
const tag = value[0].expression.properties.find(
(prop: any) => prop.key.name === 'tag'
);
if (tag) {
parse_tag(tag, tag.value?.value);
} else {
return component.error(attribute, compiler_errors.invalid_customElement_attribute);
}
const props = value[0].expression.properties.find(
(prop: any) => prop.key.name === 'props'
);
if (props) {
const error = () => component.error(attribute, compiler_errors.invalid_props_attribute);
if (props.value?.type !== 'ObjectExpression') {
return error();
}
component_options.customElement.props = {};
for (const property of (props.value as ObjectExpression).properties) {
if (property.type !== 'Property' || property.computed || property.key.type !== 'Identifier' || property.value.type !== 'ObjectExpression') {
return error();
}
component_options.customElement.props[property.key.name] = {};
for (const prop of property.value.properties) {
if (prop.type !== 'Property' || prop.computed || prop.key.type !== 'Identifier' || prop.value.type !== 'Literal') {
return error();
}
if (['reflect', 'attribute', 'type'].indexOf(prop.key.name) === -1 ||
prop.key.name === 'type' && ['String', 'Number', 'Boolean', 'Array', 'Object'].indexOf(prop.value.value as string) === -1 ||
prop.key.name === 'reflect' && typeof prop.value.value !== 'boolean' ||
prop.key.name === 'attribute' && typeof prop.value.value !== 'string'
) {
return error();
}
component_options.customElement.props[property.key.name][prop.key.name] = prop.value.value;
}
}
}
const shadow = value[0].expression.properties.find(
(prop: any) => prop.key.name === 'shadow'
);
if (shadow) {
const shadowdom = shadow.value?.value;
if (shadowdom !== 'open' && shadowdom !== 'none') {
return component.error(shadow, compiler_errors.invalid_shadow_attribute);
}
component_options.customElement.shadow = shadowdom;
}
break; break;
} }
@ -1613,7 +1697,7 @@ function process_component_options(component: Component, nodes) {
} }
default: default:
return component.error(attribute, compiler_errors.invalid_options_attribute_unknown); return component.error(attribute, compiler_errors.invalid_options_attribute_unknown(name));
} }
} else { } else {
return component.error(attribute, compiler_errors.invalid_options_attribute); return component.error(attribute, compiler_errors.invalid_options_attribute);

@ -202,10 +202,24 @@ export default {
code: 'invalid-tag-property', code: 'invalid-tag-property',
message: "tag name must be two or more words joined by the '-' character" message: "tag name must be two or more words joined by the '-' character"
}, },
invalid_customElement_attribute: {
code: 'invalid-customElement-attribute',
message: "'customElement' must be a string literal defining a valid custom element name or an object of the form " +
"{ tag: string; shadow?: 'open' | 'none'; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }"
},
invalid_tag_attribute: { invalid_tag_attribute: {
code: 'invalid-tag-attribute', code: 'invalid-tag-attribute',
message: "'tag' must be a string literal" message: "'tag' must be a string literal"
}, },
invalid_shadow_attribute: {
code: 'invalid-shadow-attribute',
message: "'shadow' must be either 'open' or 'none'"
},
invalid_props_attribute: {
code: 'invalid-props-attribute',
message: "'props' must be a statically analyzable object literal of the form " +
"'{ [key: string]: { attribute?: string; reflect?: boolean; type?: 'String' | 'Boolean' | 'Number' | 'Array' | 'Object' }'"
},
invalid_namespace_property: (namespace: string, suggestion?: string) => ({ invalid_namespace_property: (namespace: string, suggestion?: string) => ({
code: 'invalid-namespace-property', code: 'invalid-namespace-property',
message: `Invalid namespace '${namespace}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '') message: `Invalid namespace '${namespace}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '')
@ -218,10 +232,10 @@ export default {
code: `invalid-${name}-value`, code: `invalid-${name}-value`,
message: `${name} attribute must be true or false` message: `${name} attribute must be true or false`
}), }),
invalid_options_attribute_unknown: { invalid_options_attribute_unknown: (name: string) => ({
code: 'invalid-options-attribute', code: 'invalid-options-attribute',
message: '<svelte:options> unknown attribute' message: `<svelte:options> unknown attribute '${name}'`
}, }),
invalid_options_attribute: { invalid_options_attribute: {
code: 'invalid-options-attribute', code: 'invalid-options-attribute',
message: "<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes" message: "<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes"

@ -6,9 +6,9 @@ import { ARIAPropertyDefinition } from 'aria-query';
* @internal * @internal
*/ */
export default { export default {
custom_element_no_tag: { tag_option_deprecated: {
code: 'custom-element-no-tag', code: 'tag-option-deprecated',
message: 'No custom element \'tag\' option was specified. To automatically register a custom element, specify a name with a hyphen in it, e.g. <svelte:options tag="my-thing"/>. To hide this warning, use <svelte:options tag={null}/>' message: "'tag' option is deprecated — use 'customElement' instead"
}, },
unused_export_let: (component: string, property: string) => ({ unused_export_let: (component: string, property: string) => ({
code: 'unused-export-let', code: 'unused-export-let',
@ -32,7 +32,7 @@ export default {
}), }),
missing_custom_element_compile_options: { missing_custom_element_compile_options: {
code: '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?" message: "The 'customElement' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?"
}, },
css_unused_selector: (selector: string) => ({ css_unused_selector: (selector: string) => ({
code: 'css-unused-selector', code: 'css-unused-selector',

@ -470,10 +470,46 @@ function get_element_parent(node: Element): Element | null {
return parent as Element | null; return parent as Element | null;
} }
/**
* Finds the given node's previous sibling in the DOM
*
* The Svelte <slot> is just a placeholder and is not actually real. Any children nodes
* in <slot> are 'flattened' and considered as the same level as the <slot>'s siblings
*
* e.g.
* <h1>Heading 1</h1>
* <slot>
* <h2>Heading 2</h2>
* </slot>
*
* is considered to look like:
* <h1>Heading 1</h1>
* <h2>Heading 2</h2>
*/
function find_previous_sibling(node: INode): INode {
let current_node: INode = node;
do {
if (current_node.type === 'Slot') {
const slot_children = current_node.children;
if (slot_children.length > 0) {
current_node = slot_children.slice(-1)[0]; // go to its last child first
continue;
}
}
while (!current_node.prev && current_node.parent && current_node.parent.type === 'Slot') {
current_node = current_node.parent;
}
current_node = current_node.prev;
} while (current_node && current_node.type === 'Slot');
return current_node;
}
function get_possible_element_siblings(node: INode, adjacent_only: boolean): Map<Element, NodeExist> { function get_possible_element_siblings(node: INode, adjacent_only: boolean): Map<Element, NodeExist> {
const result: Map<Element, NodeExist> = new Map(); const result: Map<Element, NodeExist> = new Map();
let prev: INode = node; let prev: INode = node;
while (prev = prev.prev) { while (prev = find_previous_sibling(prev)) {
if (prev.type === 'Element') { if (prev.type === 'Element') {
if (!prev.attributes.find(attr => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot')) { if (!prev.attributes.find(attr => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot')) {
result.set(prev, NodeExist.Definitely); result.set(prev, NodeExist.Definitely);

@ -407,7 +407,7 @@ export default class Stylesheet {
}); });
} }
render(file: string, should_transform_selectors: boolean) { render(file: string) {
if (!this.has_styles) { if (!this.has_styles) {
return { code: null, map: null }; return { code: null, map: null };
} }
@ -421,12 +421,10 @@ export default class Stylesheet {
} }
}); });
if (should_transform_selectors) {
const max = Math.max(...this.children.map(rule => rule.get_max_amount_class_specificity_increased())); const max = Math.max(...this.children.map(rule => rule.get_max_amount_class_specificity_increased()));
this.children.forEach((child: (Atrule | Rule)) => { this.children.forEach((child: (Atrule | Rule)) => {
child.transform(code, this.id, this.keyframes, max); child.transform(code, this.id, this.keyframes, max);
}); });
}
let c = 0; let c = 0;
this.children.forEach(child => { this.children.forEach(child => {

@ -59,6 +59,11 @@ export default class Attribute extends Node {
return expression; return expression;
}); });
} }
if (this.dependencies.size > 0) {
parent.cannot_use_innerhtml();
parent.not_static_content();
}
} }
get_dependencies() { get_dependencies() {

@ -27,6 +27,8 @@ export default class AwaitBlock extends Node {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression); this.expression = new Expression(component, this, scope, info.expression);

@ -33,6 +33,8 @@ export default class EachBlock extends AbstractBlock {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression); this.expression = new Expression(component, this, scope, info.expression);
this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring

@ -15,6 +15,7 @@ import { is_name_contenteditable, get_contenteditable_attr, has_contenteditable_
import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character, regex_box_size } from '../../utils/patterns'; import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character, regex_box_size } from '../../utils/patterns';
import fuzzymatch from '../../utils/fuzzymatch'; import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list'; import list from '../../utils/list';
import hash from '../utils/hash';
import Let from './Let'; import Let from './Let';
import TemplateScope from './shared/TemplateScope'; import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces'; import { INode } from './interfaces';
@ -503,6 +504,25 @@ export default class Element extends Node {
this.optimise(); this.optimise();
component.apply_stylesheet(this); component.apply_stylesheet(this);
if (this.parent) {
if (this.actions.length > 0 ||
this.animation ||
this.bindings.length > 0 ||
this.classes.length > 0 ||
this.intro || this.outro ||
this.handlers.length > 0 ||
this.styles.length > 0 ||
this.name === 'option' ||
this.is_dynamic_element ||
this.tag_expr.dynamic_dependencies().length ||
this.is_dynamic_element ||
component.compile_options.dev
) {
this.parent.cannot_use_innerhtml(); // need to use add_location
this.parent.not_static_content();
}
}
} }
validate() { validate() {
@ -1262,6 +1282,20 @@ export default class Element extends Node {
} }
}); });
} }
get can_use_textcontent() {
return this.is_static_content && this.children.every(node => node.type === 'Text' || node.type === 'MustacheTag');
}
get can_optimise_to_html_string() {
const can_use_textcontent = this.can_use_textcontent;
const is_template_with_text_content = this.name === 'template' && can_use_textcontent;
return !is_template_with_text_content && !this.namespace && (this.can_use_innerhtml || can_use_textcontent) && this.children.length > 0;
}
hash() {
return `svelte-${hash(this.component.source.slice(this.start, this.end))}`;
}
} }
const regex_starts_with_vowel = /^[aeiou]/; const regex_starts_with_vowel = /^[aeiou]/;

@ -15,6 +15,8 @@ export default class Head extends Node {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.cannot_use_innerhtml();
if (info.attributes.length) { if (info.attributes.length) {
component.error(info.attributes[0], compiler_errors.invalid_attribute_head); component.error(info.attributes[0], compiler_errors.invalid_attribute_head);
return; return;

@ -18,6 +18,8 @@ export default class IfBlock extends AbstractBlock {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.scope = scope.child(); this.scope = scope.child();
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, this.scope, info.expression); this.expression = new Expression(component, this, this.scope, info.expression);
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this)); ([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));

@ -28,6 +28,9 @@ export default class InlineComponent extends Node {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
if (info.name !== 'svelte:component' && info.name !== 'svelte:self') { if (info.name !== 'svelte:component' && info.name !== 'svelte:self') {
const name = info.name.split('.')[0]; // accommodate namespaces const name = info.name.split('.')[0]; // accommodate namespaces
component.warn_if_undefined(name, info, scope); component.warn_if_undefined(name, info, scope);

@ -13,6 +13,8 @@ export default class KeyBlock extends AbstractBlock {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression); this.expression = new Expression(component, this, scope, info.expression);

@ -2,4 +2,9 @@ import Tag from './shared/Tag';
export default class RawMustacheTag extends Tag { export default class RawMustacheTag extends Tag {
type: 'RawMustacheTag'; type: 'RawMustacheTag';
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
}
} }

@ -7,7 +7,8 @@ import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors'; import compiler_errors from '../compiler_errors';
export default class Slot extends Element { export default class Slot extends Element {
type: 'Element'; // @ts-ignore Slot elements have the 'Slot' type, but TypeScript doesn't allow us to have 'Slot' when it extends Element
type: 'Slot';
name: string; name: string;
children: INode[]; children: INode[];
slot_name: string; slot_name: string;
@ -60,5 +61,8 @@ export default class Slot extends Element {
} }
component.slots.set(this.slot_name, this); component.slots.set(this.slot_name, this);
this.cannot_use_innerhtml();
this.not_static_content();
} }
} }

@ -18,6 +18,7 @@ const elements_without_text = new Set([
]); ]);
const regex_ends_with_svg = /svg$/; const regex_ends_with_svg = /svg$/;
const regex_non_whitespace_characters = /[\S\u00A0]/;
export default class Text extends Node { export default class Text extends Node {
type: 'Text'; type: 'Text';
@ -63,4 +64,11 @@ export default class Text extends Node {
return false; return false;
} }
use_space(): boolean {
if (this.component.compile_options.preserveWhitespace) return false;
if (regex_non_whitespace_characters.test(this.data)) return false;
return !this.within_pre();
}
} }

@ -197,6 +197,15 @@ export default class Expression {
}); });
} }
dynamic_contextual_dependencies() {
return Array.from(this.contextual_dependencies).filter(name => {
return Array.from(this.template_scope.dependencies_for_name.get(name)).some(variable_name => {
const variable = this.component.var_lookup.get(variable_name);
return is_dynamic(variable);
});
});
}
// TODO move this into a render-dom wrapper? // TODO move this into a render-dom wrapper?
manipulate(block?: Block, ctx?: string | void) { manipulate(block?: Block, ctx?: string | void) {
// TODO ideally we wouldn't end up calling this method // TODO ideally we wouldn't end up calling this method

@ -15,6 +15,7 @@ export default class Node {
next?: INode; next?: INode;
can_use_innerhtml: boolean; can_use_innerhtml: boolean;
is_static_content: boolean;
var: string; var: string;
attributes: Attribute[]; attributes: Attribute[];
@ -33,6 +34,9 @@ export default class Node {
value: parent value: parent
} }
}); });
this.can_use_innerhtml = true;
this.is_static_content = true;
} }
cannot_use_innerhtml() { cannot_use_innerhtml() {
@ -42,6 +46,11 @@ export default class Node {
} }
} }
not_static_content() {
this.is_static_content = false;
if (this.parent) this.parent.not_static_content();
}
find_nearest(selector: RegExp) { find_nearest(selector: RegExp) {
if (selector.test(this.type)) return this; if (selector.test(this.type)) return this;
if (this.parent) return this.parent.find_nearest(selector); if (this.parent) return this.parent.find_nearest(selector);

@ -8,6 +8,9 @@ export default class Tag extends Node {
constructor(component, parent, scope, info) { constructor(component, parent, scope, info) {
super(component, parent, scope, info); super(component, parent, scope, info);
component.tags.push(this);
this.cannot_use_innerhtml();
this.expression = new Expression(component, this, scope, info.expression); this.expression = new Expression(component, this, scope, info.expression);
this.should_cache = ( this.should_cache = (
@ -15,4 +18,12 @@ export default class Tag extends Node {
(this.expression.dependencies.size && scope.names.has(info.expression.name)) (this.expression.dependencies.size && scope.names.has(info.expression.name))
); );
} }
is_dependencies_static() {
return this.expression.dynamic_contextual_dependencies().length === 0 && this.expression.dynamic_dependencies().length === 0;
}
check_if_content_dynamic() {
if (!this.is_dependencies_static()) {
this.not_static_content();
}
}
} }

@ -7,12 +7,11 @@ import { walk } from 'estree-walker';
import { extract_names, Scope } from 'periscopic'; import { extract_names, Scope } from 'periscopic';
import { invalidate } from './invalidate'; import { invalidate } from './invalidate';
import Block from './Block'; import Block from './Block';
import { ImportDeclaration, ClassDeclaration, FunctionExpression, Node, Statement, ObjectExpression, Expression } from 'estree'; import { ImportDeclaration, ClassDeclaration, Node, Statement, ObjectExpression, Expression } from 'estree';
import { apply_preprocessor_sourcemap } from '../../utils/mapped_code'; import { apply_preprocessor_sourcemap } from '../../utils/mapped_code';
import { flatten } from '../../utils/flatten'; import { flatten } from '../../utils/flatten';
import check_enable_sourcemap from '../utils/check_enable_sourcemap'; import check_enable_sourcemap from '../utils/check_enable_sourcemap';
import { push_array } from '../../utils/push_array'; import { push_array } from '../../utils/push_array';
import { regex_backslashes } from '../../utils/patterns';
export default function dom( export default function dom(
component: Component, component: Component,
@ -25,9 +24,6 @@ export default function dom(
block.has_outro_method = true; block.has_outro_method = true;
// prevent fragment being created twice (#1063)
if (options.customElement) block.chunks.create.push(b`this.c = @noop;`);
const body = []; const body = [];
if (renderer.file_var) { if (renderer.file_var) {
@ -35,7 +31,7 @@ export default function dom(
body.push(b`const ${renderer.file_var} = ${file};`); body.push(b`const ${renderer.file_var} = ${file};`);
} }
const css = component.stylesheet.render(options.filename, !options.customElement); const css = component.stylesheet.render(options.filename);
const css_sourcemap_enabled = check_enable_sourcemap(options.enableSourcemap, 'css'); const css_sourcemap_enabled = check_enable_sourcemap(options.enableSourcemap, 'css');
@ -52,9 +48,8 @@ export default function dom(
const add_css = component.get_unique_name('add_css'); const add_css = component.get_unique_name('add_css');
const should_add_css = ( const should_add_css = (
!options.customElement &&
!!styles && !!styles &&
options.css === 'injected' (options.customElement || options.css === 'injected')
); );
if (should_add_css) { if (should_add_css) {
@ -519,63 +514,6 @@ export default function dom(
} }
} }
if (options.customElement) {
let init_props = x`@attribute_to_object(this.attributes)`;
if (uses_slots) {
init_props = x`{ ...${init_props}, $$slots: @get_custom_elements_slots(this) }`;
}
const declaration = b`
class ${name} extends @SvelteElement {
constructor(options) {
super();
${css.code && b`
const style = document.createElement('style');
style.textContent = \`${css.code.replace(regex_backslashes, '\\\\')}${css_sourcemap_enabled && options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}\`
this.shadowRoot.appendChild(style)`}
@init(this, { target: this.shadowRoot, props: ${init_props}, customElement: true }, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, null, ${dirty});
if (options) {
if (options.target) {
@insert(options.target, this, options.anchor);
}
${(props.length > 0 || uses_props || uses_rest) && b`
if (options.props) {
this.$set(options.props);
@flush();
}`}
}
}
}
`[0] as ClassDeclaration;
if (props.length > 0) {
declaration.body.body.push({
type: 'MethodDefinition',
kind: 'get',
static: true,
computed: false,
key: { type: 'Identifier', name: 'observedAttributes' },
value: x`function() {
return [${props.map(prop => x`"${prop.export_name}"`)}];
}` as FunctionExpression
});
}
push_array(declaration.body.body, accessors);
body.push(declaration);
if (component.tag != null) {
body.push(b`
@_customElements.define("${component.tag}", ${name});
`);
}
} else {
const superclass = { const superclass = {
type: 'Identifier', type: 'Identifier',
name: options.dev ? '@SvelteComponentDev' : '@SvelteComponent' name: options.dev ? '@SvelteComponentDev' : '@SvelteComponent'
@ -602,8 +540,30 @@ export default function dom(
`[0] as ClassDeclaration; `[0] as ClassDeclaration;
push_array(declaration.body.body, accessors); push_array(declaration.body.body, accessors);
body.push(declaration); body.push(declaration);
if (options.customElement) {
const props_str = writable_props.reduce((def, prop) => {
def[prop.export_name] = component.component_options.customElement?.props?.[prop.export_name] || {};
if (prop.is_boolean && !def[prop.export_name].type) {
def[prop.export_name].type = 'Boolean';
}
return def;
}, {});
const slots_str = [...component.slots.keys()].map(key => `"${key}"`).join(',');
const accessors_str = accessors
.filter(accessor => !writable_props.some(prop => prop.export_name === accessor.key.name))
.map(accessor => `"${accessor.key.name}"`)
.join(',');
const use_shadow_dom = component.component_options.customElement?.shadow !== 'none' ? 'true' : 'false';
if (component.component_options.customElement?.tag) {
body.push(
b`@_customElements.define("${component.component_options.customElement.tag}", @create_custom_element(${name}, ${JSON.stringify(props_str)}, [${slots_str}], [${accessors_str}], ${use_shadow_dom}));`
);
} else {
body.push(b`@create_custom_element(${name}, ${JSON.stringify(props_str)}, [${slots_str}], [${accessors_str}], ${use_shadow_dom});`);
}
} }
return { js: flatten(body), css }; return { js: flatten(body), css };

@ -143,9 +143,6 @@ export default class AwaitBlockWrapper extends Wrapper {
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
block.add_dependencies(this.node.expression.dependencies); block.add_dependencies(this.node.expression.dependencies);
let is_dynamic = false; let is_dynamic = false;

@ -38,4 +38,10 @@ export default class CommentWrapper extends Wrapper {
parent_node parent_node
); );
} }
text() {
if (!this.renderer.options.preserveComments) return '';
return `<!--${this.node.data}-->`;
}
} }

@ -80,8 +80,6 @@ export default class EachBlockWrapper extends Wrapper {
next_sibling: Wrapper next_sibling: Wrapper
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
const { dependencies } = node.expression; const { dependencies } = node.expression;
block.add_dependencies(dependencies); block.add_dependencies(dependencies);

@ -36,9 +36,6 @@ export class BaseAttributeWrapper {
this.parent = parent; this.parent = parent;
if (node.dependencies.size > 0) { if (node.dependencies.size > 0) {
parent.cannot_use_innerhtml();
parent.not_static_content();
block.add_dependencies(node.dependencies); block.add_dependencies(node.dependencies);
} }
} }

@ -29,6 +29,7 @@ import is_dynamic from '../shared/is_dynamic';
import { is_name_contenteditable, has_contenteditable_attr } from '../../../utils/contenteditable'; import { is_name_contenteditable, has_contenteditable_attr } from '../../../utils/contenteditable';
import create_debugging_comment from '../shared/create_debugging_comment'; import create_debugging_comment from '../shared/create_debugging_comment';
import { push_array } from '../../../../utils/push_array'; import { push_array } from '../../../../utils/push_array';
import CommentWrapper from '../Comment';
interface BindingGroup { interface BindingGroup {
events: string[]; events: string[];
@ -288,24 +289,6 @@ export default class ElementWrapper extends Wrapper {
} }
}); });
if (this.parent) {
if (node.actions.length > 0 ||
node.animation ||
node.bindings.length > 0 ||
node.classes.length > 0 ||
node.intro || node.outro ||
node.handlers.length > 0 ||
node.styles.length > 0 ||
this.node.name === 'option' ||
node.tag_expr.dynamic_dependencies().length ||
node.is_dynamic_element ||
renderer.options.dev
) {
this.parent.cannot_use_innerhtml(); // need to use add_location
this.parent.not_static_content();
}
}
this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling); this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling);
this.element_data_name = block.get_unique_name(`${this.var.name}_data`); this.element_data_name = block.get_unique_name(`${this.var.name}_data`);
@ -445,6 +428,7 @@ export default class ElementWrapper extends Wrapper {
render_element(block: Block, parent_node: Identifier, parent_nodes: Identifier) { render_element(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
const { renderer } = this; const { renderer } = this;
const hydratable = renderer.options.hydratable;
if (this.node.name === 'noscript') return; if (this.node.name === 'noscript') return;
@ -458,13 +442,15 @@ export default class ElementWrapper extends Wrapper {
b`${node} = ${render_statement};` b`${node} = ${render_statement};`
); );
if (renderer.options.hydratable) { const { can_use_textcontent, can_optimise_to_html_string } = this.node;
if (hydratable) {
if (parent_nodes) { if (parent_nodes) {
block.chunks.claim.push(b` block.chunks.claim.push(b`
${node} = ${this.get_claim_statement(block, parent_nodes)}; ${node} = ${this.get_claim_statement(block, parent_nodes, can_optimise_to_html_string)};
`); `);
if (!this.void && this.node.children.length > 0) { if (!can_optimise_to_html_string && !this.void && this.node.children.length > 0) {
block.chunks.claim.push(b` block.chunks.claim.push(b`
var ${nodes} = ${children}; var ${nodes} = ${children};
`); `);
@ -502,15 +488,19 @@ export default class ElementWrapper extends Wrapper {
// insert static children with textContent or innerHTML // insert static children with textContent or innerHTML
// skip textcontent for <template>. append nodes to TemplateElement.content instead // skip textcontent for <template>. append nodes to TemplateElement.content instead
const can_use_textcontent = this.can_use_textcontent(); if (can_optimise_to_html_string) {
const is_template = this.node.name === 'template';
const is_template_with_text_content = is_template && can_use_textcontent;
if (!is_template_with_text_content && !this.node.namespace && (this.can_use_innerhtml || can_use_textcontent) && this.fragment.nodes.length > 0) {
if (this.fragment.nodes.length === 1 && this.fragment.nodes[0].node.type === 'Text') { if (this.fragment.nodes.length === 1 && this.fragment.nodes[0].node.type === 'Text') {
block.chunks.create.push( let text: Node = string_literal((this.fragment.nodes[0] as TextWrapper).data);
b`${node}.textContent = ${string_literal((this.fragment.nodes[0] as TextWrapper).data)};` if (hydratable) {
); const variable = block.get_unique_name('textContent');
block.add_variable(variable, text);
text = variable;
}
block.chunks.create.push(b`${node}.textContent = ${text};`);
if (hydratable) {
block.chunks.claim.push(b`if (@get_svelte_dataset(${node}) !== "${this.node.hash()}") ${node}.textContent = ${text};`);
}
} else { } else {
const state = { const state = {
quasi: { quasi: {
@ -519,25 +509,33 @@ export default class ElementWrapper extends Wrapper {
} }
}; };
const literal = { let literal: Node = {
type: 'TemplateLiteral', type: 'TemplateLiteral',
expressions: [], expressions: [],
quasis: [] quasis: []
}; };
const can_use_raw_text = !this.can_use_innerhtml && can_use_textcontent; const can_use_raw_text = !this.node.can_use_innerhtml && can_use_textcontent;
to_html((this.fragment.nodes as unknown as Array<ElementWrapper | TextWrapper>), block, literal, state, can_use_raw_text); to_html((this.fragment.nodes as unknown as Array<ElementWrapper | CommentWrapper | TextWrapper>), block, literal, state, can_use_raw_text);
literal.quasis.push(state.quasi); literal.quasis.push(state.quasi as any);
block.chunks.create.push( if (hydratable) {
b`${node}.${this.can_use_innerhtml ? 'innerHTML' : 'textContent'} = ${literal};` const variable = block.get_unique_name('textContent');
); block.add_variable(variable, literal);
literal = variable;
}
const property = this.node.can_use_innerhtml ? 'innerHTML' : 'textContent';
block.chunks.create.push(b`${node}.${property} = ${literal};`);
if (hydratable) {
block.chunks.claim.push(b`if (@get_svelte_dataset(${node}) !== "${this.node.hash()}") ${node}.${property} = ${literal};`);
}
} }
} else { } else {
this.fragment.nodes.forEach((child: Wrapper) => { this.fragment.nodes.forEach((child: Wrapper) => {
child.render( child.render(
block, block,
is_template ? x`${node}.content` : node, this.node.name === 'template' ? x`${node}.content` : node,
nodes, nodes,
{ element_data_name: this.element_data_name } { element_data_name: this.element_data_name }
); );
@ -566,7 +564,7 @@ export default class ElementWrapper extends Wrapper {
this.add_styles(block); this.add_styles(block);
this.add_manual_style_scoping(block); this.add_manual_style_scoping(block);
if (nodes && this.renderer.options.hydratable && !this.void) { if (nodes && hydratable && !this.void && !can_optimise_to_html_string) {
block.chunks.claim.push( block.chunks.claim.push(
b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);` b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);`
); );
@ -582,10 +580,6 @@ export default class ElementWrapper extends Wrapper {
block.renderer.dirty(this.node.tag_expr.dynamic_dependencies()); block.renderer.dirty(this.node.tag_expr.dynamic_dependencies());
} }
can_use_textcontent() {
return this.is_static_content && this.fragment.nodes.every(node => node.node.type === 'Text' || node.node.type === 'MustacheTag');
}
get_render_statement(block: Block) { get_render_statement(block: Block) {
const { name, namespace, tag_expr } = this.node; const { name, namespace, tag_expr } = this.node;
const reference = tag_expr.manipulate(block); const reference = tag_expr.manipulate(block);
@ -606,7 +600,7 @@ export default class ElementWrapper extends Wrapper {
return x`@element(${reference})`; return x`@element(${reference})`;
} }
get_claim_statement(block: Block, nodes: Identifier) { get_claim_statement(block: Block, nodes: Identifier, to_optimise_hydration: boolean) {
const attributes = this.attributes const attributes = this.attributes
.filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name) .filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name)
.map((attr) => p`${(attr as StyleAttributeWrapper | AttributeWrapper).name}: true`); .map((attr) => p`${(attr as StyleAttributeWrapper | AttributeWrapper).name}: true`);
@ -624,6 +618,10 @@ export default class ElementWrapper extends Wrapper {
reference = x`(${this.node.tag_expr.manipulate(block)} || 'null').toUpperCase()`; reference = x`(${this.node.tag_expr.manipulate(block)} || 'null').toUpperCase()`;
} }
if (to_optimise_hydration) {
attributes.push(p`["data-svelte-h"]: true`);
}
if (this.node.namespace === namespaces.svg) { if (this.node.namespace === namespaces.svg) {
return x`@claim_svg_element(${nodes}, ${reference}, { ${attributes} })`; return x`@claim_svg_element(${nodes}, ${reference}, { ${attributes} })`;
} else { } else {
@ -1288,13 +1286,19 @@ export default class ElementWrapper extends Wrapper {
const regex_backticks = /`/g; const regex_backticks = /`/g;
const regex_dollar_signs = /\$/g; const regex_dollar_signs = /\$/g;
function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapper | RawMustacheTagWrapper>, block: Block, literal: any, state: any, can_use_raw_text?: boolean) { function to_html(wrappers: Array<CommentWrapper | ElementWrapper | TextWrapper | MustacheTagWrapper | RawMustacheTagWrapper>, block: Block, literal: any, state: any, can_use_raw_text?: boolean) {
wrappers.forEach(wrapper => { wrappers.forEach(wrapper => {
if (wrapper instanceof TextWrapper) { if (wrapper instanceof CommentWrapper) {
state.quasi.value.raw += wrapper.text();
} else if (wrapper instanceof TextWrapper) {
// Don't add the <pre>/<textarea> newline logic here because pre/textarea.innerHTML // Don't add the <pre>/<textarea> newline logic here because pre/textarea.innerHTML
// would keep the leading newline, too, only someParent.innerHTML = '..<pre/textarea>..' won't // would keep the leading newline, too, only someParent.innerHTML = '..<pre/textarea>..' won't
if ((wrapper as TextWrapper).use_space()) state.quasi.value.raw += ' '; if (wrapper.use_space()) {
// use space instead of the text content
state.quasi.value.raw += ' ';
return;
}
const parent = wrapper.node.parent as Element; const parent = wrapper.node.parent as Element;

@ -20,8 +20,6 @@ export default class HeadWrapper extends Wrapper {
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.can_use_innerhtml = false;
this.fragment = new FragmentWrapper( this.fragment = new FragmentWrapper(
renderer, renderer,
block, block,

@ -105,9 +105,6 @@ export default class IfBlockWrapper extends Wrapper {
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
this.branches = []; this.branches = [];
const blocks: Block[] = []; const blocks: Block[] = [];

@ -44,9 +44,6 @@ export default class InlineComponentWrapper extends Wrapper {
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
if (this.node.expression) { if (this.node.expression) {
block.add_dependencies(this.node.expression.dependencies); block.add_dependencies(this.node.expression.dependencies);
} }

@ -24,9 +24,6 @@ export default class KeyBlockWrapper extends Wrapper {
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
this.dependencies = node.expression.dynamic_dependencies(); this.dependencies = node.expression.dynamic_dependencies();
if (this.dependencies.length) { if (this.dependencies.length) {

@ -20,8 +20,6 @@ export default class RawMustacheTagWrapper extends Tag {
node: MustacheTag | RawMustacheTag node: MustacheTag | RawMustacheTag
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
} }
render(block: Block, parent_node: Identifier, _parent_nodes: Identifier) { render(block: Block, parent_node: Identifier, _parent_nodes: Identifier) {

@ -30,8 +30,6 @@ export default class SlotWrapper extends Wrapper {
next_sibling: Wrapper next_sibling: Wrapper
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
if (this.node.children.length) { if (this.node.children.length) {
this.fallback = block.child({ this.fallback = block.child({

@ -5,11 +5,9 @@ import Wrapper from './shared/Wrapper';
import { x } from 'code-red'; import { x } from 'code-red';
import { Identifier } from 'estree'; import { Identifier } from 'estree';
const regex_non_whitespace_characters = /[\S\u00A0]/;
export default class TextWrapper extends Wrapper { export default class TextWrapper extends Wrapper {
node: Text; node: Text;
data: string; _data: string;
skip: boolean; skip: boolean;
var: Identifier; var: Identifier;
@ -23,15 +21,22 @@ export default class TextWrapper extends Wrapper {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.skip = this.node.should_skip(); this.skip = this.node.should_skip();
this.data = data; this._data = data;
this.var = (this.skip ? null : x`t`) as unknown as Identifier; this.var = (this.skip ? null : x`t`) as unknown as Identifier;
} }
use_space() { use_space() {
if (this.renderer.component.component_options.preserveWhitespace) return false; return this.node.use_space();
if (regex_non_whitespace_characters.test(this.data)) return false; }
return !this.node.within_pre(); set data(value: string) {
// when updating `this.data` during optimisation
// propagate the changes over to the underlying node
// so that the node.use_space reflects on the latest `data` value
this.node.data = this._data = value;
}
get data() {
return this._data;
} }
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {

@ -12,18 +12,9 @@ export default class Tag extends Wrapper {
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: MustacheTag | RawMustacheTag) { constructor(renderer: Renderer, block: Block, parent: Wrapper, node: MustacheTag | RawMustacheTag) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
if (!this.is_dependencies_static()) {
this.not_static_content();
}
block.add_dependencies(node.expression.dependencies); block.add_dependencies(node.expression.dependencies);
} }
is_dependencies_static() {
return this.node.expression.contextual_dependencies.size === 0 && this.node.expression.dynamic_dependencies().length === 0;
}
rename_this_method( rename_this_method(
block: Block, block: Block,
update: ((value: Node) => (Node | Node[])) update: ((value: Node) => (Node | Node[]))

@ -13,8 +13,6 @@ export default class Wrapper {
next: Wrapper | null; next: Wrapper | null;
var: Identifier; var: Identifier;
can_use_innerhtml: boolean;
is_static_content: boolean;
constructor( constructor(
renderer: Renderer, renderer: Renderer,
@ -35,22 +33,9 @@ export default class Wrapper {
} }
}); });
this.can_use_innerhtml = !renderer.options.hydratable;
this.is_static_content = !renderer.options.hydratable;
block.wrappers.push(this); block.wrappers.push(this);
} }
cannot_use_innerhtml() {
this.can_use_innerhtml = false;
if (this.parent) this.parent.cannot_use_innerhtml();
}
not_static_content() {
this.is_static_content = false;
if (this.parent) this.parent.not_static_content();
}
get_or_create_anchor(block: Block, parent_node: Identifier, parent_nodes: Identifier) { get_or_create_anchor(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
// TODO use this in EachBlock and IfBlock — tricky because // TODO use this in EachBlock and IfBlock — tricky because
// children need to be created first // children need to be created first

@ -48,6 +48,7 @@ const handlers: Record<string, Handler> = {
export interface RenderOptions extends CompileOptions{ export interface RenderOptions extends CompileOptions{
locate: (c: number) => { line: number; column: number }; locate: (c: number) => { line: number; column: number };
head_id?: string; head_id?: string;
has_added_svelte_hash?: boolean;
} }
export default class Renderer { export default class Renderer {

@ -158,6 +158,13 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
} }
}); });
if (options.hydratable) {
if (node.can_optimise_to_html_string && !options.has_added_svelte_hash) {
renderer.add_string(` data-svelte-h="${node.hash()}"`);
options = { ...options, has_added_svelte_hash: true };
}
}
renderer.add_string('>'); renderer.add_string('>');
if (node_contents !== undefined) { if (node_contents !== undefined) {

@ -5,7 +5,9 @@ import Element from '../../nodes/Element';
export default function(node: Text, renderer: Renderer, _options: RenderOptions) { export default function(node: Text, renderer: Renderer, _options: RenderOptions) {
let text = node.data; let text = node.data;
if ( if (node.use_space()) {
text = ' ';
} else if (
!node.parent || !node.parent ||
node.parent.type !== 'Element' || node.parent.type !== 'Element' ||
((node.parent as Element).name !== 'script' && (node.parent as Element).name !== 'style') ((node.parent as Element).name !== 'script' && (node.parent as Element).name !== 'style')

@ -39,6 +39,7 @@ export default function remove_whitespace_children(children: INode[], next?: INo
continue; continue;
} }
child.data = data;
nodes.unshift(child); nodes.unshift(child);
link(last_child, last_child = child); link(last_child, last_child = child);
} else { } else {

@ -33,7 +33,7 @@ export default function ssr(
// TODO concatenate CSS maps // TODO concatenate CSS maps
const css = options.customElement ? const css = options.customElement ?
{ code: null, map: null } : { code: null, map: null } :
component.stylesheet.render(options.filename, true); component.stylesheet.render(options.filename);
const uses_rest = component.var_lookup.has('$$restProps'); const uses_rest = component.var_lookup.has('$$restProps');
const props = component.vars.filter(variable => !variable.module && variable.export_name); const props = component.vars.filter(variable => !variable.module && variable.export_name);

@ -1,4 +1,5 @@
export function string_literal(data: string) { import { Literal } from 'estree';
export function string_literal(data: string): Literal {
return { return {
type: 'Literal', type: 'Literal',
value: data value: data

@ -206,7 +206,10 @@ export interface AppendTarget {
export interface Var { export interface Var {
name: string; name: string;
export_name?: string; // the `bar` in `export { foo as bar }` /** the `bar` in `export { foo as bar }` or `export let bar` */
export_name?: string;
/** true if assigned a boolean default value (`export let foo = true`) */
is_boolean?: boolean;
injected?: boolean; injected?: boolean;
module?: boolean; module?: boolean;
mutated?: boolean; mutated?: boolean;

@ -115,7 +115,7 @@ export default function tag(parser: Parser) {
: (regex_capital_letter.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent' : (regex_capital_letter.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
: name === 'svelte:fragment' ? 'SlotTemplate' : name === 'svelte:fragment' ? 'SlotTemplate'
: name === 'title' && parent_is_head(parser.stack) ? 'Title' : name === 'title' && parent_is_head(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element'; : name === 'slot' ? 'Slot' : 'Element';
const element: TemplateNode = { const element: TemplateNode = {
start, start,

@ -1,9 +1,10 @@
import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components } from './scheduler'; import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components } from './scheduler';
import { current_component, set_current_component } from './lifecycle'; import { current_component, set_current_component } from './lifecycle';
import { blank_object, is_empty, is_function, run, run_all, noop } from './utils'; import { blank_object, is_empty, is_function, run, run_all, noop } from './utils';
import { children, detach, start_hydrating, end_hydrating } from './dom'; import { children, detach, start_hydrating, end_hydrating, get_custom_elements_slots, insert } from './dom';
import { transition_in } from './transitions'; import { transition_in } from './transitions';
import { T$$ } from './types'; import { T$$ } from './types';
import { ComponentType } from './dev';
export function bind(component, name, callback) { export function bind(component, name, callback) {
const index = component.$$.props[name]; const index = component.$$.props[name];
@ -21,12 +22,11 @@ export function claim_component(block, parent_nodes) {
block && block.l(parent_nodes); block && block.l(parent_nodes);
} }
export function mount_component(component, target, anchor, customElement) { export function mount_component(component, target, anchor) {
const { fragment, after_update } = component.$$; const { fragment, after_update } = component.$$;
fragment && fragment.m(target, anchor); fragment && fragment.m(target, anchor);
if (!customElement) {
// onMount happens before the initial afterUpdate // onMount happens before the initial afterUpdate
add_render_callback(() => { add_render_callback(() => {
@ -43,7 +43,6 @@ export function mount_component(component, target, anchor, customElement) {
} }
component.$$.on_mount = []; component.$$.on_mount = [];
}); });
}
after_update.forEach(add_render_callback); after_update.forEach(add_render_callback);
} }
@ -137,7 +136,7 @@ export function init(component, options, instance, create_fragment, not_equal, p
} }
if (options.intro) transition_in(component.$$.fragment); if (options.intro) transition_in(component.$$.fragment);
mount_component(component, options.target, options.anchor, options.customElement); mount_component(component, options.target, options.anchor);
end_hydrating(); end_hydrating();
flush(); flush();
} }
@ -148,59 +147,257 @@ export function init(component, options, instance, create_fragment, not_equal, p
export let SvelteElement; export let SvelteElement;
if (typeof HTMLElement === 'function') { if (typeof HTMLElement === 'function') {
SvelteElement = class extends HTMLElement { SvelteElement = class extends HTMLElement {
$$: T$$; private $$component?: SvelteComponent;
$$set?: ($$props: any) => void; private $$connected = false;
constructor() { private $$data = {};
private $$reflecting = false;
private $$props_definition: Record<string, CustomElementPropDefinition> = {};
private $$listeners: Record<string, Function[]> = {};
private $$listener_unsubscribe_fns = new Map<Function, Function>();
constructor(
private $$componentCtor: ComponentType,
private $$slots: string[],
use_shadow_dom: boolean
) {
super(); super();
if (use_shadow_dom) {
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
} }
}
connectedCallback() { addEventListener(type: string, listener: any, options?: any): void {
const { on_mount } = this.$$; // We can't determine upfront if the event is a custom event or not, so we have to
this.$$.on_disconnect = on_mount.map(run).filter(is_function); // listen to both. If someone uses a custom event with the same name as a regular
// browser event, this fires twice - we can't avoid that.
this.$$listeners[type] = this.$$listeners[type] || [];
this.$$listeners[type].push(listener);
if (this.$$component) {
const unsub = this.$$component!.$on(type, listener);
this.$$listener_unsubscribe_fns.set(listener, unsub);
}
super.addEventListener(type, listener, options);
}
// @ts-ignore todo: improve typings removeEventListener(type: string, listener: any, options?: any): void {
for (const key in this.$$.slotted) { super.removeEventListener(type, listener, options);
// @ts-ignore todo: improve typings if (this.$$component) {
this.appendChild(this.$$.slotted[key]); const unsub = this.$$listener_unsubscribe_fns.get(listener);
if (unsub) {
unsub();
this.$$listener_unsubscribe_fns.delete(listener);
}
} }
} }
attributeChangedCallback(attr, _oldValue, newValue) { async connectedCallback() {
this[attr] = newValue; this.$$connected = true;
if (!this.$$component) {
// We wait one tick to let possible child slot elements be created/mounted
await Promise.resolve();
if (!this.$$connected) {
return;
} }
disconnectedCallback() { function create_slot(name: string) {
run_all(this.$$.on_disconnect); return () => {
let node: HTMLSlotElement;
const obj = {
c: function create() {
node = document.createElement('slot');
if (name !== 'default') {
node.setAttribute('name', name);
}
},
m: function mount(target: HTMLElement, anchor?: HTMLElement) {
insert(target, node, anchor);
},
d: function destroy(detaching: boolean) {
if (detaching) {
detach(node);
}
}
};
return obj;
};
} }
$destroy() { const $$slots: Record<string, any> = {};
destroy_component(this, 1); const existing_slots = get_custom_elements_slots(this);
this.$destroy = noop; for (const name of this.$$slots) {
if (name in existing_slots) {
$$slots[name] = [create_slot(name)];
}
} }
$on(type, callback) { for (const attribute of this.attributes) {
// TODO should this delegate to addEventListener? // this.$$data takes precedence over this.attributes
if (!is_function(callback)) { const name = this.$$get_prop_name(attribute.name);
return noop; if (!(name in this.$$data)) {
this.$$data[name] = get_custom_element_value(name, attribute.value, this.$$props_definition, 'toProp');
}
} }
const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
callbacks.push(callback);
return () => { this.$$component = new this.$$componentCtor({
const index = callbacks.indexOf(callback); target: this.shadowRoot || this,
if (index !== -1) callbacks.splice(index, 1); props: {
...this.$$data,
$$slots,
$$scope: {
ctx: []
}
}
});
for (const type in this.$$listeners) {
for (const listener of this.$$listeners[type]) {
const unsub = this.$$component!.$on(type, listener);
this.$$listener_unsubscribe_fns.set(listener, unsub);
}
}
this.$$listeners = {};
}
}
// We don't need this when working within Svelte code, but for compatibility of people using this outside of Svelte
// and setting attributes through setAttribute etc, this is helpful
attributeChangedCallback(attr: string, _oldValue: any, newValue: any) {
if (this.$$reflecting) return;
attr = this.$$get_prop_name(attr);
this.$$data[attr] = get_custom_element_value(attr, newValue, this.$$props_definition, 'toProp');
this.$$component!.$set({ [attr]: this.$$data[attr] });
}
disconnectedCallback() {
this.$$connected = false;
// In a microtask, because this could be a move within the DOM
Promise.resolve().then(() => {
if (!this.$$connected) {
this.$$component!.$destroy();
this.$$component = undefined;
}
});
}
private $$get_prop_name(attribute_name: string): string {
return Object.keys(this.$$props_definition).find(
key => this.$$props_definition[key].attribute === attribute_name ||
(!this.$$props_definition[key].attribute && key.toLowerCase() === attribute_name)
) || attribute_name;
}
}; };
} }
$set($$props) { function get_custom_element_value(prop: string, value: any, props_definition: Record<string, CustomElementPropDefinition>, transform?: 'toAttribute' | 'toProp') {
if (this.$$set && !is_empty($$props)) { const type = props_definition[prop]?.type;
this.$$.skip_bound = true; value = type === 'Boolean' && typeof value !== 'boolean' ? value != null : value;
this.$$set($$props); if (!transform || !props_definition[prop]) {
this.$$.skip_bound = false; return value;
} else if (transform === 'toAttribute') {
switch (type) {
case 'Object':
case 'Array':
return value == null ? null : JSON.stringify(value);
case 'Boolean':
return value ? '' : null;
case 'Number':
return value == null ? null : value;
default:
return value;
}
} else {
switch (type) {
case 'Object':
case 'Array':
return value && JSON.parse(value);
case 'Boolean':
return value; // conversion already handled above
case 'Number':
return value != null ? +value : value;
default:
return value;
}
}
}
interface CustomElementPropDefinition {
attribute?: string;
reflect?: boolean;
type?: 'String' | 'Boolean' | 'Number' | 'Array' | 'Object';
} }
/**
* @internal
*
* Turn a Svelte component into a custom element.
* @param Component A Svelte component constructor
* @param props_definition The props to observe
* @param slots The slots to create
* @param accessors Other accessors besides the ones for props the component has
* @param use_shadow_dom Whether to use shadow DOM
* @returns A custom element class
*/
export function create_custom_element(
Component: ComponentType,
props_definition: Record<string, CustomElementPropDefinition>,
slots: string[],
accessors: string[],
use_shadow_dom: boolean
) {
const Class = class extends SvelteElement {
constructor() {
super(Component, slots, use_shadow_dom);
this.$$props_definition = props_definition;
}
static get observedAttributes() {
return Object.keys(props_definition).map(key => (props_definition[key].attribute || key).toLowerCase());
} }
}; };
Object.keys(props_definition).forEach((prop) => {
Object.defineProperty(Class.prototype, prop, {
get() {
return this.$$component && prop in this.$$component
? this.$$component[prop]
: this.$$data[prop];
},
set(value) {
value = get_custom_element_value(prop, value, props_definition);
this.$$data[prop] = value;
this.$$component?.$set({ [prop]: value });
if (props_definition[prop].reflect) {
this.$$reflecting = true;
const attribute_value = get_custom_element_value(prop, value, props_definition, 'toAttribute');
if (attribute_value == null) {
this.removeAttribute(prop);
} else {
this.setAttribute(
props_definition[prop].attribute || prop,
attribute_value as string
);
}
this.$$reflecting = false;
}
}
});
});
accessors.forEach(accessor => {
Object.defineProperty(Class.prototype, accessor, {
get() {
return this.$$component?.[accessor];
}
});
});
Component.element = Class as any;
return Class;
} }
/** /**

@ -157,10 +157,13 @@ export function construct_svelte_component_dev(component, props) {
} }
} }
type Props = Record<string, any>; export interface SvelteComponentDev<
export interface SvelteComponentDev { Props extends Record<string, any> = any,
$set(props?: Props): void; Events extends Record<string, any> = any,
$on(event: string, callback: ((event: any) => void) | null | undefined): () => void; Slots extends Record<string, any> = any // eslint-disable-line @typescript-eslint/no-unused-vars
> {
$set(props?: Partial<Props>): void;
$on<K extends Extract<keyof Events, string>>(type: K, callback: ((e: Events[K]) => void) | null | undefined): () => void;
$destroy(): void; $destroy(): void;
[accessor: string]: any; [accessor: string]: any;
} }
@ -177,8 +180,33 @@ export interface ComponentConstructorOptions<Props extends Record<string, any> =
/** /**
* Base class for Svelte components with some minor dev-enhancements. Used when dev=true. * Base class for Svelte components with some minor dev-enhancements. Used when dev=true.
*
* Can be used to create strongly typed Svelte components.
*
* ### Example:
*
* You have component library on npm called `component-library`, from which
* you export a component called `MyComponent`. For Svelte+TypeScript users,
* you want to provide typings. Therefore you create a `index.d.ts`:
* ```ts
* import { SvelteComponent } from "svelte";
* export class MyComponent extends SvelteComponent<{foo: string}> {}
* ```
* Typing this makes it possible for IDEs like VS Code with the Svelte extension
* to provide intellisense and to use the component like this in a Svelte file
* with TypeScript:
* ```svelte
* <script lang="ts">
* import { MyComponent } from "component-library";
* </script>
* <MyComponent foo={'bar'} />
* ```
*/ */
export class SvelteComponentDev extends SvelteComponent { export class SvelteComponentDev<
Props extends Record<string, any> = any,
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> extends SvelteComponent {
/** /**
* @private * @private
* For type checking capabilities only. * For type checking capabilities only.
@ -192,16 +220,16 @@ export class SvelteComponentDev extends SvelteComponent {
* Does not exist at runtime. * Does not exist at runtime.
* ### DO NOT USE! * ### DO NOT USE!
*/ */
$$events_def: any; $$events_def: Events;
/** /**
* @private * @private
* For type checking capabilities only. * For type checking capabilities only.
* Does not exist at runtime. * Does not exist at runtime.
* ### DO NOT USE! * ### DO NOT USE!
*/ */
$$slot_def: any; $$slot_def: Slots;
constructor(options: ComponentConstructorOptions) { constructor(options: ComponentConstructorOptions<Props>) {
if (!options || (!options.target && !options.$$inline)) { if (!options || (!options.target && !options.$$inline)) {
throw new Error("'target' is a required option"); throw new Error("'target' is a required option");
} }
@ -221,82 +249,21 @@ export class SvelteComponentDev extends SvelteComponent {
$inject_state() {} $inject_state() {}
} }
// TODO https://github.com/microsoft/TypeScript/issues/41770 is the reason // eslint-disable-next-line @typescript-eslint/no-empty-interface
// why we have to split out SvelteComponentTyped to not break existing usage of SvelteComponent.
// Try to find a better way for Svelte 4.0.
export interface SvelteComponentTyped< export interface SvelteComponentTyped<
Props extends Record<string, any> = any, Props extends Record<string, any> = any,
Events extends Record<string, any> = any, Events extends Record<string, any> = any,
Slots extends Record<string, any> = any // eslint-disable-line @typescript-eslint/no-unused-vars Slots extends Record<string, any> = any
> { > extends SvelteComponentDev<Props, Events, Slots> {}
$set(props?: Partial<Props>): void;
$on<K extends Extract<keyof Events, string>>(type: K, callback: ((e: Events[K]) => void) | null | undefined): () => void;
$destroy(): void;
[accessor: string]: any;
}
/** /**
* Base class to create strongly typed Svelte components. * @deprecated Use `SvelteComponent` instead. See PR for more information: https://github.com/sveltejs/svelte/pull/8512
* This only exists for typing purposes and should be used in `.d.ts` files.
*
* ### Example:
*
* You have component library on npm called `component-library`, from which
* you export a component called `MyComponent`. For Svelte+TypeScript users,
* you want to provide typings. Therefore you create a `index.d.ts`:
* ```ts
* import { SvelteComponentTyped } from "svelte";
* export class MyComponent extends SvelteComponentTyped<{foo: string}> {}
* ```
* Typing this makes it possible for IDEs like VS Code with the Svelte extension
* to provide intellisense and to use the component like this in a Svelte file
* with TypeScript:
* ```svelte
* <script lang="ts">
* import { MyComponent } from "component-library";
* </script>
* <MyComponent foo={'bar'} />
* ```
*
* #### Why not make this part of `SvelteComponent(Dev)`?
* Because
* ```ts
* class ASubclassOfSvelteComponent extends SvelteComponent<{foo: string}> {}
* const component: typeof SvelteComponent = ASubclassOfSvelteComponent;
* ```
* will throw a type error, so we need to separate the more strictly typed class.
*/ */
export class SvelteComponentTyped< export class SvelteComponentTyped<
Props extends Record<string, any> = any, Props extends Record<string, any> = any,
Events extends Record<string, any> = any, Events extends Record<string, any> = any,
Slots extends Record<string, any> = any Slots extends Record<string, any> = any
> extends SvelteComponentDev { > extends SvelteComponentDev<Props, Events, Slots> {}
/**
* @private
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
*/
$$prop_def: Props;
/**
* @private
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
*/
$$events_def: Events;
/**
* @private
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
*/
$$slot_def: Slots;
constructor(options: ComponentConstructorOptions<Props>) {
super(options);
}
}
/** /**
* Convenience type to get the type of a Svelte component. Useful for example in combination with * Convenience type to get the type of a Svelte component. Useful for example in combination with
@ -305,23 +272,26 @@ export class SvelteComponentTyped<
* Example: * Example:
* ```html * ```html
* <script lang="ts"> * <script lang="ts">
* import type { ComponentType, SvelteComponentTyped } from 'svelte'; * import type { ComponentType, SvelteComponent } from 'svelte';
* import Component1 from './Component1.svelte'; * import Component1 from './Component1.svelte';
* import Component2 from './Component2.svelte'; * import Component2 from './Component2.svelte';
* *
* const component: ComponentType = someLogic() ? Component1 : Component2; * const component: ComponentType = someLogic() ? Component1 : Component2;
* const componentOfCertainSubType: ComponentType<SvelteComponentTyped<{ needsThisProp: string }>> = someLogic() ? Component1 : Component2; * const componentOfCertainSubType: ComponentType<SvelteComponent<{ needsThisProp: string }>> = someLogic() ? Component1 : Component2;
* </script> * </script>
* *
* <svelte:component this={component} /> * <svelte:component this={component} />
* <svelte:component this={componentOfCertainSubType} needsThisProp="hello" /> * <svelte:component this={componentOfCertainSubType} needsThisProp="hello" />
* ``` * ```
*/ */
export type ComponentType<Component extends SvelteComponentTyped = SvelteComponentTyped> = new ( export type ComponentType<Component extends SvelteComponentDev = SvelteComponentDev> = (new (
options: ComponentConstructorOptions< options: ComponentConstructorOptions<
Component extends SvelteComponentTyped<infer Props> ? Props : Record<string, any> Component extends SvelteComponentDev<infer Props> ? Props : Record<string, any>
> >
) => Component; ) => Component) & {
/** The custom element version of the component. Only present if compiled with the `customElement` compiler option */
element?: typeof HTMLElement
};
/** /**
* Convenience type to get the props the given component expects. Example: * Convenience type to get the props the given component expects. Example:
@ -334,7 +304,7 @@ export type ComponentType<Component extends SvelteComponentTyped = SvelteCompone
* </script> * </script>
* ``` * ```
*/ */
export type ComponentProps<Component extends SvelteComponent> = Component extends SvelteComponentTyped<infer Props> export type ComponentProps<Component extends SvelteComponent> = Component extends SvelteComponentDev<infer Props>
? Props ? Props
: never; : never;
@ -354,7 +324,7 @@ export type ComponentProps<Component extends SvelteComponent> = Component extend
* ``` * ```
*/ */
export type ComponentEvents<Component extends SvelteComponent> = export type ComponentEvents<Component extends SvelteComponent> =
Component extends SvelteComponentTyped<any, infer Events> ? Events : never; Component extends SvelteComponentDev<any, infer Events> ? Events : never;
export function loop_guard(timeout) { export function loop_guard(timeout) {
const start = Date.now(); const start = Date.now();

@ -157,6 +157,12 @@ export function get_root_for_style(node: Node): ShadowRoot | Document {
export function append_empty_stylesheet(node: Node) { export function append_empty_stylesheet(node: Node) {
const style_element = element('style') as HTMLStyleElement; const style_element = element('style') as HTMLStyleElement;
// For transitions to work without 'style-src: unsafe-inline' Content Security Policy,
// these empty tags need to be allowed with a hash as a workaround until we move to the Web Animations API.
// Using the hash for the empty string (for an empty tag) works in all browsers except Safari.
// So as a workaround for the workaround, when we append empty style tags we set their content to /* empty */.
// The hash 'sha256-9OlNO0DNEeaVzHL4RZwCLsBHA8WBQ8toBp/4F5XV2nc=' will then work even in Safari.
style_element.textContent = '/* empty */';
append_stylesheet(get_root_for_style(node), style_element); append_stylesheet(get_root_for_style(node), style_element);
return style_element.sheet as CSSStyleSheet; return style_element.sheet as CSSStyleSheet;
} }
@ -362,6 +368,10 @@ export function xlink_attr(node, attribute, value) {
node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value); node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
} }
export function get_svelte_dataset(node: HTMLElement) {
return node.dataset.svelteH;
}
export function get_binding_group_value(group, __value, checked) { export function get_binding_group_value(group, __value, checked) {
const value = new Set(); const value = new Set();
for (let i = 0; i < group.length; i += 1) { for (let i = 0; i < group.length; i += 1) {

@ -108,12 +108,18 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list
} }
export function validate_each_keys(ctx, list, get_context, get_key) { export function validate_each_keys(ctx, list, get_context, get_key) {
const keys = new Set(); const keys = new Map();
for (let i = 0; i < list.length; i++) { for (let i = 0; i < list.length; i++) {
const key = get_key(get_context(ctx, list, i)); const key = get_key(get_context(ctx, list, i));
if (keys.has(key)) { if (keys.has(key)) {
throw new Error('Cannot have duplicate keys in a keyed each'); let value = '';
try {
value = `with value '${String(key)}' `;
} catch (e) {
// can't stringify
} }
keys.add(key); throw new Error(`Cannot have duplicate keys in a keyed each: Keys at index ${keys.get(key)} and ${i} ${value}are duplicates`);
}
keys.set(key, i);
} }
} }

@ -68,6 +68,9 @@ export function validate_store(store, name) {
export function subscribe(store, ...callbacks) { export function subscribe(store, ...callbacks) {
if (store == null) { if (store == null) {
for (const callback of callbacks) {
callback(undefined);
}
return noop; return noop;
} }
const unsub = store.subscribe(...callbacks); const unsub = store.subscribe(...callbacks);

@ -17,10 +17,11 @@ type Invalidator<T> = (value?: T) => void;
* This function is called when the first subscriber subscribes. * This function is called when the first subscriber subscribes.
* *
* @param {(value: T) => void} set Function that sets the value of the store. * @param {(value: T) => void} set Function that sets the value of the store.
* @param {(value: Updater<T>) => void} set Function that sets the value of the store after passing the current value to the update function.
* @returns {void | (() => void)} Optionally, a cleanup function that is called when the last remaining * @returns {void | (() => void)} Optionally, a cleanup function that is called when the last remaining
* subscriber unsubscribes. * subscriber unsubscribes.
*/ */
export type StartStopNotifier<T> = (set: (value: T) => void) => void | (() => void); export type StartStopNotifier<T> = (set: (value: T) => void, update: (fn: Updater<T>) => void) => void | (() => void);
/** Readable interface for subscribing. */ /** Readable interface for subscribing. */
export interface Readable<T> { export interface Readable<T> {
@ -99,7 +100,7 @@ export function writable<T>(value?: T, start: StartStopNotifier<T> = noop): Writ
const subscriber: SubscribeInvalidateTuple<T> = [run, invalidate]; const subscriber: SubscribeInvalidateTuple<T> = [run, invalidate];
subscribers.add(subscriber); subscribers.add(subscriber);
if (subscribers.size === 1) { if (subscribers.size === 1) {
stop = start(set) || noop; stop = start(set, update) || noop;
} }
run(value); run(value);
@ -132,7 +133,7 @@ type StoresValues<T> = T extends Readable<infer U> ? U :
*/ */
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>, set: Subscriber<T>, update: (fn: Updater<T>) => void) => Unsubscriber | void,
initial_value?: T initial_value?: T
): Readable<T>; ): Readable<T>;
@ -164,13 +165,14 @@ export function derived<S extends Stores, 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> {
const single = !Array.isArray(stores); const single = !Array.isArray(stores);
const stores_array: Array<Readable<any>> = single const stores_array: Array<Readable<any>> = single ? [stores as Readable<any>] : stores as Array<Readable<any>>;
? [stores as Readable<any>] if (!stores_array.every(Boolean)) {
: stores as Array<Readable<any>>; throw new Error('derived() expects stores as input, got a falsy value');
}
const auto = fn.length < 2; const auto = fn.length < 2;
return readable(initial_value, (set) => { return readable(initial_value, (set, update) => {
let started = false; let started = false;
const values = []; const values = [];
@ -182,7 +184,7 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
return; return;
} }
cleanup(); cleanup();
const result = fn(single ? values[0] : values, set); const result = fn(single ? values[0] : values, set, update);
if (auto) { if (auto) {
set(result as T); set(result as T);
} else { } else {

@ -0,0 +1,12 @@
<slot>
<h1>Heading 1</h1>
</slot>
<span>Span 1</span>
<span>Span 2</span>
<p>Paragraph 2</p>
<style>
h1 ~ p {
color: red;
}
</style>

@ -0,0 +1,7 @@
<slot><slot><h1>Heading 1</h1></slot></slot><span>Span 1</span><span>Span 2</span><slot><slot><p>Paragraph 2</p></slot></slot>
<style>
h1 ~ p {
color: red;
}
</style>

@ -0,0 +1,18 @@
<slot>
<slot>
<h1>Heading 1</h1>
</slot>
</slot>
<span>Span 1</span>
<span>Span 2</span>
<slot>
<slot>
<p>Paragraph 2</p>
</slot>
</slot>
<style>
h1 ~ p {
color: red;
}
</style>

@ -0,0 +1,12 @@
<h1>Heading 1</h1>
<span>Span 1</span>
<span>Span 2</span>
<slot>
<p>Paragraph 2</p>
</slot>
<style>
h1 ~ p {
color: red;
}
</style>

@ -0,0 +1 @@
h1.svelte-xyz~span.svelte-xyz{color:green}h1.svelte-xyz~p.svelte-xyz{color:red}span.svelte-xyz~p.svelte-xyz{color:blue}

@ -0,0 +1,22 @@
<h1>Heading 1</h1>
<slot>
<span>Span 1</span>
</slot>
<slot>
<span>Span 2</span>
</slot>
<p>Paragraph 2</p>
<style>
h1 ~ span {
color: green;
}
h1 ~ p {
color: red;
}
span ~ p {
color: blue;
}
</style>

@ -0,0 +1,10 @@
<slot>
<h1>test</h1>
</slot>
<span>Hello</span>
<style>
h1 + span {
color: red;
}
</style>

@ -0,0 +1,7 @@
<slot><slot><slot><h1>test</h1></slot></slot></slot><slot><slot><span>Hello</span></slot></slot>
<style>
h1 + span {
color: red;
}
</style>

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

@ -0,0 +1,18 @@
<slot>
<slot>
<slot>
<h1>test</h1>
</slot>
</slot>
</slot>
<slot>
<slot>
<span>Hello</span>
</slot>
</slot>
<style>
h1 + span {
color: red;
}
</style>

@ -0,0 +1,10 @@
<h1>test</h1>
<slot>
<span>Hello</span>
</slot>
<style>
h1 + span {
color: red;
}
</style>

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

@ -0,0 +1,11 @@
<h1>test</h1>
<slot name="a"></slot>
<slot name="b"></slot>
<slot name="c"></slot>
<span>Hello</span>
<style>
h1 + span {
color: red;
}
</style>

@ -81,6 +81,7 @@ describe('custom-elements', function() {
const bundle = await rollup({ const bundle = await rollup({
input: `${__dirname}/samples/${dir}/test.js`, input: `${__dirname}/samples/${dir}/test.js`,
plugins: [ plugins: [
// @ts-ignore -- TODO: fix this
{ {
resolveId(importee) { resolveId(importee) {
if (importee === 'svelte/internal' || importee === './internal') { if (importee === 'svelte/internal' || importee === './internal') {

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

@ -1,8 +1,10 @@
import * as assert from 'assert'; import * as assert from 'assert';
import { tick } from 'svelte';
import './main.svelte'; import './main.svelte';
export default function (target) { export default async function (target) {
target.innerHTML = '<custom-element name="world" answer="42" test="svelte"></custom-element>'; target.innerHTML = '<custom-element name="world" answer="42" test="svelte"></custom-element>';
await tick();
const el = target.querySelector('custom-element'); const el = target.querySelector('custom-element');
assert.htmlEqual(el.shadowRoot.innerHTML, ` assert.htmlEqual(el.shadowRoot.innerHTML, `

@ -0,0 +1,10 @@
<svelte:options customElement={null} />
<script>
import "./my-widget.svelte";
export let name;
</script>
<my-widget>
<p>default {name}</p>
</my-widget>

@ -0,0 +1,4 @@
<svelte:options customElement="my-widget" />
<slot>fallback</slot>
<slot name="named"><p>named fallback</p></slot>

@ -0,0 +1,22 @@
import * as assert from 'assert';
import { tick } from 'svelte';
import Component from './main.svelte';
export default async function (target) {
const component = new Component({ target, props: { name: 'slot' } });
await tick();
await tick();
const ce = target.querySelector('my-widget');
assert.htmlEqual(ce.shadowRoot.innerHTML, `
<slot></slot>
<p>named fallback</p>
`);
component.name = 'slot2';
assert.htmlEqual(ce.shadowRoot.innerHTML, `
<slot></slot>
<p>named fallback</p>
`);
}

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

@ -1,11 +1,13 @@
import * as assert from 'assert'; import * as assert from 'assert';
import { tick } from 'svelte';
import './main.svelte'; import './main.svelte';
export default function (target) { export default async function (target) {
target.innerHTML = ` target.innerHTML = `
<custom-element><span slot="a">hello world</span><span>bye</span><span>world</span></custom-element> <custom-element><span slot="a">hello world</span><span>bye</span><span>world</span></custom-element>
<custom-element><span slot="a">hello world</span><span slot="b">hello world</span><span>bye world</span></custom-element> <custom-element><span slot="a">hello world</span><span slot="b">hello world</span><span>bye world</span></custom-element>
`; `;
await tick();
const [a, b] = target.querySelectorAll('custom-element'); const [a, b] = target.querySelectorAll('custom-element');

@ -0,0 +1,20 @@
<svelte:options customElement="custom-element" />
<script>
export let name;
export let events = [];
function action(_node, name) {
events.push(name);
return {
update(name) {
events.push(name);
},
destroy() {
events.push("destroy");
},
};
}
</script>
<div use:action={name}>action</div>

@ -0,0 +1,19 @@
import * as assert from 'assert';
import { tick } from 'svelte';
import './main.svelte';
export default async function (target) {
target.innerHTML = '<custom-element name="foo"></custom-element>';
await tick();
const el = target.querySelector('custom-element');
const events = el.events; // need to get the array reference, else it's gone when destroyed
assert.deepEqual(events, ['foo']);
el.name = 'bar';
await tick();
assert.deepEqual(events, ['foo', 'bar']);
target.innerHTML = '';
await tick();
assert.deepEqual(events, ['foo', 'bar', 'destroy']);
}

@ -0,0 +1,21 @@
<svelte:options
customElement={{
tag: "custom-element",
props: {
camelCase: { attribute: "camel-case" },
camelCase2: { reflect: true },
anArray: { attribute: "an-array", type: "Array", reflect: true },
},
}}
/>
<script>
export let camelCase;
export let camelCase2;
export let anArray;
</script>
<h1>{camelCase2} {camelCase}!</h1>
{#each anArray as item}
<p>{item}</p>
{/each}

@ -0,0 +1,25 @@
import * as assert from 'assert';
import { tick } from 'svelte';
import './main.svelte';
export default async function (target) {
target.innerHTML = '<custom-element camelcase2="Hello" camel-case="world" an-array="[1,2]"></custom-element>';
await tick();
const el = target.querySelector('custom-element');
assert.equal(el.shadowRoot.innerHTML, '<h1>Hello world!</h1> <p>1</p><p>2</p>');
el.setAttribute('camel-case', 'universe');
el.setAttribute('an-array', '[3,4]');
el.setAttribute('camelcase2', 'Hi');
await tick();
assert.equal(el.shadowRoot.innerHTML, '<h1>Hi universe!</h1> <p>3</p><p>4</p>');
assert.equal(target.innerHTML, '<custom-element camelcase2="Hi" camel-case="universe" an-array="[3,4]"></custom-element>');
el.camelCase = 'galaxy';
el.camelCase2 = 'Hey';
el.anArray = [5, 6];
await tick();
assert.equal(el.shadowRoot.innerHTML, '<h1>Hey galaxy!</h1> <p>5</p><p>6</p>');
assert.equal(target.innerHTML, '<custom-element camelcase2="Hey" camel-case="universe" an-array="[5,6]"></custom-element>');
}

@ -0,0 +1,14 @@
<svelte:options
customElement={{
tag: "custom-element",
props: {
name: { reflect: false, type: "String", attribute: "name" },
},
}}
/>
<script>
export let name;
</script>
<h1>Hello {name}!</h1>

@ -0,0 +1,13 @@
import * as assert from 'assert';
import { tick } from 'svelte';
import './main.svelte';
export default async function (target) {
target.innerHTML = '<custom-element name="world"></custom-element>';
await tick();
const el = target.querySelector('custom-element');
const h1 = el.shadowRoot.querySelector('h1');
assert.equal(h1.textContent, 'Hello world!');
}

@ -1,4 +1,4 @@
<svelte:options tag="custom-element"/> <svelte:options customElement="custom-element" />
<script> <script>
export function updateFoo(value) { export function updateFoo(value) {

@ -1,8 +1,10 @@
import * as assert from 'assert'; import * as assert from 'assert';
import { tick } from 'svelte';
import './main.svelte'; import './main.svelte';
export default async function (target) { export default async function (target) {
target.innerHTML = '<custom-element></custom-element>'; target.innerHTML = '<custom-element></custom-element>';
await tick();
const el = target.querySelector('custom-element'); const el = target.querySelector('custom-element');
await el.updateFoo(42); await el.updateFoo(42);

@ -1,9 +1,9 @@
<svelte:options tag="custom-element"/> <svelte:options customElement="custom-element" />
<span class='icon'></span> <span class="icon" />
<style> <style>
.icon::before { .icon::before {
content: '\ff' content: "\ff";
} }
</style> </style>

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

Loading…
Cancel
Save