Merge branch 'main' into svelte-html

svelte-html
Simon H 2 years ago committed by GitHub
commit 4cf2077ecb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: avoid mutation validation for invalidate_inner_signals

@ -42,3 +42,20 @@ A convenient way to find the origin of some change is to pass `console.trace` to
// @errors: 2304 // @errors: 2304
$inspect(stuff).with(console.trace); $inspect(stuff).with(console.trace);
``` ```
## $inspect.trace(...)
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.
```svelte
<script>
import { doSomeWork } from './elsewhere';
$effect(() => {
+++$inspect.trace();+++
doSomeWork();
});
</script>
```
`$inspect.trace` takes an optional first argument which will be used as the label.

@ -112,7 +112,7 @@ Snippets can reference themselves and each other ([demo](/playground/untitled#H4
## Passing snippets to components ## Passing snippets to components
Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/playground/untitled#H4sIAAAAAAAAE41SwY6bMBD9lRGplKQlYRMpF5ZF7T_0ttmDwSZYJbZrT9pGlv-9g4Fkk-xhxYV5vHlvhjc-aWQnXJK_-kSxo0jy5IcxSZrg2fSF-yM6FFQ7fbJ1jxSuttJguVd7lEejLcJPVnUCGquPMF9nsVoPjfNnohGx1sohMU4SHbzAa4_t0UNvmcOcGUNDzFP4jeccdikYK2v6sIWQ3lErpui5cDdPF_LmkVy3wlp5Vd5e2U_rHYSe_kYjFtl1KeVnTkljBEIrGBd2sYy8AtsyLlBk9DYhJHtTR_UbBDWybkR8NkqHWyOr_y74ZMNLz9f9AoG6ePkOJLMHLBp-xISvcPf11r0YUuMM2Ysfkgngh5XphUYKkJWU_FFz2UjBkxztSYT0cihR4LOn0tGaPrql439N-7Uh0Dl8MVYbt1jeJ1Fg7xDb_Uw2Y18YQqZ_S2U5FH1pS__dCkWMa3C0uR0pfQRTg89kE4bLLLDS_Dxy_Eywuo1TAnPAw4fqY1rvtH3W9w35ZZMgvU3jq8LhedwkguCHRhT_cMU6eVA5dKLB5wGutCWjlTOslupAxxrxceKoD2hzhe2qbmXHF1v1bbOcNCtW_zpYfVI8h5kQ4qY3mueHTlesW2C7TOEO4hcdwzgf3Nc7cZxUKKC4yuNhvIX_MlV_Xk0EAAA=)): Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/playground/untitled#H4sIAAAAAAAAE3VS247aMBD9lZGpBGwDASRegonaPvQL2qdlH5zYEKvBNvbQLbL875VzAcKyj3PmzJnLGU8UOwqSkd8KJdaCk4TsZS0cyV49wYuJuQiQpGd-N2bu_ooaI1YwJ57hpVYoFDqSEepKKw3mO7VDeTTaIvxiRS1gb_URxvO0ibrS8WanIrHUyiHs7Vmigy28RmyHHmKvDMbMmFq4cQInvGSwTsBYWYoMVhCSB2rBFFPsyl0uruTlR3JZCWvlTXl1Yy_mawiR_rbZKZrellJ-5JQ0RiBUgnFhJ9OGR7HKmwVoilXeIye8DOJGfYCgRlZ3iE876TBsZPX7hPdteO75PC4QaIo8vwNPePmANQ2fMeEFHrLD7rR1jTNkW986E8C3KwfwVr8HSHOSEBT_kGRozyIkn_zQveXDL3rIfPJHtUDwzShJd_Qk3gQCbOGLsdq4yfTRJopRuin3I7nv6kL7ARRjmLdBDG3uv1mhuLA3V2mKtqNEf_oCn8p9aN-WYqH5peP4kWBl1UwJzAEPT9U7K--0fRrrWnPTXpCm1_EVdXjpNmlA8G1hPPyM1fKgMqjFHjctXGjLhZ05w0qpDhksGrybuNEHtJnCalZWsuaTlfq6nPaaBSv_HKw-K57BjzOiVj9ZKQYKzQjZodYFqydYTRN4gPhVzTDO2xnma3HsVWjaLjT8nbfwHy7Q5f2dBAAA)):
```svelte ```svelte
<script> <script>
@ -144,7 +144,7 @@ Within the template, snippets are values just like any other. As such, they can
Think about it like passing content instead of data to a component. The concept is similar to slots in web components. Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/playground/untitled#H4sIAAAAAAAAE41Sy27bMBD8lYVcwHYrW4kBXxRFaP-htzgHSqQsojLJkuu2BqF_74qUrfhxCHQRh7MzO9z1SSM74ZL8zSeKHUSSJz-MSdIET2Y4uD-iQ0Fnp4-2HpDC1VYaLHdqh_JgtEX4yapOQGP1AebrLJzWsXD-QjQi1lo5JMZRooNXeBuwHXoYLHOYM2OoiXkKv_GUwzYFY2VNFxvo0xtqxRR9F-7z04X8fE-uW2GtnJQ3E_tpvYV-oL9Ti0U2hVJFjMMZslcfW-5DWj9zShojEFrBuLCLZR_9CmzLQCwy-psw8rxBgvkNhhpZd8F8NppE7Stbq_8u-GTKS8_XQ9Keqnl5BZP1AzTYP2bDV7i7_9hLEeda0iocNJeNFDzJ0R5Fn142JzA-uzsdBfLhldPxPdMhIPS0H1-M1cYtlnejwdBDfBXZjHXTFOg4BhuOtvTfrVDEmAZG2ew5ezYV-Ew2fVzVAivNTyPHzwSr29AlMAe8f6g-zuWDts-GusAmdBSkv3P7qnB4GpMEEHwsRPEPV6yTe5VDJxp8iXClLRmtnGG1VHva3oCPHQd9QJsrbFd1Kzu-2Khvz8uzZsXqX3urj4rnMBNCXNUG83zf6Yp1C2yXKdxA_KJjGOfRfb0Vh7MKDShEuV-M9_4_nq6svF4EAAA=)): As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/playground/untitled#H4sIAAAAAAAAE3VSTa_aMBD8Kyu_SkAbCA-JSzBR20N_QXt6vIMTO8SqsY29tI2s_PcqTiB8vaPHs7MzuxuIZgdBMvJLo0QlOElIJZXwJHsLBBvb_XUASc7Mb9Yu_B-hsMMK5sUzvDQahUZPMkJ96aTFfKd3KA_WOISfrFACKmcOMFmk8TWUTjY73RFLoz1C5U4SPWzhrcN2GKDrlcGEWauEnyRwxCaDdQLWyVJksII2uaMWTDPNLtzX5YX8-kgua-GcHJVXI3u5WEPb0d83O03TMZSmfRzOkG1Db7mNacOL19JagVALxoWbztq-H8U6j0SaYp2P2BGbOyQ2v8PQIFMXLKRDk177pq0zf6d8bMrzwBdd0pamyPMb-IjNEzS2f86Gz_Dwf-2F9nvNSUJQ_EOSoTuJNvngqK5v4Pas7n4-OCwlEEJcQTIMO-nSQwtb-GSdsX46e9gbRoP9yGQ11I0rEuycunu6PHx1QnPhxm3SFN15MOlYEFJZtf0dUywMbwZOeBGsrKNLYB54-1R9WNqVdki7usim6VmQphf7mnpshiQRhNAXdoOfMyX3OgMlKtz0cGEcF27uLSul3mewjPjgOOoDukxjPS9rqfh0pb-8zs6aBSt_7505aZ7B9xOi0T9YKW4UooVsr0zB1BTrWQJ3EL-oWcZ572GxFoezCk37QLe3897-B2i2U62uBAAA)):
```svelte ```svelte
<!-- this is semantically the same as the above --> <!-- this is semantically the same as the above -->
@ -165,7 +165,7 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
</Table> </Table>
``` ```
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/playground/untitled#H4sIAAAAAAAAE41S247aMBD9lVFYCegGsiDxks1G7T_0bdkHJ3aI1cR27aEtsvzvtZ0LZeGhiiJ5js-cmTMemzS8YybJ320iSM-SPPmmVJImeFEhML9Yh8zHRp51HZDC1JorLI_iiLxXUiN8J1XHoNGyh-U2i9F2SFy-epon1lIY9IwzRwNv8B6wI1oIJXNYEqV8E8sUfuIlh0MKSvPaX-zBpZ-oFRH-m7m7l5m8uyfXLdOaX5X3V_bL9gAu0D98i0V2NSWKwQ4lSN7s0LKLbgtsyxgXmT9NiBe-iaP-DYISSTcj4bcLI7hSDEHL3yu6dkPfBdLS0m1o3nk-LW9gX-gBGss9ZsMXuLu32VjZBdfRaelft5eUN5zRJEd9Zi6dlyEy_ncdOm_IxsGlULe8o5qJNFgE5x_9SWmpzGp9N2-MXQxz4c2cOQ-lZWQyF0Jd2q_-mjI9U1fr4FBPE8iuKTbjjRt2sMBK0svIsQtG6jb2CsQAdQ_1x9f5R9tmIS-yPToK-tNkQRQGL6ObCIIdEpH9wQ3p-Enk0LEGXwe4ktoX2hhFai5Ofi0jPnYc9QF1LrDdRK-rvXjerSfNitQ_TlqeBc1hwRi7yY3F81MnK9KtsF2n8Amis44ilA7VtwfWTyr-kaKV-_X4cH8BTOhfRzcEAAA=)): Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/playground/untitled#H4sIAAAAAAAAE3WOQQrCMBBFrzIMggql3ddY1Du4si5sOmIwnYRkFKX07lKqglqX8_7_w2uRDw1hjlsWI5ZqTPBoLEXMdy3K3fdZDzB5Ndfep_FKVnpWHSKNce1YiCVijirqYLwUJQOYxrsgsLmIOIZjcA1M02w4n-PpomSVvTclqyEutDX6DA2pZ7_ABIVugrmEC3XJH92P55_G39GodCmWBFrQJ2PrQAwdLGHig_NxNv9xrQa1dhWIawrv1Wzeqawa8953D-8QOmaEAQAA)):
```svelte ```svelte
<!--- file: App.svelte ---> <!--- file: App.svelte --->

@ -33,19 +33,22 @@ Note that unlike calling `new App(...)` in Svelte 4, things like effects (includ
## `unmount` ## `unmount`
Unmounts a component created with [`mount`](#mount) or [`hydrate`](#hydrate): Unmounts a component that was previously created with [`mount`](#mount) or [`hydrate`](#hydrate).
If `options.outro` is `true`, [transitions](transition) will play before the component is removed from the DOM:
```js ```js
// @errors: 1109
import { mount, unmount } from 'svelte'; import { mount, unmount } from 'svelte';
import App from './App.svelte'; import App from './App.svelte';
const app = mount(App, {...}); const app = mount(App, { target: document.body });
// later // later
unmount(app); unmount(app, { outro: true });
``` ```
Returns a `Promise` that resolves after transitions have completed if `options.outro` is true, or immediately otherwise.
## `render` ## `render`
Only available on the server and when compiling with the `server` option. Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app: Only available on the server and when compiling with the `server` option. Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app:

@ -442,6 +442,18 @@ Expected whitespace
Imports of `svelte/internal/*` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from `svelte/internal/*` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case Imports of `svelte/internal/*` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from `svelte/internal/*` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case
``` ```
### inspect_trace_generator
```
`$inspect.trace(...)` cannot be used inside a generator function
```
### inspect_trace_invalid_placement
```
`$inspect.trace(...)` must be the first statement of a function body
```
### invalid_arguments_usage ### invalid_arguments_usage
``` ```

@ -775,7 +775,7 @@ Reassignments of module-level declarations will not cause reactive statements to
``` ```
```svelte ```svelte
<script ---context="module"--- +++context+++> <script ---context="module"--- +++module+++>
let foo = 'bar'; let foo = 'bar';
</script> </script>
``` ```

@ -1,5 +1,43 @@
# svelte # svelte
## 5.14.0
### Minor Changes
- feat: adds $inspect.trace rune ([#14290](https://github.com/sveltejs/svelte/pull/14290))
## 5.13.0
### Minor Changes
- feat: add `outro` option to `unmount` ([#14540](https://github.com/sveltejs/svelte/pull/14540))
- feat: provide loose parser mode ([#14691](https://github.com/sveltejs/svelte/pull/14691))
## 5.12.0
### Minor Changes
- feat: expose more AST types from `"svelte/compiler"` ([#14601](https://github.com/sveltejs/svelte/pull/14601))
### Patch Changes
- fix: don't add parenthesis to media query if already present ([#14699](https://github.com/sveltejs/svelte/pull/14699))
- fix: ensure if block paths retain correct template namespacing ([#14685](https://github.com/sveltejs/svelte/pull/14685))
## 5.11.3
### Patch Changes
- fix: allow unquoted slash in attributes ([#14615](https://github.com/sveltejs/svelte/pull/14615))
- fix: better handle hydration of script/style elements ([#14683](https://github.com/sveltejs/svelte/pull/14683))
- fix: make `defaultValue` work with spread ([#14640](https://github.com/sveltejs/svelte/pull/14640))
- fix: avoid mutation validation for invalidate_inner_signals ([#14688](https://github.com/sveltejs/svelte/pull/14688))
## 5.11.2 ## 5.11.2
### Patch Changes ### Patch Changes

@ -54,6 +54,14 @@
> Imports of `svelte/internal/*` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from `svelte/internal/*` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case > Imports of `svelte/internal/*` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from `svelte/internal/*` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case
## inspect_trace_generator
> `$inspect.trace(...)` cannot be used inside a generator function
## inspect_trace_invalid_placement
> `$inspect.trace(...)` must be the first statement of a function body
## invalid_arguments_usage ## invalid_arguments_usage
> The arguments keyword cannot be used within the template or at the top level of a component > The arguments keyword cannot be used within the template or at the top level of a component

@ -57,7 +57,7 @@ This code will work when the component is rendered on the client (which is why t
> `context="module"` is deprecated, use the `module` attribute instead > `context="module"` is deprecated, use the `module` attribute instead
```svelte ```svelte
<script ---context="module"--- +++context+++> <script ---context="module"--- +++module+++>
let foo = 'bar'; let foo = 'bar';
</script> </script>
``` ```

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.11.2", "version": "5.14.0",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {

@ -371,6 +371,25 @@ declare function $inspect<T extends any[]>(
...values: T ...values: T
): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void }; ): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void };
declare namespace $inspect {
/**
* Tracks which reactive state changes caused an effect to re-run. Must be the first
* statement of a function body. Example:
*
* ```svelte
* <script>
* let count = $state(0);
*
* $effect(() => {
* $inspect.trace('my effect');
*
* count;
* });
* </script>
*/
export function trace(name: string): void;
}
/** /**
* Retrieves the `this` reference of the custom element that contains this component. Example: * Retrieves the `this` reference of the custom element that contains this component. Example:
* *

@ -206,6 +206,24 @@ export function import_svelte_internal_forbidden(node) {
e(node, "import_svelte_internal_forbidden", `Imports of \`svelte/internal/*\` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from \`svelte/internal/*\` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case\nhttps://svelte.dev/e/import_svelte_internal_forbidden`); e(node, "import_svelte_internal_forbidden", `Imports of \`svelte/internal/*\` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from \`svelte/internal/*\` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case\nhttps://svelte.dev/e/import_svelte_internal_forbidden`);
} }
/**
* `$inspect.trace(...)` cannot be used inside a generator function
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function inspect_trace_generator(node) {
e(node, "inspect_trace_generator", `\`$inspect.trace(...)\` cannot be used inside a generator function\nhttps://svelte.dev/e/inspect_trace_generator`);
}
/**
* `$inspect.trace(...)` must be the first statement of a function body
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function inspect_trace_invalid_placement(node) {
e(node, "inspect_trace_invalid_placement", `\`$inspect.trace(...)\` must be the first statement of a function body\nhttps://svelte.dev/e/inspect_trace_invalid_placement`);
}
/** /**
* The arguments keyword cannot be used within the template or at the top level of a component * The arguments keyword cannot be used within the template or at the top level of a component
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node

@ -76,7 +76,7 @@ export function compileModule(source, options) {
* *
* @overload * @overload
* @param {string} source * @param {string} source
* @param {{ filename?: string; modern: true }} options * @param {{ filename?: string; modern: true; loose?: boolean }} options
* @returns {AST.Root} * @returns {AST.Root}
*/ */
@ -88,7 +88,7 @@ export function compileModule(source, options) {
* *
* @overload * @overload
* @param {string} source * @param {string} source
* @param {{ filename?: string; modern?: false }} [options] * @param {{ filename?: string; modern?: false; loose?: boolean }} [options]
* @returns {Record<string, any>} * @returns {Record<string, any>}
*/ */
@ -98,16 +98,18 @@ export function compileModule(source, options) {
* The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST. * The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST.
* `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7. * `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7.
* *
* The `loose` option, available since 5.13.0, tries to always return an AST even if the input will not successfully compile.
*
* @param {string} source * @param {string} source
* @param {{ filename?: string; rootDir?: string; modern?: boolean }} [options] * @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options]
* @returns {AST.Root | LegacyRoot} * @returns {AST.Root | LegacyRoot}
*/ */
export function parse(source, { filename, rootDir, modern } = {}) { export function parse(source, { filename, rootDir, modern, loose } = {}) {
source = remove_bom(source); source = remove_bom(source);
state.reset_warning_filter(() => false); state.reset_warning_filter(() => false);
state.reset(source, { filename: filename ?? '(unknown)', rootDir }); state.reset(source, { filename: filename ?? '(unknown)', rootDir });
const ast = _parse(source); const ast = _parse(source, loose);
return to_public_ast(source, ast, modern); return to_public_ast(source, ast, modern);
} }

@ -1,5 +1,5 @@
/** @import { Expression } from 'estree' */ /** @import { Expression } from 'estree' */
/** @import { AST, SvelteNode, TemplateNode } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import * as Legacy from './types/legacy-nodes.js' */ /** @import * as Legacy from './types/legacy-nodes.js' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { import {
@ -11,7 +11,7 @@ import { extract_svelte_ignore } from './utils/extract_svelte_ignore.js';
/** /**
* Some of the legacy Svelte AST nodes remove whitespace from the start and end of their children. * Some of the legacy Svelte AST nodes remove whitespace from the start and end of their children.
* @param {TemplateNode[]} nodes * @param {AST.TemplateNode[]} nodes
*/ */
function remove_surrounding_whitespace_nodes(nodes) { function remove_surrounding_whitespace_nodes(nodes) {
const first = nodes.at(0); const first = nodes.at(0);
@ -40,7 +40,7 @@ function remove_surrounding_whitespace_nodes(nodes) {
* @returns {Legacy.LegacyRoot} * @returns {Legacy.LegacyRoot}
*/ */
export function convert(source, ast) { export function convert(source, ast) {
const root = /** @type {SvelteNode | Legacy.LegacySvelteNode} */ (ast); const root = /** @type {AST.SvelteNode | Legacy.LegacySvelteNode} */ (ast);
return /** @type {Legacy.LegacyRoot} */ ( return /** @type {Legacy.LegacyRoot} */ (
walk(root, null, { walk(root, null, {

@ -2,7 +2,7 @@
/** @import { Visitors } from 'zimmerframe' */ /** @import { Visitors } from 'zimmerframe' */
/** @import { ComponentAnalysis } from '../phases/types.js' */ /** @import { ComponentAnalysis } from '../phases/types.js' */
/** @import { Scope, ScopeRoot } from '../phases/scope.js' */ /** @import { Scope, ScopeRoot } from '../phases/scope.js' */
/** @import { AST, Binding, SvelteNode, ValidatedCompileOptions } from '#compiler' */ /** @import { AST, Binding, ValidatedCompileOptions } from '#compiler' */
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { parse } from '../phases/1-parse/index.js'; import { parse } from '../phases/1-parse/index.js';
@ -479,7 +479,7 @@ export function migrate(source, { filename, use_ts } = {}) {
* }} State * }} State
*/ */
/** @type {Visitors<SvelteNode, State>} */ /** @type {Visitors<AST.SvelteNode, State>} */
const instance_script = { const instance_script = {
_(node, { state, next }) { _(node, { state, next }) {
// @ts-expect-error // @ts-expect-error
@ -1050,7 +1050,7 @@ function trim_block(state, start, end) {
} }
} }
/** @type {Visitors<SvelteNode, State>} */ /** @type {Visitors<AST.SvelteNode, State>} */
const template = { const template = {
Identifier(node, { state, path }) { Identifier(node, { state, path }) {
handle_identifier(node, state, path); handle_identifier(node, state, path);
@ -1410,7 +1410,7 @@ const template = {
/** /**
* @param {AST.RegularElement | AST.SvelteElement | AST.SvelteComponent | AST.Component | AST.SlotElement | AST.SvelteFragment} node * @param {AST.RegularElement | AST.SvelteElement | AST.SvelteComponent | AST.Component | AST.SlotElement | AST.SvelteFragment} node
* @param {SvelteNode[]} path * @param {AST.SvelteNode[]} path
* @param {State} state * @param {State} state
*/ */
function migrate_slot_usage(node, path, state) { function migrate_slot_usage(node, path, state) {
@ -1580,7 +1580,7 @@ function migrate_slot_usage(node, path, state) {
/** /**
* @param {VariableDeclarator} declarator * @param {VariableDeclarator} declarator
* @param {State} state * @param {State} state
* @param {SvelteNode[]} path * @param {AST.SvelteNode[]} path
*/ */
function extract_type_and_comment(declarator, state, path) { function extract_type_and_comment(declarator, state, path) {
const str = state.str; const str = state.str;

@ -1,4 +1,4 @@
/** @import { AST, TemplateNode } from '#compiler' */ /** @import { AST } from '#compiler' */
// @ts-expect-error acorn type definitions are borked in the release we use // @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn'; import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js'; import fragment from './state/fragment.js';
@ -22,13 +22,26 @@ export class Parser {
*/ */
template; template;
/**
* @readonly
* @type {string}
*/
template_untrimmed;
/**
* Whether or not we're in loose parsing mode, in which
* case we try to continue parsing as much as possible
* @type {boolean}
*/
loose;
/** */ /** */
index = 0; index = 0;
/** Whether we're parsing in TypeScript mode */ /** Whether we're parsing in TypeScript mode */
ts = false; ts = false;
/** @type {TemplateNode[]} */ /** @type {AST.TemplateNode[]} */
stack = []; stack = [];
/** @type {AST.Fragment[]} */ /** @type {AST.Fragment[]} */
@ -43,12 +56,17 @@ export class Parser {
/** @type {LastAutoClosedTag | undefined} */ /** @type {LastAutoClosedTag | undefined} */
last_auto_closed_tag; last_auto_closed_tag;
/** @param {string} template */ /**
constructor(template) { * @param {string} template
* @param {boolean} loose
*/
constructor(template, loose) {
if (typeof template !== 'string') { if (typeof template !== 'string') {
throw new TypeError('Template must be a string'); throw new TypeError('Template must be a string');
} }
this.loose = loose;
this.template_untrimmed = template;
this.template = template.trimEnd(); this.template = template.trimEnd();
let match_lang; let match_lang;
@ -88,7 +106,9 @@ export class Parser {
if (this.stack.length > 1) { if (this.stack.length > 1) {
const current = this.current(); const current = this.current();
if (current.type === 'RegularElement') { if (this.loose) {
current.end = this.template.length;
} else if (current.type === 'RegularElement') {
current.end = current.start + 1; current.end = current.start + 1;
e.element_unclosed(current, current.name); e.element_unclosed(current, current.name);
} else { } else {
@ -151,14 +171,15 @@ export class Parser {
/** /**
* @param {string} str * @param {string} str
* @param {boolean} required * @param {boolean} required
* @param {boolean} required_in_loose
*/ */
eat(str, required = false) { eat(str, required = false, required_in_loose = true) {
if (this.match(str)) { if (this.match(str)) {
this.index += str.length; this.index += str.length;
return true; return true;
} }
if (required) { if (required && (!this.loose || required_in_loose)) {
e.expected_token(this.index, str); e.expected_token(this.index, str);
} }
@ -233,6 +254,7 @@ export class Parser {
/** @param {RegExp} pattern */ /** @param {RegExp} pattern */
read_until(pattern) { read_until(pattern) {
if (this.index >= this.template.length) { if (this.index >= this.template.length) {
if (this.loose) return '';
e.unexpected_eof(this.template.length); e.unexpected_eof(this.template.length);
} }
@ -274,10 +296,11 @@ export class Parser {
/** /**
* @param {string} template * @param {string} template
* @param {boolean} [loose]
* @returns {AST.Root} * @returns {AST.Root}
*/ */
export function parse(template) { export function parse(template, loose = false) {
const parser = new Parser(template); const parser = new Parser(template, loose);
return parser.root; return parser.root;
} }

@ -3,6 +3,7 @@
import { parse_expression_at } from '../acorn.js'; import { parse_expression_at } from '../acorn.js';
import { regex_whitespace } from '../../patterns.js'; import { regex_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { find_matching_bracket } from '../utils/bracket.js';
/** /**
* @param {Parser} parser * @param {Parser} parser
@ -39,6 +40,22 @@ export default function read_expression(parser) {
return /** @type {Expression} */ (node); return /** @type {Expression} */ (node);
} catch (err) { } catch (err) {
if (parser.loose) {
// Find the next } and treat it as the end of the expression
const end = find_matching_bracket(parser.template, parser.index, '{');
if (end) {
const start = parser.index;
parser.index = end;
// We don't know what the expression is and signal this by returning an empty identifier
return {
type: 'Identifier',
start,
end,
name: ''
};
}
}
parser.acorn_error(err); parser.acorn_error(err);
} }
} }

@ -1,5 +1,5 @@
/** @import { Program } from 'estree' */ /** @import { Program } from 'estree' */
/** @import { AST, Directive } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
import * as acorn from '../acorn.js'; import * as acorn from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js'; import { regex_not_newline_characters } from '../../patterns.js';
@ -16,7 +16,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
/** /**
* @param {Parser} parser * @param {Parser} parser
* @param {number} start * @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | Directive>} attributes * @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive>} attributes
* @returns {AST.Script} * @returns {AST.Script}
*/ */
export function read_script(parser, start, attributes) { export function read_script(parser, start, attributes) {

@ -1,4 +1,4 @@
/** @import { AST, Css, Directive } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
@ -18,8 +18,8 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
/** /**
* @param {Parser} parser * @param {Parser} parser
* @param {number} start * @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | Directive>} attributes * @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive>} attributes
* @returns {Css.StyleSheet} * @returns {AST.CSS.StyleSheet}
*/ */
export default function read_style(parser, start, attributes) { export default function read_style(parser, start, attributes) {
const content_start = parser.index; const content_start = parser.index;
@ -49,7 +49,7 @@ export default function read_style(parser, start, attributes) {
* @returns {any[]} * @returns {any[]}
*/ */
function read_body(parser, close) { function read_body(parser, close) {
/** @type {Array<Css.Rule | Css.Atrule>} */ /** @type {Array<AST.CSS.Rule | AST.CSS.Atrule>} */
const children = []; const children = [];
while (parser.index < parser.template.length) { while (parser.index < parser.template.length) {
@ -71,7 +71,7 @@ function read_body(parser, close) {
/** /**
* @param {Parser} parser * @param {Parser} parser
* @returns {Css.Atrule} * @returns {AST.CSS.Atrule}
*/ */
function read_at_rule(parser) { function read_at_rule(parser) {
const start = parser.index; const start = parser.index;
@ -81,7 +81,7 @@ function read_at_rule(parser) {
const prelude = read_value(parser); const prelude = read_value(parser);
/** @type {Css.Block | null} */ /** @type {AST.CSS.Block | null} */
let block = null; let block = null;
if (parser.match('{')) { if (parser.match('{')) {
@ -104,7 +104,7 @@ function read_at_rule(parser) {
/** /**
* @param {Parser} parser * @param {Parser} parser
* @returns {Css.Rule} * @returns {AST.CSS.Rule}
*/ */
function read_rule(parser) { function read_rule(parser) {
const start = parser.index; const start = parser.index;
@ -126,10 +126,10 @@ function read_rule(parser) {
/** /**
* @param {Parser} parser * @param {Parser} parser
* @param {boolean} [inside_pseudo_class] * @param {boolean} [inside_pseudo_class]
* @returns {Css.SelectorList} * @returns {AST.CSS.SelectorList}
*/ */
function read_selector_list(parser, inside_pseudo_class = false) { function read_selector_list(parser, inside_pseudo_class = false) {
/** @type {Css.ComplexSelector[]} */ /** @type {AST.CSS.ComplexSelector[]} */
const children = []; const children = [];
allow_comment_or_whitespace(parser); allow_comment_or_whitespace(parser);
@ -162,18 +162,18 @@ function read_selector_list(parser, inside_pseudo_class = false) {
/** /**
* @param {Parser} parser * @param {Parser} parser
* @param {boolean} [inside_pseudo_class] * @param {boolean} [inside_pseudo_class]
* @returns {Css.ComplexSelector} * @returns {AST.CSS.ComplexSelector}
*/ */
function read_selector(parser, inside_pseudo_class = false) { function read_selector(parser, inside_pseudo_class = false) {
const list_start = parser.index; const list_start = parser.index;
/** @type {Css.RelativeSelector[]} */ /** @type {AST.CSS.RelativeSelector[]} */
const children = []; const children = [];
/** /**
* @param {Css.Combinator | null} combinator * @param {AST.CSS.Combinator | null} combinator
* @param {number} start * @param {number} start
* @returns {Css.RelativeSelector} * @returns {AST.CSS.RelativeSelector}
*/ */
function create_selector(combinator, start) { function create_selector(combinator, start) {
return { return {
@ -190,7 +190,7 @@ function read_selector(parser, inside_pseudo_class = false) {
}; };
} }
/** @type {Css.RelativeSelector} */ /** @type {AST.CSS.RelativeSelector} */
let relative_selector = create_selector(null, parser.index); let relative_selector = create_selector(null, parser.index);
while (parser.index < parser.template.length) { while (parser.index < parser.template.length) {
@ -247,7 +247,7 @@ function read_selector(parser, inside_pseudo_class = false) {
} else if (parser.eat(':')) { } else if (parser.eat(':')) {
const name = read_identifier(parser); const name = read_identifier(parser);
/** @type {null | Css.SelectorList} */ /** @type {null | AST.CSS.SelectorList} */
let args = null; let args = null;
if (parser.eat('(')) { if (parser.eat('(')) {
@ -372,7 +372,7 @@ function read_selector(parser, inside_pseudo_class = false) {
/** /**
* @param {Parser} parser * @param {Parser} parser
* @returns {Css.Combinator | null} * @returns {AST.CSS.Combinator | null}
*/ */
function read_combinator(parser) { function read_combinator(parser) {
const start = parser.index; const start = parser.index;
@ -407,14 +407,14 @@ function read_combinator(parser) {
/** /**
* @param {Parser} parser * @param {Parser} parser
* @returns {Css.Block} * @returns {AST.CSS.Block}
*/ */
function read_block(parser) { function read_block(parser) {
const start = parser.index; const start = parser.index;
parser.eat('{', true); parser.eat('{', true);
/** @type {Array<Css.Declaration | Css.Rule | Css.Atrule>} */ /** @type {Array<AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule>} */
const children = []; const children = [];
while (parser.index < parser.template.length) { while (parser.index < parser.template.length) {
@ -441,7 +441,7 @@ function read_block(parser) {
* Reads a declaration, rule or at-rule * Reads a declaration, rule or at-rule
* *
* @param {Parser} parser * @param {Parser} parser
* @returns {Css.Declaration | Css.Rule | Css.Atrule} * @returns {AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule}
*/ */
function read_block_item(parser) { function read_block_item(parser) {
if (parser.match('@')) { if (parser.match('@')) {
@ -460,7 +460,7 @@ function read_block_item(parser) {
/** /**
* @param {Parser} parser * @param {Parser} parser
* @returns {Css.Declaration} * @returns {AST.CSS.Declaration}
*/ */
function read_declaration(parser) { function read_declaration(parser) {
const start = parser.index; const start = parser.index;

@ -1,5 +1,5 @@
/** @import { Expression } from 'estree' */ /** @import { Expression } from 'estree' */
/** @import { AST, Directive, ElementLike, TemplateNode } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
import { is_void } from '../../../../utils.js'; import { is_void } from '../../../../utils.js';
import read_expression from '../read/expression.js'; import read_expression from '../read/expression.js';
@ -9,10 +9,11 @@ import { decode_character_references } from '../utils/html.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { create_fragment } from '../utils/create.js'; import { create_fragment } from '../utils/create.js';
import { create_attribute, create_expression_metadata } from '../../nodes.js'; import { create_attribute, create_expression_metadata, is_element_node } from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js'; import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js'; import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js'; import { list } from '../../../utils/string.js';
import { regex_whitespace } from '../../patterns.js';
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/; const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i; const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
@ -28,7 +29,7 @@ export const regex_valid_component_name =
// (must start with uppercase letter if no dots, can contain dots) // (must start with uppercase letter if no dots, can contain dots)
/^(?:\p{Lu}[$\u200c\u200d\p{ID_Continue}.]*|\p{ID_Start}[$\u200c\u200d\p{ID_Continue}]*(?:\.[$\u200c\u200d\p{ID_Continue}]+)+)$/u; /^(?:\p{Lu}[$\u200c\u200d\p{ID_Continue}.]*|\p{ID_Start}[$\u200c\u200d\p{ID_Continue}]*(?:\.[$\u200c\u200d\p{ID_Continue}]+)+)$/u;
/** @type {Map<string, ElementLike['type']>} */ /** @type {Map<string, AST.ElementLike['type']>} */
const root_only_meta_tags = new Map([ const root_only_meta_tags = new Map([
['svelte:head', 'SvelteHead'], ['svelte:head', 'SvelteHead'],
['svelte:options', 'SvelteOptions'], ['svelte:options', 'SvelteOptions'],
@ -38,7 +39,7 @@ const root_only_meta_tags = new Map([
['svelte:body', 'SvelteBody'] ['svelte:body', 'SvelteBody']
]); ]);
/** @type {Map<string, ElementLike['type']>} */ /** @type {Map<string, AST.ElementLike['type']>} */
const meta_tags = new Map([ const meta_tags = new Map([
...root_only_meta_tags, ...root_only_meta_tags,
['svelte:element', 'SvelteElement'], ['svelte:element', 'SvelteElement'],
@ -81,7 +82,19 @@ export default function element(parser) {
// close any elements that don't have their own closing tags, e.g. <div><p></div> // close any elements that don't have their own closing tags, e.g. <div><p></div>
while (/** @type {AST.RegularElement} */ (parent).name !== name) { while (/** @type {AST.RegularElement} */ (parent).name !== name) {
if (parent.type !== 'RegularElement') { if (parser.loose) {
// If the previous element did interpret the next opening tag as an attribute, backtrack
if (is_element_node(parent)) {
const last = parent.attributes.at(-1);
if (last?.type === 'Attribute' && last.name === `<${name}`) {
parser.index = last.start;
parent.attributes.pop();
break;
}
}
}
if (parent.type !== 'RegularElement' && !parser.loose) {
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) { if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason); e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
} else { } else {
@ -138,7 +151,7 @@ export default function element(parser) {
? 'SlotElement' ? 'SlotElement'
: 'RegularElement'; : 'RegularElement';
/** @type {ElementLike} */ /** @type {AST.ElementLike} */
const element = const element =
type === 'RegularElement' type === 'RegularElement'
? { ? {
@ -156,7 +169,7 @@ export default function element(parser) {
path: [] path: []
} }
} }
: /** @type {ElementLike} */ ({ : /** @type {AST.ElementLike} */ ({
type, type,
start, start,
end: -1, end: -1,
@ -320,10 +333,41 @@ export default function element(parser) {
parser.append(element); parser.append(element);
const self_closing = parser.eat('/') || is_void(name); const self_closing = parser.eat('/') || is_void(name);
const closed = parser.eat('>', true, false);
// Loose parsing mode
if (!closed) {
// We may have eaten an opening `<` of the next element and treated it as an attribute...
const last = element.attributes.at(-1);
if (last?.type === 'Attribute' && last.name === '<') {
parser.index = last.start;
element.attributes.pop();
} else {
// ... or we may have eaten part of a following block ...
const prev_1 = parser.template[parser.index - 1];
const prev_2 = parser.template[parser.index - 2];
const current = parser.template[parser.index];
if (prev_2 === '{' && prev_1 === '/') {
parser.index -= 2;
} else if (prev_1 === '{' && (current === '#' || current === '@' || current === ':')) {
parser.index -= 1;
} else {
// ... or we're followed by whitespace, for example near the end of the template,
// which we want to take in so that language tools has more room to work with
parser.allow_whitespace();
if (parser.index === parser.template.length) {
while (
parser.index < parser.template_untrimmed.length &&
regex_whitespace.test(parser.template_untrimmed[parser.index])
) {
parser.index++;
}
}
}
}
}
parser.eat('>', true); if (self_closing || !closed) {
if (self_closing) {
// don't push self-closing elements onto the stack // don't push self-closing elements onto the stack
element.end = parser.index; element.end = parser.index;
} else if (name === 'textarea') { } else if (name === 'textarea') {
@ -359,7 +403,7 @@ export default function element(parser) {
} }
} }
/** @param {TemplateNode[]} stack */ /** @param {AST.TemplateNode[]} stack */
function parent_is_head(stack) { function parent_is_head(stack) {
let i = stack.length; let i = stack.length;
while (i--) { while (i--) {
@ -370,7 +414,7 @@ function parent_is_head(stack) {
return false; return false;
} }
/** @param {TemplateNode[]} stack */ /** @param {AST.TemplateNode[]} stack */
function parent_is_shadowroot_template(stack) { function parent_is_shadowroot_template(stack) {
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root // https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root
let i = stack.length; let i = stack.length;
@ -434,7 +478,7 @@ function read_static_attribute(parser) {
/** /**
* @param {Parser} parser * @param {Parser} parser
* @returns {AST.Attribute | AST.SpreadAttribute | Directive | null} * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null}
*/ */
function read_attribute(parser) { function read_attribute(parser) {
const start = parser.index; const start = parser.index;
@ -462,11 +506,23 @@ function read_attribute(parser) {
return spread; return spread;
} else { } else {
const value_start = parser.index; const value_start = parser.index;
const name = parser.read_identifier(); let name = parser.read_identifier();
if (name === null) { if (name === null) {
if (
parser.loose &&
(parser.match('#') || parser.match('/') || parser.match('@') || parser.match(':'))
) {
// We're likely in an unclosed opening tag and did read part of a block.
// Return null to not crash the parser so it can continue with closing the tag.
return null;
} else if (parser.loose && parser.match('}')) {
// Likely in the middle of typing, just created the shorthand
name = '';
} else {
e.attribute_empty_shorthand(start); e.attribute_empty_shorthand(start);
} }
}
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
@ -505,8 +561,24 @@ function read_attribute(parser) {
let value = true; let value = true;
if (parser.eat('=')) { if (parser.eat('=')) {
parser.allow_whitespace(); parser.allow_whitespace();
if (parser.template[parser.index] === '/' && parser.template[parser.index + 1] === '>') {
const char_start = parser.index;
parser.index++; // consume '/'
value = [
{
start: char_start,
end: char_start + 1,
type: 'Text',
raw: '/',
data: '/'
}
];
end = parser.index;
} else {
value = read_attribute_value(parser); value = read_attribute_value(parser);
end = parser.index; end = parser.index;
}
} else if (parser.match_regex(regex_starts_with_quote_characters)) { } else if (parser.match_regex(regex_starts_with_quote_characters)) {
e.expected_token(parser.index, '='); e.expected_token(parser.index, '=');
} }
@ -549,7 +621,7 @@ function read_attribute(parser) {
} }
} }
/** @type {Directive} */ /** @type {AST.Directive} */
const directive = { const directive = {
start, start,
end, end,
@ -741,5 +813,9 @@ function read_sequence(parser, done, location) {
} }
} }
if (parser.loose) {
return chunks;
} else {
e.unexpected_eof(parser.template.length); e.unexpected_eof(parser.template.length);
}
} }

@ -279,18 +279,24 @@ function open(parser) {
parser.require_whitespace(); parser.require_whitespace();
const name_start = parser.index; const name_start = parser.index;
const name = parser.read_identifier(); let name = parser.read_identifier();
const name_end = parser.index; const name_end = parser.index;
if (name === null) { if (name === null) {
if (parser.loose) {
name = '';
} else {
e.expected_identifier(parser.index); e.expected_identifier(parser.index);
} }
}
parser.allow_whitespace(); parser.allow_whitespace();
const params_start = parser.index; const params_start = parser.index;
parser.eat('(', true); const matched = parser.eat('(', true, false);
if (matched) {
let parentheses = 1; let parentheses = 1;
while (parser.index < parser.template.length && (!parser.match(')') || parentheses !== 1)) { while (parser.index < parser.template.length && (!parser.match(')') || parentheses !== 1)) {
@ -300,13 +306,16 @@ function open(parser) {
} }
parser.eat(')', true); parser.eat(')', true);
}
const prelude = parser.template.slice(0, params_start).replace(/\S/g, ' '); const prelude = parser.template.slice(0, params_start).replace(/\S/g, ' ');
const params = parser.template.slice(params_start, parser.index); const params = parser.template.slice(params_start, parser.index);
let function_expression = /** @type {ArrowFunctionExpression} */ ( let function_expression = matched
? /** @type {ArrowFunctionExpression} */ (
parse_expression_at(prelude + `${params} => {}`, parser.ts, params_start) parse_expression_at(prelude + `${params} => {}`, parser.ts, params_start)
); )
: { params: [] };
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
@ -454,43 +463,66 @@ function close(parser) {
const start = parser.index - 1; const start = parser.index - 1;
let block = parser.current(); let block = parser.current();
/** Only relevant/reached for loose parsing mode */
let matched;
switch (block.type) { switch (block.type) {
case 'IfBlock': case 'IfBlock':
parser.eat('if', true); matched = parser.eat('if', true, false);
if (!matched) {
block.end = start - 1;
parser.pop();
close(parser);
return;
}
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
while (block.elseif) { while (block.elseif) {
block.end = parser.index; block.end = parser.index;
parser.stack.pop(); parser.stack.pop();
block = /** @type {AST.IfBlock} */ (parser.current()); block = /** @type {AST.IfBlock} */ (parser.current());
} }
block.end = parser.index; block.end = parser.index;
parser.pop(); parser.pop();
return; return;
case 'EachBlock': case 'EachBlock':
parser.eat('each', true); matched = parser.eat('each', true, false);
break; break;
case 'KeyBlock': case 'KeyBlock':
parser.eat('key', true); matched = parser.eat('key', true, false);
break; break;
case 'AwaitBlock': case 'AwaitBlock':
parser.eat('await', true); matched = parser.eat('await', true, false);
break; break;
case 'SnippetBlock': case 'SnippetBlock':
parser.eat('snippet', true); matched = parser.eat('snippet', true, false);
break; break;
case 'RegularElement': case 'RegularElement':
if (parser.loose) {
matched = false;
} else {
// TODO handle implicitly closed elements // TODO handle implicitly closed elements
e.block_unexpected_close(start); e.block_unexpected_close(start);
}
break; break;
default: default:
e.block_unexpected_close(start); e.block_unexpected_close(start);
} }
if (!matched) {
block.end = start - 1;
parser.pop();
close(parser);
return;
}
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
block.end = parser.index; block.end = parser.index;

@ -1,3 +1,5 @@
import full_char_code_at from './full_char_code_at.js';
const SQUARE_BRACKET_OPEN = '['.charCodeAt(0); const SQUARE_BRACKET_OPEN = '['.charCodeAt(0);
const SQUARE_BRACKET_CLOSE = ']'.charCodeAt(0); const SQUARE_BRACKET_CLOSE = ']'.charCodeAt(0);
const CURLY_BRACKET_OPEN = '{'.charCodeAt(0); const CURLY_BRACKET_OPEN = '{'.charCodeAt(0);
@ -33,3 +35,137 @@ export function get_bracket_close(open) {
return CURLY_BRACKET_CLOSE; return CURLY_BRACKET_CLOSE;
} }
} }
/**
* @param {number} num
* @returns {number} Infinity if {@link num} is negative, else {@link num}.
*/
function infinity_if_negative(num) {
if (num < 0) {
return Infinity;
}
return num;
}
/**
* @param {string} string The string to search.
* @param {number} search_start_index The index to start searching at.
* @param {"'" | '"' | '`'} string_start_char The character that started this string.
* @returns {number} The index of the end of this string expression, or `Infinity` if not found.
*/
function find_string_end(string, search_start_index, string_start_char) {
let string_to_search;
if (string_start_char === '`') {
string_to_search = string;
} else {
// we could slice at the search start index, but this way the index remains valid
string_to_search = string.slice(
0,
infinity_if_negative(string.indexOf('\n', search_start_index))
);
}
return find_unescaped_char(string_to_search, search_start_index, string_start_char);
}
/**
* @param {string} string The string to search.
* @param {number} search_start_index The index to start searching at.
* @returns {number} The index of the end of this regex expression, or `Infinity` if not found.
*/
function find_regex_end(string, search_start_index) {
return find_unescaped_char(string, search_start_index, '/');
}
/**
*
* @param {string} string The string to search.
* @param {number} search_start_index The index to begin the search at.
* @param {string} char The character to search for.
* @returns {number} The index of the first unescaped instance of {@link char}, or `Infinity` if not found.
*/
function find_unescaped_char(string, search_start_index, char) {
let i = search_start_index;
while (true) {
const found_index = string.indexOf(char, i);
if (found_index === -1) {
return Infinity;
}
if (count_leading_backslashes(string, found_index - 1) % 2 === 0) {
return found_index;
}
i = found_index + 1;
}
}
/**
* Count consecutive leading backslashes before {@link search_start_index}.
*
* @example
* ```js
* count_leading_backslashes('\\\\\\foo', 2); // 3 (the backslashes have to be escaped in the string literal, there are three in reality)
* ```
*
* @param {string} string The string to search.
* @param {number} search_start_index The index to begin the search at.
*/
function count_leading_backslashes(string, search_start_index) {
let i = search_start_index;
let count = 0;
while (string[i] === '\\') {
count++;
i--;
}
return count;
}
/**
* Finds the corresponding closing bracket, ignoring brackets found inside comments, strings, or regex expressions.
* @param {string} template The string to search.
* @param {number} index The index to begin the search at.
* @param {string} open The opening bracket (ex: `'{'` will search for `'}'`).
* @returns {number | undefined} The index of the closing bracket, or undefined if not found.
*/
export function find_matching_bracket(template, index, open) {
const open_code = full_char_code_at(open, 0);
const close_code = get_bracket_close(open_code);
let brackets = 1;
let i = index;
while (brackets > 0 && i < template.length) {
const char = template[i];
switch (char) {
case "'":
case '"':
case '`':
i = find_string_end(template, i + 1, char) + 1;
continue;
case '/': {
const next_char = template[i + 1];
if (!next_char) continue;
if (next_char === '/') {
i = infinity_if_negative(template.indexOf('\n', i + 1)) + '\n'.length;
continue;
}
if (next_char === '*') {
i = infinity_if_negative(template.indexOf('*/', i + 1)) + '*/'.length;
continue;
}
i = find_regex_end(template, i + 1) + '/'.length;
continue;
}
default: {
const code = full_char_code_at(template, i);
if (code === open_code) {
brackets++;
} else if (code === close_code) {
brackets--;
}
if (brackets === 0) {
return i;
}
i++;
}
}
}
return undefined;
}

@ -1,5 +1,5 @@
/** @import { ComponentAnalysis } from '../../types.js' */ /** @import { ComponentAnalysis } from '../../types.js' */
/** @import { Css } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Visitors } from 'zimmerframe' */ /** @import { Visitors } from 'zimmerframe' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
@ -8,17 +8,17 @@ import { is_global, is_unscoped_pseudo_class } from './utils.js';
/** /**
* @typedef {Visitors< * @typedef {Visitors<
* Css.Node, * AST.CSS.Node,
* { * {
* keyframes: string[]; * keyframes: string[];
* rule: Css.Rule | null; * rule: AST.CSS.Rule | null;
* } * }
* >} CssVisitors * >} CssVisitors
*/ */
/** /**
* True if is `:global` * True if is `:global`
* @param {Css.SimpleSelector} simple_selector * @param {AST.CSS.SimpleSelector} simple_selector
*/ */
function is_global_block_selector(simple_selector) { function is_global_block_selector(simple_selector) {
return ( return (
@ -112,7 +112,7 @@ const css_visitors = {
} }
}, },
RelativeSelector(node, context) { RelativeSelector(node, context) {
const parent = /** @type {Css.ComplexSelector} */ (context.path.at(-1)); const parent = /** @type {AST.CSS.ComplexSelector} */ (context.path.at(-1));
if ( if (
node.combinator != null && node.combinator != null &&
@ -149,7 +149,7 @@ const css_visitors = {
if (node.metadata.is_global_like || node.metadata.is_global) { if (node.metadata.is_global_like || node.metadata.is_global) {
// So that nested selectors like `:root:not(.x)` are not marked as unused // So that nested selectors like `:root:not(.x)` are not marked as unused
for (const child of node.selectors) { for (const child of node.selectors) {
walk(/** @type {Css.Node} */ (child), null, { walk(/** @type {AST.CSS.Node} */ (child), null, {
ComplexSelector(node, context) { ComplexSelector(node, context) {
node.metadata.used = true; node.metadata.used = true;
context.next(); context.next();
@ -177,7 +177,7 @@ const css_visitors = {
if (idx !== -1) { if (idx !== -1) {
is_global_block = true; is_global_block = true;
for (let i = idx + 1; i < child.selectors.length; i++) { for (let i = idx + 1; i < child.selectors.length; i++) {
walk(/** @type {Css.Node} */ (child.selectors[i]), null, { walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
ComplexSelector(node) { ComplexSelector(node) {
node.metadata.used = true; node.metadata.used = true;
} }
@ -240,7 +240,7 @@ const css_visitors = {
}); });
}, },
NestingSelector(node, context) { NestingSelector(node, context) {
const rule = /** @type {Css.Rule} */ (context.state.rule); const rule = /** @type {AST.CSS.Rule} */ (context.state.rule);
const parent_rule = rule.metadata.parent_rule; const parent_rule = rule.metadata.parent_rule;
if (!parent_rule) { if (!parent_rule) {
@ -271,7 +271,7 @@ const css_visitors = {
}; };
/** /**
* @param {Css.StyleSheet} stylesheet * @param {AST.CSS.StyleSheet} stylesheet
* @param {ComponentAnalysis} analysis * @param {ComponentAnalysis} analysis
*/ */
export function analyze_css(stylesheet, analysis) { export function analyze_css(stylesheet, analysis) {

@ -14,7 +14,7 @@ const whitelist_attribute_selector = new Map([
['dialog', ['open']] ['dialog', ['open']]
]); ]);
/** @type {Compiler.Css.Combinator} */ /** @type {Compiler.AST.CSS.Combinator} */
const descendant_combinator = { const descendant_combinator = {
type: 'Combinator', type: 'Combinator',
name: ' ', name: ' ',
@ -22,7 +22,7 @@ const descendant_combinator = {
end: -1 end: -1
}; };
/** @type {Compiler.Css.RelativeSelector} */ /** @type {Compiler.AST.CSS.RelativeSelector} */
const nesting_selector = { const nesting_selector = {
type: 'RelativeSelector', type: 'RelativeSelector',
start: -1, start: -1,
@ -51,11 +51,11 @@ const seen = new Set();
/** /**
* *
* @param {Compiler.Css.StyleSheet} stylesheet * @param {Compiler.AST.CSS.StyleSheet} stylesheet
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
*/ */
export function prune(stylesheet, element) { export function prune(stylesheet, element) {
walk(/** @type {Compiler.Css.Node} */ (stylesheet), null, { walk(/** @type {Compiler.AST.CSS.Node} */ (stylesheet), null, {
Rule(node, context) { Rule(node, context) {
if (node.metadata.is_global_block) { if (node.metadata.is_global_block) {
context.visit(node.prelude); context.visit(node.prelude);
@ -69,7 +69,11 @@ export function prune(stylesheet, element) {
seen.clear(); seen.clear();
if ( if (
apply_selector(selectors, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), element) apply_selector(
selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
element
)
) { ) {
node.metadata.used = true; node.metadata.used = true;
} }
@ -86,7 +90,7 @@ export function prune(stylesheet, element) {
* Also searches them for any existing `&` selectors and adds one if none are found. * Also searches them for any existing `&` selectors and adds one if none are found.
* This ensures we traverse up to the parent rule when the inner selectors match and we're * This ensures we traverse up to the parent rule when the inner selectors match and we're
* trying to see if the parent rule also matches. * trying to see if the parent rule also matches.
* @param {Compiler.Css.ComplexSelector} node * @param {Compiler.AST.CSS.ComplexSelector} node
*/ */
function get_relative_selectors(node) { function get_relative_selectors(node) {
const selectors = truncate(node); const selectors = truncate(node);
@ -124,7 +128,7 @@ function get_relative_selectors(node) {
/** /**
* Discard trailing `:global(...)` selectors, these are unused for scoping purposes * Discard trailing `:global(...)` selectors, these are unused for scoping purposes
* @param {Compiler.Css.ComplexSelector} node * @param {Compiler.AST.CSS.ComplexSelector} node
*/ */
function truncate(node) { function truncate(node) {
const i = node.children.findLastIndex(({ metadata, selectors }) => { const i = node.children.findLastIndex(({ metadata, selectors }) => {
@ -152,8 +156,8 @@ function truncate(node) {
} }
/** /**
* @param {Compiler.Css.RelativeSelector[]} relative_selectors * @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors
* @param {Compiler.Css.Rule} rule * @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @returns {boolean} * @returns {boolean}
*/ */
@ -178,9 +182,9 @@ function apply_selector(relative_selectors, rule, element) {
} }
/** /**
* @param {Compiler.Css.RelativeSelector} relative_selector * @param {Compiler.AST.CSS.RelativeSelector} relative_selector
* @param {Compiler.Css.RelativeSelector[]} parent_selectors * @param {Compiler.AST.CSS.RelativeSelector[]} parent_selectors
* @param {Compiler.Css.Rule} rule * @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @returns {boolean} * @returns {boolean}
*/ */
@ -263,8 +267,8 @@ function apply_combinator(relative_selector, parent_selectors, rule, node) {
* it's a `:global(...)` or unscopeable selector, or * it's a `:global(...)` or unscopeable selector, or
* is an `:is(...)` or `:where(...)` selector that contains * is an `:is(...)` or `:where(...)` selector that contains
* a global selector * a global selector
* @param {Compiler.Css.RelativeSelector} selector * @param {Compiler.AST.CSS.RelativeSelector} selector
* @param {Compiler.Css.Rule} rule * @param {Compiler.AST.CSS.Rule} rule
*/ */
function is_global(selector, rule) { function is_global(selector, rule) {
if (selector.metadata.is_global || selector.metadata.is_global_like) { if (selector.metadata.is_global || selector.metadata.is_global_like) {
@ -272,7 +276,7 @@ function is_global(selector, rule) {
} }
for (const s of selector.selectors) { for (const s of selector.selectors) {
/** @type {Compiler.Css.SelectorList | null} */ /** @type {Compiler.AST.CSS.SelectorList | null} */
let selector_list = null; let selector_list = null;
let owner = rule; let owner = rule;
@ -283,7 +287,7 @@ function is_global(selector, rule) {
} }
if (s.type === 'NestingSelector') { if (s.type === 'NestingSelector') {
owner = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule); owner = /** @type {Compiler.AST.CSS.Rule} */ (rule.metadata.parent_rule);
selector_list = owner.prelude; selector_list = owner.prelude;
} }
@ -306,8 +310,8 @@ const regex_backslash_and_following_character = /\\(.)/g;
/** /**
* Ensure that `element` satisfies each simple selector in `relative_selector` * Ensure that `element` satisfies each simple selector in `relative_selector`
* *
* @param {Compiler.Css.RelativeSelector} relative_selector * @param {Compiler.AST.CSS.RelativeSelector} relative_selector
* @param {Compiler.Css.Rule} rule * @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @returns {boolean} * @returns {boolean}
*/ */
@ -352,7 +356,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
const seen = new Set(); const seen = new Set();
/** /**
* @param {Compiler.SvelteNode} node * @param {Compiler.AST.SvelteNode} node
* @param {{ is_child: boolean }} state * @param {{ is_child: boolean }} state
*/ */
function walk_children(node, state) { function walk_children(node, state) {
@ -389,7 +393,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the // upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
// selector in a way that is similar to ancestor matching. In a sense, we're treating `.x:has(.y)` as `.x .y`. // selector in a way that is similar to ancestor matching. In a sense, we're treating `.x:has(.y)` as `.x .y`.
for (const has_selector of has_selectors) { for (const has_selector of has_selectors) {
const complex_selectors = /** @type {Compiler.Css.SelectorList} */ (has_selector.args) const complex_selectors = /** @type {Compiler.AST.CSS.SelectorList} */ (has_selector.args)
.children; .children;
let matched = false; let matched = false;
@ -578,7 +582,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
case 'NestingSelector': { case 'NestingSelector': {
let matched = false; let matched = false;
const parent = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule); const parent = /** @type {Compiler.AST.CSS.Rule} */ (rule.metadata.parent_rule);
for (const complex_selector of parent.prelude.children) { for (const complex_selector of parent.prelude.children) {
if ( if (
@ -611,9 +615,9 @@ function get_following_sibling_elements(element, include_self) {
const path = element.metadata.path; const path = element.metadata.path;
let i = path.length; let i = path.length;
/** @type {Compiler.SvelteNode} */ /** @type {Compiler.AST.SvelteNode} */
let start = element; let start = element;
let nodes = /** @type {Compiler.SvelteNode[]} */ ( let nodes = /** @type {Compiler.AST.SvelteNode[]} */ (
/** @type {Compiler.AST.Fragment} */ (path[0]).nodes /** @type {Compiler.AST.Fragment} */ (path[0]).nodes
); );
@ -639,7 +643,7 @@ function get_following_sibling_elements(element, include_self) {
const seen = new Set(); const seen = new Set();
/** @param {Compiler.SvelteNode} node */ /** @param {Compiler.AST.SvelteNode} node */
function get_siblings(node) { function get_siblings(node) {
walk(node, null, { walk(node, null, {
RegularElement(node) { RegularElement(node) {
@ -836,7 +840,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
const result = new Map(); const result = new Map();
const path = node.metadata.path; const path = node.metadata.path;
/** @type {Compiler.SvelteNode} */ /** @type {Compiler.AST.SvelteNode} */
let current = node; let current = node;
let i = path.length; let i = path.length;
@ -1008,7 +1012,7 @@ function higher_existence(exist1, exist2) {
} }
/** /**
* @param {Compiler.SvelteNode[]} children * @param {Compiler.AST.SvelteNode[]} children
* @param {boolean} adjacent_only * @param {boolean} adjacent_only
*/ */
function loop_child(children, adjacent_only) { function loop_child(children, adjacent_only) {
@ -1038,7 +1042,7 @@ function loop_child(children, adjacent_only) {
} }
/** /**
* @param {Compiler.SvelteNode} node * @param {Compiler.AST.SvelteNode} node
* @returns {node is Compiler.AST.IfBlock | Compiler.AST.EachBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement} * @returns {node is Compiler.AST.IfBlock | Compiler.AST.EachBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement}
*/ */
function is_block(node) { function is_block(node) {

@ -1,17 +1,17 @@
/** @import { Visitors } from 'zimmerframe' */ /** @import { Visitors } from 'zimmerframe' */
/** @import { Css } from '#compiler' */ /** @import { AST } from '#compiler' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { is_keyframes_node } from '../../css.js'; import { is_keyframes_node } from '../../css.js';
/** /**
* @param {Css.StyleSheet} stylesheet * @param {AST.CSS.StyleSheet} stylesheet
*/ */
export function warn_unused(stylesheet) { export function warn_unused(stylesheet) {
walk(stylesheet, { stylesheet }, visitors); walk(stylesheet, { stylesheet }, visitors);
} }
/** @type {Visitors<Css.Node, { stylesheet: Css.StyleSheet }>} */ /** @type {Visitors<AST.CSS.Node, { stylesheet: AST.CSS.StyleSheet }>} */
const visitors = { const visitors = {
Atrule(node, context) { Atrule(node, context) {
if (!is_keyframes_node(node)) { if (!is_keyframes_node(node)) {
@ -28,7 +28,7 @@ const visitors = {
!node.metadata.used && !node.metadata.used &&
// prevent double-marking of `.unused:is(.unused)` // prevent double-marking of `.unused:is(.unused)`
(context.path.at(-2)?.type !== 'PseudoClassSelector' || (context.path.at(-2)?.type !== 'PseudoClassSelector' ||
/** @type {Css.ComplexSelector} */ (context.path.at(-4))?.metadata.used) /** @type {AST.CSS.ComplexSelector} */ (context.path.at(-4))?.metadata.used)
) { ) {
const content = context.state.stylesheet.content; const content = context.state.stylesheet.content;
const text = content.styles.substring(node.start - content.start, node.end - content.start); const text = content.styles.substring(node.start - content.start, node.end - content.start);

@ -1,4 +1,4 @@
/** @import { AST, Css } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Node } from 'estree' */ /** @import { Node } from 'estree' */
const UNKNOWN = {}; const UNKNOWN = {};
@ -36,7 +36,7 @@ export function get_possible_values(chunk) {
/** /**
* Returns all parent rules; root is last * Returns all parent rules; root is last
* @param {Css.Rule | null} rule * @param {AST.CSS.Rule | null} rule
*/ */
export function get_parent_rules(rule) { export function get_parent_rules(rule) {
const rules = []; const rules = [];
@ -51,8 +51,8 @@ export function get_parent_rules(rule) {
/** /**
* True if is `:global(...)` or `:global` and no pseudo class that is scoped. * True if is `:global(...)` or `:global` and no pseudo class that is scoped.
* @param {Css.RelativeSelector} relative_selector * @param {AST.CSS.RelativeSelector} relative_selector
* @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array<Css.PseudoClassSelector | Css.PseudoElementSelector>] }} * @returns {relative_selector is AST.CSS.RelativeSelector & { selectors: [AST.CSS.PseudoClassSelector, ...Array<AST.CSS.PseudoClassSelector | AST.CSS.PseudoElementSelector>] }}
*/ */
export function is_global(relative_selector) { export function is_global(relative_selector) {
const first = relative_selector.selectors[0]; const first = relative_selector.selectors[0];
@ -72,7 +72,7 @@ export function is_global(relative_selector) {
/** /**
* `true` if is a pseudo class that cannot be or is not scoped * `true` if is a pseudo class that cannot be or is not scoped
* @param {Css.SimpleSelector} selector * @param {AST.CSS.SimpleSelector} selector
*/ */
export function is_unscoped_pseudo_class(selector) { export function is_unscoped_pseudo_class(selector) {
return ( return (
@ -96,8 +96,8 @@ export function is_unscoped_pseudo_class(selector) {
/** /**
* True if is `:global(...)` or `:global`, irrespective of whether or not there are any pseudo classes that are scoped. * True if is `:global(...)` or `:global`, irrespective of whether or not there are any pseudo classes that are scoped.
* Difference to `is_global`: `:global(x):has(y)` is `true` for `is_outer_global` but `false` for `is_global`. * Difference to `is_global`: `:global(x):has(y)` is `true` for `is_outer_global` but `false` for `is_global`.
* @param {Css.RelativeSelector} relative_selector * @param {AST.CSS.RelativeSelector} relative_selector
* @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array<Css.PseudoClassSelector | Css.PseudoElementSelector>] }} * @returns {relative_selector is AST.CSS.RelativeSelector & { selectors: [AST.CSS.PseudoClassSelector, ...Array<AST.CSS.PseudoClassSelector | AST.CSS.PseudoElementSelector>] }}
*/ */
export function is_outer_global(relative_selector) { export function is_outer_global(relative_selector) {
const first = relative_selector.selectors[0]; const first = relative_selector.selectors[0];

@ -1,5 +1,5 @@
/** @import { Expression, Node, Program } from 'estree' */ /** @import { Expression, Node, Program } from 'estree' */
/** @import { Binding, AST, SvelteNode, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */ /** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ /** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
@ -527,7 +527,7 @@ export function analyze_component(root, source, options) {
// more legacy nonsense: if an `each` binding is reassigned/mutated, // more legacy nonsense: if an `each` binding is reassigned/mutated,
// treat the expression as being mutated as well // treat the expression as being mutated as well
walk(/** @type {SvelteNode} */ (template.ast), null, { walk(/** @type {AST.SvelteNode} */ (template.ast), null, {
EachBlock(node) { EachBlock(node) {
const scope = /** @type {Scope} */ (template.scopes.get(node)); const scope = /** @type {Scope} */ (template.scopes.get(node));
@ -610,7 +610,7 @@ export function analyze_component(root, source, options) {
reactive_statements: new Map() reactive_statements: new Map()
}; };
walk(/** @type {SvelteNode} */ (ast), state, visitors); walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
} }
// warn on any nonstate declarations that are a) reassigned and b) referenced in the template // warn on any nonstate declarations that are a) reassigned and b) referenced in the template
@ -679,7 +679,7 @@ export function analyze_component(root, source, options) {
function_depth: scope.function_depth function_depth: scope.function_depth
}; };
walk(/** @type {SvelteNode} */ (ast), state, visitors); walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
} }
for (const [name, binding] of instance.scope.declarations) { for (const [name, binding] of instance.scope.declarations) {

@ -1,11 +1,11 @@
import type { Scope } from '../scope.js'; import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js'; import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { ExpressionMetadata, AST, ValidatedCompileOptions, SvelteNode } from '#compiler'; import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler';
import type { LabeledStatement } from 'estree'; import type { LabeledStatement } from 'estree';
export interface AnalysisState { export interface AnalysisState {
scope: Scope; scope: Scope;
scopes: Map<SvelteNode, Scope>; scopes: Map<AST.SvelteNode, Scope>;
analysis: ComponentAnalysis; analysis: ComponentAnalysis;
options: ValidatedCompileOptions; options: ValidatedCompileOptions;
ast_type: 'instance' | 'template' | 'module'; ast_type: 'instance' | 'template' | 'module';
@ -31,11 +31,11 @@ export interface AnalysisState {
} }
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context< export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<
SvelteNode, AST.SvelteNode,
State State
>; >;
export type Visitors<State extends AnalysisState = AnalysisState> = import('zimmerframe').Visitors< export type Visitors<State extends AnalysisState = AnalysisState> = import('zimmerframe').Visitors<
SvelteNode, AST.SvelteNode,
State State
>; >;

@ -1,5 +1,4 @@
/** @import { AssignmentExpression } from 'estree' */ /** @import { AssignmentExpression } from 'estree' */
/** @import { SvelteNode } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { extract_identifiers, object } from '../../../utils/ast.js'; import { extract_identifiers, object } from '../../../utils/ast.js';
import { validate_assignment } from './shared/utils.js'; import { validate_assignment } from './shared/utils.js';

@ -1,5 +1,5 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */ /** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */
/** @import { AST, DelegatedEvent, SvelteNode } from '#compiler' */ /** @import { AST, DelegatedEvent } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { cannot_be_set_statically, is_capture_event, is_delegated } from '../../../../utils.js'; import { cannot_be_set_statically, is_capture_event, is_delegated } from '../../../../utils.js';
import { import {
@ -16,7 +16,7 @@ import { mark_subtree_dynamic } from './shared/fragment.js';
export function Attribute(node, context) { export function Attribute(node, context) {
context.next(); context.next();
const parent = /** @type {SvelteNode} */ (context.path.at(-1)); const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
if (parent.type === 'RegularElement') { if (parent.type === 'RegularElement') {
// special case <option value="" /> // special case <option value="" />

@ -1,18 +1,19 @@
/** @import { CallExpression, VariableDeclarator } from 'estree' */ /** @import { ArrowFunctionExpression, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, VariableDeclarator } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { get_rune } from '../../scope.js'; import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { get_parent, unwrap_optional } from '../../../utils/ast.js'; import { get_parent, unwrap_optional } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js'; import { is_pure, is_safe_identifier } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js'; import { dev, locate_node, source } from '../../../state.js';
import * as b from '../../../utils/builders.js';
/** /**
* @param {CallExpression} node * @param {CallExpression} node
* @param {Context} context * @param {Context} context
*/ */
export function CallExpression(node, context) { export function CallExpression(node, context) {
const parent = /** @type {SvelteNode} */ (get_parent(context.path, -1)); const parent = /** @type {AST.SvelteNode} */ (get_parent(context.path, -1));
const rune = get_rune(node, context.state.scope); const rune = get_rune(node, context.state.scope);
@ -136,6 +137,45 @@ export function CallExpression(node, context) {
break; break;
case '$inspect.trace': {
if (node.arguments.length > 1) {
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
}
const grand_parent = context.path.at(-2);
const fn = context.path.at(-3);
if (
parent.type !== 'ExpressionStatement' ||
grand_parent?.type !== 'BlockStatement' ||
!(
fn?.type === 'FunctionDeclaration' ||
fn?.type === 'FunctionExpression' ||
fn?.type === 'ArrowFunctionExpression'
) ||
grand_parent.body[0] !== parent
) {
e.inspect_trace_invalid_placement(node);
}
if (fn.generator) {
e.inspect_trace_generator(node);
}
if (dev) {
if (node.arguments[0]) {
context.state.scope.tracing = b.thunk(/** @type {Expression} */ (node.arguments[0]));
} else {
const label = get_function_label(context.path.slice(0, -2)) ?? 'trace';
const loc = `(${locate_node(fn)})`;
context.state.scope.tracing = b.thunk(b.literal(label + ' ' + loc));
}
}
break;
}
case '$state.snapshot': case '$state.snapshot':
if (node.arguments.length !== 1) { if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument'); e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
@ -182,3 +222,31 @@ export function CallExpression(node, context) {
} }
} }
} }
/**
* @param {AST.SvelteNode[]} nodes
*/
function get_function_label(nodes) {
const fn = /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} */ (
nodes.at(-1)
);
if ((fn.type === 'FunctionDeclaration' || fn.type === 'FunctionExpression') && fn.id != null) {
return fn.id.name;
}
const parent = nodes.at(-2);
if (!parent) return;
if (parent.type === 'CallExpression') {
return source.slice(parent.callee.start, parent.callee.end) + '(...)';
}
if (parent.type === 'Property' && !parent.computed) {
return /** @type {Identifier} */ (parent.key).name;
}
if (parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
return parent.id.name;
}
}

@ -1,5 +1,5 @@
/** @import { Expression, LabeledStatement } from 'estree' */ /** @import { Expression, LabeledStatement } from 'estree' */
/** @import { AST, ReactiveStatement, SvelteNode } from '#compiler' */ /** @import { AST, ReactiveStatement } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { extract_identifiers, object } from '../../../utils/ast.js'; import { extract_identifiers, object } from '../../../utils/ast.js';
@ -11,7 +11,7 @@ import * as w from '../../../warnings.js';
*/ */
export function LabeledStatement(node, context) { export function LabeledStatement(node, context) {
if (node.label.name === '$') { if (node.label.name === '$') {
const parent = /** @type {SvelteNode} */ (context.path.at(-1)); const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
const is_reactive_statement = const is_reactive_statement =
context.state.ast_type === 'instance' && parent.type === 'Program'; context.state.ast_type === 'instance' && parent.type === 'Program';

@ -1,4 +1,4 @@
/** @import { AST, Binding, SvelteNode } from '#compiler' */ /** @import { AST, Binding } from '#compiler' */
/** @import { Scope } from '../../scope' */ /** @import { Scope } from '../../scope' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js'; import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
@ -80,7 +80,7 @@ export function SnippetBlock(node, context) {
} }
/** /**
* @param {Map<SvelteNode, Scope>} scopes * @param {Map<AST.SvelteNode, Scope>} scopes
* @param {Scope} scope * @param {Scope} scope
*/ */
function can_hoist_snippet(scope, scopes, visited = new Set()) { function can_hoist_snippet(scope, scopes, visited = new Set()) {

@ -1,5 +1,5 @@
/** @import { Context } from '../../types.js' */ /** @import { Context } from '../../types.js' */
/** @import { AST, SvelteNode, TemplateNode } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ARIARoleDefinitionKey, ARIARoleRelationConcept, ARIAProperty, ARIAPropertyDefinition, ARIARoleDefinition } from 'aria-query' */ /** @import { ARIARoleDefinitionKey, ARIARoleRelationConcept, ARIAProperty, ARIAPropertyDefinition, ARIARoleDefinition } from 'aria-query' */
import { roles as roles_map, aria, elementRoles } from 'aria-query'; import { roles as roles_map, aria, elementRoles } from 'aria-query';
// @ts-expect-error package doesn't provide typings // @ts-expect-error package doesn't provide typings
@ -582,7 +582,7 @@ function get_implicit_role(name, attribute_map) {
const invisible_elements = ['meta', 'html', 'script', 'style']; const invisible_elements = ['meta', 'html', 'script', 'style'];
/** /**
* @param {SvelteNode[]} path * @param {AST.SvelteNode[]} path
* @param {string[]} elements * @param {string[]} elements
*/ */
function is_parent(path, elements) { function is_parent(path, elements) {
@ -1062,7 +1062,7 @@ export function check_element(node, context) {
} }
if (node.name === 'label') { if (node.name === 'label') {
/** @param {TemplateNode} node */ /** @param {AST.TemplateNode} node */
const has_input_child = (node) => { const has_input_child = (node) => {
let has = false; let has = false;
walk( walk(

@ -1,4 +1,4 @@
/** @import { AST, ElementLike } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Context } from '../../types' */ /** @import { Context } from '../../types' */
import * as e from '../../../../errors.js'; import * as e from '../../../../errors.js';
import { is_text_attribute } from '../../../../utils/ast.js'; import { is_text_attribute } from '../../../../utils/ast.js';
@ -22,7 +22,7 @@ export function validate_attribute_name(attribute) {
/** /**
* @param {AST.Attribute} attribute * @param {AST.Attribute} attribute
* @param {ElementLike} parent * @param {AST.ElementLike} parent
*/ */
export function validate_attribute(attribute, parent) { export function validate_attribute(attribute, parent) {
if ( if (

@ -1,7 +1,7 @@
/** @import { AST, SvelteNode } from '#compiler' */ /** @import { AST } from '#compiler' */
/** /**
* @param {SvelteNode[]} path * @param {AST.SvelteNode[]} path
*/ */
export function mark_subtree_dynamic(path) { export function mark_subtree_dynamic(path) {
let i = path.length; let i = path.length;

@ -1,5 +1,5 @@
/** @import * as ESTree from 'estree' */ /** @import * as ESTree from 'estree' */
/** @import { ValidatedCompileOptions, AST, ValidatedModuleCompileOptions, SvelteNode } from '#compiler' */ /** @import { AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { ComponentAnalysis, Analysis } from '../../types' */ /** @import { ComponentAnalysis, Analysis } from '../../types' */
/** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */ /** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
@ -180,7 +180,7 @@ export function client_component(analysis, options) {
}; };
const module = /** @type {ESTree.Program} */ ( const module = /** @type {ESTree.Program} */ (
walk(/** @type {SvelteNode} */ (analysis.module.ast), state, visitors) walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, visitors)
); );
const instance_state = { const instance_state = {
@ -192,12 +192,12 @@ export function client_component(analysis, options) {
}; };
const instance = /** @type {ESTree.Program} */ ( const instance = /** @type {ESTree.Program} */ (
walk(/** @type {SvelteNode} */ (analysis.instance.ast), instance_state, visitors) walk(/** @type {AST.SvelteNode} */ (analysis.instance.ast), instance_state, visitors)
); );
const template = /** @type {ESTree.Program} */ ( const template = /** @type {ESTree.Program} */ (
walk( walk(
/** @type {SvelteNode} */ (analysis.template.ast), /** @type {AST.SvelteNode} */ (analysis.template.ast),
{ {
...state, ...state,
transform: instance_state.transform, transform: instance_state.transform,
@ -666,7 +666,7 @@ export function client_module(analysis, options) {
}; };
const module = /** @type {ESTree.Program} */ ( const module = /** @type {ESTree.Program} */ (
walk(/** @type {SvelteNode} */ (analysis.module.ast), state, visitors) walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, visitors)
); );
return { return {

@ -9,7 +9,7 @@ import type {
UpdateExpression, UpdateExpression,
VariableDeclaration VariableDeclaration
} from 'estree'; } from 'estree';
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler'; import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js'; import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js'; import type { ComponentAnalysis } from '../../types.js';
import type { SourceLocation } from '#shared'; import type { SourceLocation } from '#shared';
@ -98,14 +98,14 @@ export interface StateField {
id: PrivateIdentifier; id: PrivateIdentifier;
} }
export type Context = import('zimmerframe').Context<SvelteNode, ClientTransformState>; export type Context = import('zimmerframe').Context<AST.SvelteNode, ClientTransformState>;
export type Visitors = import('zimmerframe').Visitors<SvelteNode, any>; export type Visitors = import('zimmerframe').Visitors<AST.SvelteNode, any>;
export type ComponentContext = import('zimmerframe').Context< export type ComponentContext = import('zimmerframe').Context<
SvelteNode, AST.SvelteNode,
ComponentClientTransformState ComponentClientTransformState
>; >;
export type ComponentVisitors = import('zimmerframe').Visitors< export type ComponentVisitors = import('zimmerframe').Visitors<
SvelteNode, AST.SvelteNode,
ComponentClientTransformState ComponentClientTransformState
>; >;

@ -1,5 +1,5 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */ /** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */
/** @import { Binding, SvelteNode } from '#compiler' */ /** @import { AST, Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } from '../../types.js' */ /** @import { Analysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */ /** @import { Scope } from '../../scope.js' */
@ -271,7 +271,7 @@ export function should_proxy(node, scope) {
/** /**
* @param {Pattern} node * @param {Pattern} node
* @param {import('zimmerframe').Context<SvelteNode, ComponentClientTransformState>} context * @param {import('zimmerframe').Context<AST.SvelteNode, ComponentClientTransformState>} context
* @returns {{ id: Pattern, declarations: null | Statement[] }} * @returns {{ id: Pattern, declarations: null | Statement[] }}
*/ */
export function create_derived_block_argument(node, context) { export function create_derived_block_argument(node, context) {

@ -8,7 +8,7 @@ import {
get_attribute_expression, get_attribute_expression,
is_event_attribute is_event_attribute
} from '../../../../utils/ast.js'; } from '../../../../utils/ast.js';
import { dev, filename, is_ignored, locator } from '../../../../state.js'; import { dev, filename, is_ignored, locate_node, locator } from '../../../../state.js';
import { build_proxy_reassignment, should_proxy } from '../utils.js'; import { build_proxy_reassignment, should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js'; import { visit_assignment_expression } from '../../shared/assignments.js';
@ -183,9 +183,6 @@ function build_assignment(operator, left, right, context) {
if (left.type === 'MemberExpression' && should_transform) { if (left.type === 'MemberExpression' && should_transform) {
const callee = callees[operator]; const callee = callees[operator];
const loc = /** @type {Location} */ (locator(/** @type {number} */ (left.start)));
const location = `${filename}:${loc.line}:${loc.column}`;
return /** @type {Expression} */ ( return /** @type {Expression} */ (
context.visit( context.visit(
b.call( b.call(
@ -197,7 +194,7 @@ function build_assignment(operator, left, right, context) {
: b.literal(/** @type {Identifier} */ (left.property).name) : b.literal(/** @type {Identifier} */ (left.property).name)
), ),
right, right,
b.literal(location) b.literal(locate_node(left))
) )
) )
); );

@ -1,5 +1,5 @@
/** @import { CallExpression, Expression, MemberExpression, Pattern } from 'estree' */ /** @import { CallExpression, Expression, Pattern } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { dev, is_ignored } from '../../../../state.js'; import { dev, is_ignored } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js'; import { is_text_attribute } from '../../../../utils/ast.js';
@ -16,7 +16,7 @@ export function BindDirective(node, context) {
const expression = /** @type {Expression} */ (context.visit(node.expression)); const expression = /** @type {Expression} */ (context.visit(node.expression));
const property = binding_properties[node.name]; const property = binding_properties[node.name];
const parent = /** @type {SvelteNode} */ (context.path.at(-1)); const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
let get, set; let get, set;

@ -1,6 +1,7 @@
/** @import { BlockStatement } from 'estree' */ /** @import { ArrowFunctionExpression, BlockStatement, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Statement } from 'estree' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { add_state_transformers } from './shared/declarations.js'; import { add_state_transformers } from './shared/declarations.js';
import * as b from '../../../../utils/builders.js';
/** /**
* @param {BlockStatement} node * @param {BlockStatement} node
@ -8,5 +9,24 @@ import { add_state_transformers } from './shared/declarations.js';
*/ */
export function BlockStatement(node, context) { export function BlockStatement(node, context) {
add_state_transformers(context); add_state_transformers(context);
const tracing = context.state.scope.tracing;
if (tracing !== null) {
const parent =
/** @type {ArrowFunctionExpression | FunctionDeclaration | FunctionExpression} */ (
context.path.at(-1)
);
const is_async = parent.async;
const call = b.call(
'$.trace',
/** @type {Expression} */ (tracing),
b.thunk(b.block(node.body.map((n) => /** @type {Statement} */ (context.visit(n)))), is_async)
);
return b.block([b.return(is_async ? b.await(call) : call)]);
}
context.next(); context.next();
} }

@ -20,6 +20,10 @@ export function ExpressionStatement(node, context) {
return b.stmt(expr); return b.stmt(expr);
} }
if (rune === '$inspect.trace') {
return b.empty;
}
} }
context.next(); context.next();

@ -27,6 +27,7 @@ export function VariableDeclaration(node, context) {
rune === '$effect.tracking' || rune === '$effect.tracking' ||
rune === '$effect.root' || rune === '$effect.root' ||
rune === '$inspect' || rune === '$inspect' ||
rune === '$inspect.trace' ||
rune === '$state.snapshot' || rune === '$state.snapshot' ||
rune === '$host' rune === '$host'
) { ) {

@ -1,5 +1,5 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { AST, TemplateNode } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */ /** @import { ComponentContext } from '../../types.js' */
import { dev, is_ignored } from '../../../../../state.js'; import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
@ -35,7 +35,7 @@ export function build_component(node, component_name, context, anchor = context.
} }
}; };
/** @type {Record<string, TemplateNode[]>} */ /** @type {Record<string, AST.TemplateNode[]>} */
const children = {}; const children = {};
/** @type {Record<string, Expression[]>} */ /** @type {Record<string, Expression[]>} */

@ -1,5 +1,5 @@
/** @import { Expression } from 'estree' */ /** @import { Expression } from 'estree' */
/** @import { AST, ExpressionMetadata, SvelteNode } from '#compiler' */ /** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext } from '../../types' */ /** @import { ComponentContext } from '../../types' */
import { is_capture_event, is_passive_event } from '../../../../../../utils.js'; import { is_capture_event, is_passive_event } from '../../../../../../utils.js';
import { dev, locator } from '../../../../../state.js'; import { dev, locator } from '../../../../../state.js';
@ -68,7 +68,7 @@ export function visit_event_attribute(node, context) {
) )
); );
const type = /** @type {SvelteNode} */ (context.path.at(-1)).type; const type = /** @type {AST.SvelteNode} */ (context.path.at(-1)).type;
if ( if (
type === 'SvelteDocument' || type === 'SvelteDocument' ||

@ -1,5 +1,5 @@
/** @import { Expression } from 'estree' */ /** @import { Expression } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types' */ /** @import { ComponentContext } from '../../types' */
import { cannot_be_set_statically } from '../../../../../../utils.js'; import { cannot_be_set_statically } from '../../../../../../utils.js';
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js'; import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
@ -10,7 +10,7 @@ import { build_template_chunk, build_update } from './utils.js';
* Processes an array of template nodes, joining sibling text/expression nodes * Processes an array of template nodes, joining sibling text/expression nodes
* (e.g. `{a} b {c}`) into a single update function. Along the way it creates * (e.g. `{a} b {c}`) into a single update function. Along the way it creates
* corresponding template node references these updates are applied to. * corresponding template node references these updates are applied to.
* @param {SvelteNode[]} nodes * @param {AST.SvelteNode[]} nodes
* @param {(is_text: boolean) => Expression} initial * @param {(is_text: boolean) => Expression} initial
* @param {boolean} is_element * @param {boolean} is_element
* @param {ComponentContext} context * @param {ComponentContext} context
@ -124,7 +124,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
} }
/** /**
* @param {SvelteNode} node * @param {AST.SvelteNode} node
* @param {ComponentContext["state"]} state * @param {ComponentContext["state"]} state
*/ */
function is_static_element(node, state) { function is_static_element(node, state) {

@ -1,5 +1,5 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */ /** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState } from '../../types' */ /** @import { ComponentClientTransformState } from '../../types' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js'; import { object } from '../../../../../utils/ast.js';
@ -12,7 +12,7 @@ import { locator } from '../../../../../state.js';
/** /**
* @param {Array<AST.Text | AST.ExpressionTag>} values * @param {Array<AST.Text | AST.ExpressionTag>} values
* @param {(node: SvelteNode, state: any) => any} visit * @param {(node: AST.SvelteNode, state: any) => any} visit
* @param {ComponentClientTransformState} state * @param {ComponentClientTransformState} state
* @returns {{ value: Expression, has_state: boolean, has_call: boolean }} * @returns {{ value: Expression, has_state: boolean, has_call: boolean }}
*/ */
@ -145,7 +145,7 @@ export function build_update_assignment(state, id, init, value, update) {
* Serializes `bind:this` for components and elements. * Serializes `bind:this` for components and elements.
* @param {Identifier | MemberExpression | SequenceExpression} expression * @param {Identifier | MemberExpression | SequenceExpression} expression
* @param {Expression} value * @param {Expression} value
* @param {import('zimmerframe').Context<SvelteNode, ComponentClientTransformState>} context * @param {import('zimmerframe').Context<AST.SvelteNode, ComponentClientTransformState>} context
*/ */
export function build_bind_this(expression, value, { state, visit }) { export function build_bind_this(expression, value, { state, visit }) {
if (expression.type === 'SequenceExpression') { if (expression.type === 'SequenceExpression') {

@ -1,5 +1,5 @@
/** @import { Visitors } from 'zimmerframe' */ /** @import { Visitors } from 'zimmerframe' */
/** @import { ValidatedCompileOptions, Css } from '#compiler' */ /** @import { AST, ValidatedCompileOptions } from '#compiler' */
/** @import { ComponentAnalysis } from '../../types.js' */ /** @import { ComponentAnalysis } from '../../types.js' */
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
@ -41,9 +41,9 @@ export function render_stylesheet(source, analysis, options) {
} }
}; };
const ast = /** @type {Css.StyleSheet} */ (analysis.css.ast); const ast = /** @type {AST.CSS.StyleSheet} */ (analysis.css.ast);
walk(/** @type {Css.Node} */ (ast), state, visitors); walk(/** @type {AST.CSS.Node} */ (ast), state, visitors);
code.remove(0, ast.content.start); code.remove(0, ast.content.start);
code.remove(/** @type {number} */ (ast.content.end), source.length); code.remove(/** @type {number} */ (ast.content.end), source.length);
@ -71,7 +71,7 @@ export function render_stylesheet(source, analysis, options) {
return css; return css;
} }
/** @type {Visitors<Css.Node, State>} */ /** @type {Visitors<AST.CSS.Node, State>} */
const visitors = { const visitors = {
_: (node, context) => { _: (node, context) => {
context.state.code.addSourcemapLocation(node.start); context.state.code.addSourcemapLocation(node.start);
@ -259,7 +259,7 @@ const visitors = {
if (parent?.type === 'Rule') { if (parent?.type === 'Rule') {
specificity = { bumped: false }; specificity = { bumped: false };
/** @type {Css.Rule | null} */ /** @type {AST.CSS.Rule | null} */
let rule = parent.metadata.parent_rule; let rule = parent.metadata.parent_rule;
while (rule) { while (rule) {
@ -278,7 +278,7 @@ const visitors = {
for (const relative_selector of node.children) { for (const relative_selector of node.children) {
if (relative_selector.metadata.is_global) { if (relative_selector.metadata.is_global) {
const global = /** @type {Css.PseudoClassSelector} */ (relative_selector.selectors[0]); const global = /** @type {AST.CSS.PseudoClassSelector} */ (relative_selector.selectors[0]);
remove_global_pseudo_class(global, relative_selector.combinator, context.state); remove_global_pseudo_class(global, relative_selector.combinator, context.state);
if ( if (
@ -361,15 +361,15 @@ const visitors = {
/** /**
* *
* @param {Array<Css.Node>} path * @param {Array<AST.CSS.Node>} path
*/ */
function is_in_global_block(path) { function is_in_global_block(path) {
return path.some((node) => node.type === 'Rule' && node.metadata.is_global_block); return path.some((node) => node.type === 'Rule' && node.metadata.is_global_block);
} }
/** /**
* @param {Css.PseudoClassSelector} selector * @param {AST.CSS.PseudoClassSelector} selector
* @param {Css.Combinator | null} combinator * @param {AST.CSS.Combinator | null} combinator
* @param {State} state * @param {State} state
*/ */
function remove_global_pseudo_class(selector, combinator, state) { function remove_global_pseudo_class(selector, combinator, state) {
@ -399,7 +399,7 @@ function remove_preceding_whitespace(end, state) {
} }
/** /**
* @param {Css.Rule} rule * @param {AST.CSS.Rule} rule
* @param {boolean} is_in_global_block * @param {boolean} is_in_global_block
*/ */
function is_empty(rule, is_in_global_block) { function is_empty(rule, is_in_global_block) {
@ -426,14 +426,14 @@ function is_empty(rule, is_in_global_block) {
return true; return true;
} }
/** @param {Css.Rule} rule */ /** @param {AST.CSS.Rule} rule */
function is_used(rule) { function is_used(rule) {
return rule.prelude.children.some((selector) => selector.metadata.used); return rule.prelude.children.some((selector) => selector.metadata.used);
} }
/** /**
* *
* @param {Css.Rule} node * @param {AST.CSS.Rule} node
* @param {MagicString} code * @param {MagicString} code
*/ */
function escape_comment_close(node, code) { function escape_comment_close(node, code) {

@ -1,5 +1,5 @@
/** @import { Program, Property, Statement, VariableDeclarator } from 'estree' */ /** @import { Program, Property, Statement, VariableDeclarator } from 'estree' */
/** @import { SvelteNode, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { ComponentServerTransformState, ComponentVisitors, ServerTransformState, Visitors } from './types.js' */ /** @import { ComponentServerTransformState, ComponentVisitors, ServerTransformState, Visitors } from './types.js' */
/** @import { Analysis, ComponentAnalysis } from '../../types.js' */ /** @import { Analysis, ComponentAnalysis } from '../../types.js' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
@ -106,12 +106,12 @@ export function server_component(analysis, options) {
}; };
const module = /** @type {Program} */ ( const module = /** @type {Program} */ (
walk(/** @type {SvelteNode} */ (analysis.module.ast), state, global_visitors) walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors)
); );
const instance = /** @type {Program} */ ( const instance = /** @type {Program} */ (
walk( walk(
/** @type {SvelteNode} */ (analysis.instance.ast), /** @type {AST.SvelteNode} */ (analysis.instance.ast),
{ ...state, scopes: analysis.instance.scopes }, { ...state, scopes: analysis.instance.scopes },
{ {
...global_visitors, ...global_visitors,
@ -132,7 +132,7 @@ export function server_component(analysis, options) {
const template = /** @type {Program} */ ( const template = /** @type {Program} */ (
walk( walk(
/** @type {SvelteNode} */ (analysis.template.ast), /** @type {AST.SvelteNode} */ (analysis.template.ast),
{ ...state, scopes: analysis.template.scopes }, { ...state, scopes: analysis.template.scopes },
// @ts-expect-error don't know, don't care // @ts-expect-error don't know, don't care
{ ...global_visitors, ...template_visitors } { ...global_visitors, ...template_visitors }
@ -396,7 +396,7 @@ export function server_module(analysis, options) {
}; };
const module = /** @type {Program} */ ( const module = /** @type {Program} */ (
walk(/** @type {SvelteNode} */ (analysis.module.ast), state, global_visitors) walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors)
); );
return { return {

@ -1,5 +1,5 @@
import type { Expression, Statement, ModuleDeclaration, LabeledStatement } from 'estree'; import type { Expression, Statement, ModuleDeclaration, LabeledStatement } from 'estree';
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler'; import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js'; import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js'; import type { ComponentAnalysis } from '../../types.js';
import type { StateField } from '../client/types.js'; import type { StateField } from '../client/types.js';
@ -25,14 +25,14 @@ export interface ComponentServerTransformState extends ServerTransformState {
readonly skip_hydration_boundaries: boolean; readonly skip_hydration_boundaries: boolean;
} }
export type Context = import('zimmerframe').Context<SvelteNode, ServerTransformState>; export type Context = import('zimmerframe').Context<AST.SvelteNode, ServerTransformState>;
export type Visitors = import('zimmerframe').Visitors<SvelteNode, ServerTransformState>; export type Visitors = import('zimmerframe').Visitors<AST.SvelteNode, ServerTransformState>;
export type ComponentContext = import('zimmerframe').Context< export type ComponentContext = import('zimmerframe').Context<
SvelteNode, AST.SvelteNode,
ComponentServerTransformState ComponentServerTransformState
>; >;
export type ComponentVisitors = import('zimmerframe').Visitors< export type ComponentVisitors = import('zimmerframe').Visitors<
SvelteNode, AST.SvelteNode,
ComponentServerTransformState ComponentServerTransformState
>; >;

@ -1,5 +1,5 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */ /** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */
/** @import { SvelteNode } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Context, ServerTransformState } from '../types.js' */ /** @import { Context, ServerTransformState } from '../types.js' */
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import { build_assignment_value } from '../../../../utils/ast.js'; import { build_assignment_value } from '../../../../utils/ast.js';
@ -18,7 +18,7 @@ export function AssignmentExpression(node, context) {
* @param {AssignmentOperator} operator * @param {AssignmentOperator} operator
* @param {Pattern} left * @param {Pattern} left
* @param {Expression} right * @param {Expression} right
* @param {import('zimmerframe').Context<SvelteNode, ServerTransformState>} context * @param {import('zimmerframe').Context<AST.SvelteNode, ServerTransformState>} context
* @returns {Expression | null} * @returns {Expression | null}
*/ */
function build_assignment(operator, left, right, context) { function build_assignment(operator, left, right, context) {

@ -10,7 +10,12 @@ import { get_rune } from '../../../scope.js';
export function ExpressionStatement(node, context) { export function ExpressionStatement(node, context) {
const rune = get_rune(node.expression, context.state.scope); const rune = get_rune(node.expression, context.state.scope);
if (rune === '$effect' || rune === '$effect.pre' || rune === '$effect.root') { if (
rune === '$effect' ||
rune === '$effect.pre' ||
rune === '$effect.root' ||
rune === '$inspect.trace'
) {
return b.empty; return b.empty;
} }

@ -1,5 +1,5 @@
/** @import { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { AST, TemplateNode } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */ /** @import { ComponentContext } from '../../types.js' */
import { empty_comment, build_attribute_value } from './utils.js'; import { empty_comment, build_attribute_value } from './utils.js';
import * as b from '../../../../../utils/builders.js'; import * as b from '../../../../../utils/builders.js';
@ -31,7 +31,7 @@ export function build_inline_component(node, expression, context) {
scope: node.metadata.scopes.default scope: node.metadata.scopes.default
}; };
/** @type {Record<string, TemplateNode[]>} */ /** @type {Record<string, AST.TemplateNode[]>} */
const children = {}; const children = {};
/** /**

@ -1,5 +1,5 @@
/** @import { Expression, Literal } from 'estree' */ /** @import { Expression, Literal } from 'estree' */
/** @import { AST, Namespace, SvelteNode } from '#compiler' */ /** @import { AST, Namespace } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */ /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
import { import {
get_attribute_chunks, get_attribute_chunks,
@ -31,7 +31,7 @@ const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style'];
* Writes the output to the template output. Some elements may have attributes on them that require the * Writes the output to the template output. Some elements may have attributes on them that require the
* their output to be the child content instead. In this case, an object is returned. * their output to be the child content instead. In this case, an object is returned.
* @param {AST.RegularElement | AST.SvelteElement} node * @param {AST.RegularElement | AST.SvelteElement} node
* @param {import('zimmerframe').Context<SvelteNode, ComponentServerTransformState>} context * @param {import('zimmerframe').Context<AST.SvelteNode, ComponentServerTransformState>} context
*/ */
export function build_element_attributes(node, context) { export function build_element_attributes(node, context) {
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */ /** @type {Array<AST.Attribute | AST.SpreadAttribute>} */

@ -1,5 +1,5 @@
/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, TemplateElement } from 'estree' */ /** @import { AssignmentOperator, Expression, Identifier, Node, Statement, TemplateElement } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */ /** @import { ComponentContext, ServerTransformState } from '../../types.js' */
import { escape_html } from '../../../../../../escaping.js'; import { escape_html } from '../../../../../../escaping.js';
@ -24,7 +24,7 @@ export const empty_comment = b.literal(EMPTY_COMMENT);
/** /**
* Processes an array of template nodes, joining sibling text/expression nodes and * Processes an array of template nodes, joining sibling text/expression nodes and
* recursing into child nodes. * recursing into child nodes.
* @param {Array<SvelteNode>} nodes * @param {Array<AST.SvelteNode>} nodes
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function process_children(nodes, { visit, state }) { export function process_children(nodes, { visit, state }) {

@ -1,10 +1,10 @@
import type { Scope } from '../scope.js'; import type { Scope } from '../scope.js';
import type { SvelteNode, ValidatedModuleCompileOptions } from '#compiler'; import type { AST, ValidatedModuleCompileOptions } from '#compiler';
import type { Analysis } from '../types.js'; import type { Analysis } from '../types.js';
export interface TransformState { export interface TransformState {
readonly analysis: Analysis; readonly analysis: Analysis;
readonly options: ValidatedModuleCompileOptions; readonly options: ValidatedModuleCompileOptions;
readonly scope: Scope; readonly scope: Scope;
readonly scopes: Map<SvelteNode, Scope>; readonly scopes: Map<AST.SvelteNode, Scope>;
} }

@ -1,6 +1,6 @@
/** @import { Context } from 'zimmerframe' */ /** @import { Context } from 'zimmerframe' */
/** @import { TransformState } from './types.js' */ /** @import { TransformState } from './types.js' */
/** @import { AST, Binding, Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler' */ /** @import { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler' */
/** @import { Node, Expression, CallExpression } from 'estree' */ /** @import { Node, Expression, CallExpression } from 'estree' */
import { import {
regex_ends_with_whitespaces, regex_ends_with_whitespaces,
@ -33,7 +33,7 @@ export function is_hoisted_function(node) {
/** /**
* Match Svelte 4 behaviour by sorting ConstTag nodes in topological order * Match Svelte 4 behaviour by sorting ConstTag nodes in topological order
* @param {SvelteNode[]} nodes * @param {AST.SvelteNode[]} nodes
* @param {TransformState} state * @param {TransformState} state
*/ */
function sort_const_tags(nodes, state) { function sort_const_tags(nodes, state) {
@ -133,9 +133,9 @@ function sort_const_tags(nodes, state) {
* unless it's whitespace-only, in which case collapse to a single whitespace for all cases * unless it's whitespace-only, in which case collapse to a single whitespace for all cases
* except when it's children of certain elements where we know ignore whitespace (like td/option/head), * except when it's children of certain elements where we know ignore whitespace (like td/option/head),
* in which case we remove it entirely * in which case we remove it entirely
* @param {SvelteNode} parent * @param {AST.SvelteNode} parent
* @param {SvelteNode[]} nodes * @param {AST.SvelteNode[]} nodes
* @param {SvelteNode[]} path * @param {AST.SvelteNode[]} path
* @param {Namespace} namespace * @param {Namespace} namespace
* @param {TransformState & { options: ValidatedCompileOptions }} state * @param {TransformState & { options: ValidatedCompileOptions }} state
* @param {boolean} preserve_whitespace * @param {boolean} preserve_whitespace
@ -157,10 +157,10 @@ export function clean_nodes(
nodes = sort_const_tags(nodes, state); nodes = sort_const_tags(nodes, state);
} }
/** @type {SvelteNode[]} */ /** @type {AST.SvelteNode[]} */
const hoisted = []; const hoisted = [];
/** @type {SvelteNode[]} */ /** @type {AST.SvelteNode[]} */
const regular = []; const regular = [];
for (const node of nodes) { for (const node of nodes) {
@ -317,8 +317,8 @@ export function clean_nodes(
/** /**
* Infers the namespace for the children of a node that should be used when creating the `$.template(...)`. * Infers the namespace for the children of a node that should be used when creating the `$.template(...)`.
* @param {Namespace} namespace * @param {Namespace} namespace
* @param {SvelteNode} parent * @param {AST.SvelteNode} parent
* @param {SvelteNode[]} nodes * @param {AST.SvelteNode[]} nodes
*/ */
export function infer_namespace(namespace, parent, nodes) { export function infer_namespace(namespace, parent, nodes) {
if (parent.type === 'RegularElement' && parent.name === 'foreignObject') { if (parent.type === 'RegularElement' && parent.name === 'foreignObject') {
@ -348,14 +348,31 @@ export function infer_namespace(namespace, parent, nodes) {
} }
} }
return namespace; /** @type {Namespace | null} */
let new_namespace = null;
// Check the elements within the fragment and look for consistent namespaces.
// If we have no namespaces or they are mixed, then fallback to existing namespace
for (const node of nodes) {
if (node.type !== 'RegularElement') continue;
if (node.metadata.mathml) {
new_namespace = new_namespace === null || new_namespace === 'mathml' ? 'mathml' : 'html';
} else if (node.metadata.svg) {
new_namespace = new_namespace === null || new_namespace === 'svg' ? 'svg' : 'html';
} else {
return 'html';
}
}
return new_namespace ?? namespace;
} }
/** /**
* Heuristic: Keep current namespace, unless we find a regular element, * Heuristic: Keep current namespace, unless we find a regular element,
* in which case we always want html, or we only find svg nodes, * in which case we always want html, or we only find svg nodes,
* in which case we assume svg. * in which case we assume svg.
* @param {SvelteNode[]} nodes * @param {AST.SvelteNode[]} nodes
* @param {Namespace | 'keep' | 'maybe_html'} namespace * @param {Namespace | 'keep' | 'maybe_html'} namespace
*/ */
function check_nodes_for_namespace(nodes, namespace) { function check_nodes_for_namespace(nodes, namespace) {

@ -1,4 +1,4 @@
/** @import { Css } from '#compiler' */ /** @import { AST } from '#compiler' */
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/; const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
export const regex_css_name_boundary = /^[\s,;}]$/; export const regex_css_name_boundary = /^[\s,;}]$/;
@ -10,5 +10,5 @@ export function remove_css_prefix(name) {
return name.replace(regex_css_browser_prefix, ''); return name.replace(regex_css_browser_prefix, '');
} }
/** @param {Css.Atrule} node */ /** @param {AST.CSS.Atrule} node */
export const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes'; export const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes';

@ -1,4 +1,4 @@
/** @import { AST, ExpressionMetadata, SvelteNode } from '#compiler' */ /** @import { AST, ExpressionMetadata } from '#compiler' */
/** /**
* All nodes that can appear elsewhere than the top level, have attributes and can contain children * All nodes that can appear elsewhere than the top level, have attributes and can contain children
*/ */
@ -14,7 +14,7 @@ const element_nodes = [
/** /**
* Returns true for all nodes that can appear elsewhere than the top level, have attributes and can contain children * Returns true for all nodes that can appear elsewhere than the top level, have attributes and can contain children
* @param {SvelteNode} node * @param {AST.SvelteNode} node
* @returns {node is AST.Component | AST.RegularElement | AST.SlotElement | AST.SvelteComponent | AST.SvelteElement | AST.SvelteFragment | AST.SvelteSelf} * @returns {node is AST.Component | AST.RegularElement | AST.SlotElement | AST.SvelteComponent | AST.SvelteElement | AST.SvelteFragment | AST.SvelteSelf}
*/ */
export function is_element_node(node) { export function is_element_node(node) {

@ -1,6 +1,6 @@
/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ /** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */ /** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AST, Binding, DeclarationKind, ElementLike, SvelteNode } from '#compiler' */ /** @import { AST, Binding, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { create_expression_metadata } from './nodes.js'; import { create_expression_metadata } from './nodes.js';
@ -48,7 +48,7 @@ export class Scope {
/** /**
* A set of all the names referenced with this scope * A set of all the names referenced with this scope
* — useful for generating unique names * — useful for generating unique names
* @type {Map<string, { node: Identifier; path: SvelteNode[] }[]>} * @type {Map<string, { node: Identifier; path: AST.SvelteNode[] }[]>}
*/ */
references = new Map(); references = new Map();
@ -58,6 +58,12 @@ export class Scope {
*/ */
function_depth = 0; function_depth = 0;
/**
* If tracing of reactive dependencies is enabled for this scope
* @type {null | Expression}
*/
tracing = null;
/** /**
* *
* @param {ScopeRoot} root * @param {ScopeRoot} root
@ -179,7 +185,7 @@ export class Scope {
/** /**
* @param {Identifier} node * @param {Identifier} node
* @param {SvelteNode[]} path * @param {AST.SvelteNode[]} path
*/ */
reference(node, path) { reference(node, path) {
path = [...path]; // ensure that mutations to path afterwards don't affect this reference path = [...path]; // ensure that mutations to path afterwards don't affect this reference
@ -225,7 +231,7 @@ export class ScopeRoot {
} }
/** /**
* @param {SvelteNode} ast * @param {AST.SvelteNode} ast
* @param {ScopeRoot} root * @param {ScopeRoot} root
* @param {boolean} allow_reactive_declarations * @param {boolean} allow_reactive_declarations
* @param {Scope | null} parent * @param {Scope | null} parent
@ -235,7 +241,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
/** /**
* A map of node->associated scope. A node appearing in this map does not necessarily mean that it created a scope * A map of node->associated scope. A node appearing in this map does not necessarily mean that it created a scope
* @type {Map<SvelteNode, Scope>} * @type {Map<AST.SvelteNode, Scope>}
*/ */
const scopes = new Map(); const scopes = new Map();
const scope = new Scope(root, parent, false); const scope = new Scope(root, parent, false);
@ -244,7 +250,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
/** @type {State} */ /** @type {State} */
const state = { scope }; const state = { scope };
/** @type {[Scope, { node: Identifier; path: SvelteNode[] }][]} */ /** @type {[Scope, { node: Identifier; path: AST.SvelteNode[] }][]} */
const references = []; const references = [];
/** @type {[Scope, Pattern | MemberExpression][]} */ /** @type {[Scope, Pattern | MemberExpression][]} */
@ -269,7 +275,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
} }
/** /**
* @type {Visitor<Node, State, SvelteNode>} * @type {Visitor<Node, State, AST.SvelteNode>}
*/ */
const create_block_scope = (node, { state, next }) => { const create_block_scope = (node, { state, next }) => {
const scope = state.scope.child(true); const scope = state.scope.child(true);
@ -279,7 +285,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}; };
/** /**
* @type {Visitor<ElementLike, State, SvelteNode>} * @type {Visitor<AST.ElementLike, State, AST.SvelteNode>}
*/ */
const SvelteFragment = (node, { state, next }) => { const SvelteFragment = (node, { state, next }) => {
const scope = state.scope.child(); const scope = state.scope.child();
@ -288,7 +294,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}; };
/** /**
* @type {Visitor<AST.Component | AST.SvelteComponent | AST.SvelteSelf, State, SvelteNode>} * @type {Visitor<AST.Component | AST.SvelteComponent | AST.SvelteSelf, State, AST.SvelteNode>}
*/ */
const Component = (node, context) => { const Component = (node, context) => {
node.metadata.scopes = { node.metadata.scopes = {
@ -329,7 +335,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}; };
/** /**
* @type {Visitor<AST.AnimateDirective | AST.TransitionDirective | AST.UseDirective, State, SvelteNode>} * @type {Visitor<AST.AnimateDirective | AST.TransitionDirective | AST.UseDirective, State, AST.SvelteNode>}
*/ */
const SvelteDirective = (node, { state, path, visit }) => { const SvelteDirective = (node, { state, path, visit }) => {
state.scope.reference(b.id(node.name.split('.')[0]), path); state.scope.reference(b.id(node.name.split('.')[0]), path);
@ -713,9 +719,9 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
} }
/** /**
* @template {{ scope: Scope, scopes: Map<SvelteNode, Scope> }} State * @template {{ scope: Scope, scopes: Map<AST.SvelteNode, Scope> }} State
* @param {SvelteNode} node * @param {AST.SvelteNode} node
* @param {Context<SvelteNode, State>} context * @param {Context<AST.SvelteNode, State>} context
*/ */
export function set_scope(node, { next, state }) { export function set_scope(node, { next, state }) {
const scope = state.scopes.get(node); const scope = state.scopes.get(node);

@ -1,17 +1,17 @@
import type { AST, Binding, Css, SvelteNode } from '#compiler'; import type { AST, Binding } from '#compiler';
import type { Identifier, LabeledStatement, Node, Program } from 'estree'; import type { Identifier, LabeledStatement, Node, Program } from 'estree';
import type { Scope, ScopeRoot } from './scope.js'; import type { Scope, ScopeRoot } from './scope.js';
export interface Js { export interface Js {
ast: Program; ast: Program;
scope: Scope; scope: Scope;
scopes: Map<SvelteNode, Scope>; scopes: Map<AST.SvelteNode, Scope>;
} }
export interface Template { export interface Template {
ast: AST.Fragment; ast: AST.Fragment;
scope: Scope; scope: Scope;
scopes: Map<SvelteNode, Scope>; scopes: Map<AST.SvelteNode, Scope>;
} }
export interface ReactiveStatement { export interface ReactiveStatement {
@ -66,7 +66,7 @@ export interface ComponentAnalysis extends Analysis {
binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>; binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>;
slot_names: Map<string, AST.SlotElement>; slot_names: Map<string, AST.SlotElement>;
css: { css: {
ast: Css.StyleSheet | null; ast: AST.CSS.StyleSheet | null;
hash: string; hash: string;
keyframes: string[]; keyframes: string[];
}; };

@ -1,6 +1,8 @@
/** @import { CompileOptions, SvelteNode } from './types' */ /** @import { Location } from 'locate-character' */
/** @import { Warning } from '#compiler' */ /** @import { CompileOptions } from './types' */
/** @import { AST, Warning } from '#compiler' */
import { getLocator } from 'locate-character'; import { getLocator } from 'locate-character';
import { sanitize_location } from '../utils.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */ /** @typedef {{ start?: number, end?: number }} NodeLike */
@ -28,6 +30,14 @@ export let dev;
export let locator = getLocator('', { offsetLine: 1 }); export let locator = getLocator('', { offsetLine: 1 });
/**
* @param {AST.SvelteNode & { start?: number | undefined }} node
*/
export function locate_node(node) {
const loc = /** @type {Location} */ (locator(/** @type {number} */ (node.start)));
return `${sanitize_location(filename)}:${loc?.line}:${loc.column}`;
}
/** @type {NonNullable<CompileOptions['warningFilter']>} */ /** @type {NonNullable<CompileOptions['warningFilter']>} */
export let warning_filter; export let warning_filter;
@ -41,7 +51,7 @@ export let ignore_stack = [];
* For each node the list of warnings that should be ignored for that node. * For each node the list of warnings that should be ignored for that node.
* Exists in addition to `ignore_stack` because not all warnings are emitted * Exists in addition to `ignore_stack` because not all warnings are emitted
* while the stack is being built. * while the stack is being built.
* @type {Map<SvelteNode | NodeLike, Set<string>[]>} * @type {Map<AST.SvelteNode | NodeLike, Set<string>[]>}
*/ */
export let ignore_map = new Map(); export let ignore_map = new Map();
@ -66,7 +76,7 @@ export function reset_warning_filter(fn = () => true) {
} }
/** /**
* @param {SvelteNode | NodeLike} node * @param {AST.SvelteNode | NodeLike} node
* @param {import('../constants.js').IGNORABLE_RUNTIME_WARNINGS[number]} code * @param {import('../constants.js').IGNORABLE_RUNTIME_WARNINGS[number]} code
* @returns * @returns
*/ */

@ -1,6 +1,6 @@
import type { AST } from '#compiler'; import type { AST } from '#compiler';
export namespace Css { export namespace _CSS {
export interface BaseNode { export interface BaseNode {
start: number; start: number;
end: number; end: number;

@ -7,8 +7,7 @@ import type {
} from 'estree'; } from 'estree';
import type { SourceMap } from 'magic-string'; import type { SourceMap } from 'magic-string';
import type { Scope } from '../phases/scope.js'; import type { Scope } from '../phases/scope.js';
import type { Css } from './css.js'; import type { AST, Namespace } from './template.js';
import type { AST, Namespace, SvelteNode } from './template.js';
import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js'; import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js';
/** The return value of `compile` from `svelte/compiler` */ /** The return value of `compile` from `svelte/compiler` */
@ -295,7 +294,7 @@ export interface Binding {
| AST.EachBlock | AST.EachBlock
| AST.SnippetBlock; | AST.SnippetBlock;
is_called: boolean; is_called: boolean;
references: { node: Identifier; path: SvelteNode[] }[]; references: { node: Identifier; path: AST.SvelteNode[] }[];
mutated: boolean; mutated: boolean;
reassigned: boolean; reassigned: boolean;
/** `true` if mutated _or_ reassigned */ /** `true` if mutated _or_ reassigned */
@ -322,7 +321,6 @@ export interface ExpressionMetadata {
} }
export * from './template.js'; export * from './template.js';
export { Css };
// TODO this chain is a bit weird // TODO this chain is a bit weird
export { ReactiveStatement } from '../phases/types.js'; export { ReactiveStatement } from '../phases/types.js';

@ -1,4 +1,4 @@
import type { AST, Css } from '#compiler'; import type { AST } from '#compiler';
import type { import type {
ArrayExpression, ArrayExpression,
AssignmentExpression, AssignmentExpression,
@ -260,7 +260,7 @@ export interface LegacyStyle extends BaseNode {
export interface LegacySelector extends BaseNode { export interface LegacySelector extends BaseNode {
type: 'Selector'; type: 'Selector';
children: Array<Css.Combinator | Css.SimpleSelector>; children: Array<AST.CSS.Combinator | AST.CSS.SimpleSelector>;
} }
export type LegacyCssNode = LegacyStyle | LegacySelector; export type LegacyCssNode = LegacyStyle | LegacySelector;

@ -1,4 +1,4 @@
import type { Binding, Css, ExpressionMetadata } from '#compiler'; import type { Binding, ExpressionMetadata } from '#compiler';
import type { import type {
ArrayExpression, ArrayExpression,
ArrowFunctionExpression, ArrowFunctionExpression,
@ -18,6 +18,7 @@ import type {
SequenceExpression SequenceExpression
} from 'estree'; } from 'estree';
import type { Scope } from '../phases/scope'; import type { Scope } from '../phases/scope';
import type { _CSS } from './css';
/** /**
* - `html` — the default, for e.g. `<div>` or `<span>` * - `html` — the default, for e.g. `<div>` or `<span>`
@ -66,7 +67,7 @@ export namespace AST {
options: SvelteOptions | null; options: SvelteOptions | null;
fragment: Fragment; fragment: Fragment;
/** The parsed `<style>` element, if exists */ /** The parsed `<style>` element, if exists */
css: Css.StyleSheet | null; css: AST.CSS.StyleSheet | null;
/** The parsed `<script>` element, if exists */ /** The parsed `<script>` element, if exists */
instance: Script | null; instance: Script | null;
/** The parsed `<script module>` element, if exists */ /** The parsed `<script module>` element, if exists */
@ -504,11 +505,10 @@ export namespace AST {
content: Program; content: Program;
attributes: Attribute[]; attributes: Attribute[];
} }
}
type Tag = AST.ExpressionTag | AST.HtmlTag | AST.ConstTag | AST.DebugTag | AST.RenderTag; export type AttributeLike = Attribute | SpreadAttribute | Directive;
export type Directive = export type Directive =
| AST.AnimateDirective | AST.AnimateDirective
| AST.BindDirective | AST.BindDirective
| AST.ClassDirective | AST.ClassDirective
@ -518,15 +518,21 @@ export type Directive =
| AST.TransitionDirective | AST.TransitionDirective
| AST.UseDirective; | AST.UseDirective;
export type Block = AST.EachBlock | AST.IfBlock | AST.AwaitBlock | AST.KeyBlock | AST.SnippetBlock; export type Block =
| AST.EachBlock
| AST.IfBlock
| AST.AwaitBlock
| AST.KeyBlock
| AST.SnippetBlock;
export type ElementLike = export type ElementLike =
| AST.Component | AST.Component
| AST.TitleElement | AST.TitleElement
| AST.SlotElement | AST.SlotElement
| AST.RegularElement | AST.RegularElement
| AST.SvelteHTML | AST.SvelteHTML
| AST.SvelteBody | AST.SvelteBody
| AST.SvelteBoundary
| AST.SvelteComponent | AST.SvelteComponent
| AST.SvelteDocument | AST.SvelteDocument
| AST.SvelteElement | AST.SvelteElement
@ -537,7 +543,9 @@ export type ElementLike =
| AST.SvelteWindow | AST.SvelteWindow
| AST.SvelteBoundary; | AST.SvelteBoundary;
export type TemplateNode = export type Tag = AST.ExpressionTag | AST.HtmlTag | AST.ConstTag | AST.DebugTag | AST.RenderTag;
export type TemplateNode =
| AST.Root | AST.Root
| AST.Text | AST.Text
| Tag | Tag
@ -548,7 +556,10 @@ export type TemplateNode =
| AST.Comment | AST.Comment
| Block; | Block;
export type SvelteNode = Node | TemplateNode | AST.Fragment | Css.Node; export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node;
export type { _CSS as CSS };
}
declare module 'estree' { declare module 'estree' {
export interface BaseNode { export interface BaseNode {

@ -1,4 +1,4 @@
/** @import { AST, SvelteNode } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import * as ESTree from 'estree' */ /** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import * as b from '../utils/builders.js'; import * as b from '../utils/builders.js';
@ -393,7 +393,7 @@ function _extract_paths(assignments = [], param, expression, update_expression,
* Like `path.at(x)`, but skips over `TSNonNullExpression` and `TSAsExpression` nodes and eases assertions a bit * Like `path.at(x)`, but skips over `TSNonNullExpression` and `TSAsExpression` nodes and eases assertions a bit
* by removing the `| undefined` from the resulting type. * by removing the `| undefined` from the resulting type.
* *
* @template {SvelteNode} T * @template {AST.SvelteNode} T
* @param {T[]} path * @param {T[]} path
* @param {number} at * @param {number} at
*/ */

@ -1,9 +1,9 @@
/** @import { SvelteNode } from '#compiler' */ /** @import { AST } from '#compiler' */
import { is_element_node } from '../phases/nodes.js'; import { is_element_node } from '../phases/nodes.js';
import { is_text_attribute } from './ast.js'; import { is_text_attribute } from './ast.js';
/** /**
* @param {SvelteNode} node * @param {AST.SvelteNode} node
*/ */
export function determine_slot(node) { export function determine_slot(node) {
if (!is_element_node(node)) return null; if (!is_element_node(node)) return null;

@ -1,6 +1,6 @@
import { sanitize_location } from '../../../utils.js';
import { untrack } from '../runtime.js'; import { untrack } from '../runtime.js';
import * as w from '../warnings.js'; import * as w from '../warnings.js';
import { sanitize_location } from './location.js';
/** /**
* *

@ -1,25 +0,0 @@
import { DEV } from 'esm-env';
import { FILENAME } from '../../../constants.js';
import { dev_current_component_function } from '../runtime.js';
/**
*
* @param {number} [line]
* @param {number} [column]
*/
export function get_location(line, column) {
if (!DEV || line === undefined) return undefined;
var filename = dev_current_component_function?.[FILENAME];
var location = filename && `${filename}:${line}:${column}`;
return sanitize_location(location);
}
/**
* Prevent devtools trying to make `location` a clickable link by inserting a zero-width space
* @param {string | undefined} location
*/
export function sanitize_location(location) {
return location?.replace(/\//g, '/\u200b');
}

@ -0,0 +1,180 @@
/** @import { Derived, Reaction, Signal, Value } from '#client' */
import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { define_property } from '../../shared/utils.js';
import { DERIVED, STATE_SYMBOL } from '../constants.js';
import { effect_tracking } from '../reactivity/effects.js';
import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js';
/** @type { any } */
export let tracing_expressions = null;
/**
* @param { Value } signal
* @param { { read: Error[] } } [entry]
*/
function log_entry(signal, entry) {
const debug = signal.debug;
const value = signal.v;
if (value === UNINITIALIZED) {
return;
}
if (debug) {
var previous_captured_signals = captured_signals;
var captured = new Set();
set_captured_signals(captured);
try {
untrack(() => {
debug();
});
} finally {
set_captured_signals(previous_captured_signals);
}
if (captured.size > 0) {
for (const dep of captured) {
log_entry(dep);
}
return;
}
}
const type = (signal.f & DERIVED) !== 0 ? '$derived' : '$state';
const current_reaction = /** @type {Reaction} */ (active_reaction);
const status =
signal.version > current_reaction.version || current_reaction.version === 0 ? 'dirty' : 'clean';
// eslint-disable-next-line no-console
console.groupCollapsed(
`%c${type}`,
status !== 'clean'
? 'color: CornflowerBlue; font-weight: bold'
: 'color: grey; font-weight: bold',
typeof value === 'object' && STATE_SYMBOL in value ? snapshot(value, true) : value
);
if (type === '$derived') {
const deps = new Set(/** @type {Derived} */ (signal).deps);
for (const dep of deps) {
log_entry(dep);
}
}
if (signal.created) {
// eslint-disable-next-line no-console
console.log(signal.created);
}
if (signal.updated) {
// eslint-disable-next-line no-console
console.log(signal.updated);
}
const read = entry?.read;
if (read && read.length > 0) {
for (var stack of read) {
// eslint-disable-next-line no-console
console.log(stack);
}
}
// eslint-disable-next-line no-console
console.groupEnd();
}
/**
* @template T
* @param {() => string} label
* @param {() => T} fn
*/
export function trace(label, fn) {
var previously_tracing_expressions = tracing_expressions;
try {
tracing_expressions = { entries: new Map(), reaction: active_reaction };
var start = performance.now();
var value = fn();
var time = (performance.now() - start).toFixed(2);
if (!effect_tracking()) {
// eslint-disable-next-line no-console
console.log(`${label()} %cran outside of an effect (${time}ms)`, 'color: grey');
} else if (tracing_expressions.entries.size === 0) {
// eslint-disable-next-line no-console
console.log(`${label()} %cno reactive dependencies (${time}ms)`, 'color: grey');
} else {
// eslint-disable-next-line no-console
console.group(`${label()} %c(${time}ms)`, 'color: grey');
var entries = tracing_expressions.entries;
tracing_expressions = null;
for (const [signal, entry] of entries) {
log_entry(signal, entry);
}
// eslint-disable-next-line no-console
console.groupEnd();
}
if (previously_tracing_expressions !== null) {
for (const [signal, entry] of tracing_expressions.entries) {
var prev_entry = previously_tracing_expressions.get(signal);
if (prev_entry === undefined) {
previously_tracing_expressions.set(signal, entry);
} else {
prev_entry.read.push(...entry.read);
}
}
}
return value;
} finally {
tracing_expressions = previously_tracing_expressions;
}
}
/**
* @param {string} label
*/
export function get_stack(label) {
let error = Error();
const stack = error.stack;
if (stack) {
const lines = stack.split('\n');
const new_lines = ['\n'];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line === 'Error') {
continue;
}
if (line.includes('validate_each_keys')) {
return null;
}
if (line.includes('svelte/src/internal')) {
continue;
}
new_lines.push(line);
}
if (new_lines.length === 1) {
return null;
}
define_property(error, 'stack', {
value: new_lines.join('\n')
});
define_property(error, 'name', {
// 'Error' suffix is required for stack traces to be rendered properly
value: `${label}Error`
});
}
return error;
}

@ -36,6 +36,7 @@ import { array_from, is_array } from '../../../shared/utils.js';
import { INERT } from '../../constants.js'; import { INERT } from '../../constants.js';
import { queue_micro_task } from '../task.js'; import { queue_micro_task } from '../task.js';
import { active_effect, active_reaction } from '../../runtime.js'; import { active_effect, active_reaction } from '../../runtime.js';
import { DEV } from 'esm-env';
/** /**
* The row of a keyed each block that is currently updating. We track this * The row of a keyed each block that is currently updating. We track this
@ -191,7 +192,18 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
var value = array[i]; var value = array[i];
var key = get_key(value, i); var key = get_key(value, i);
item = create_item(hydrate_node, state, prev, null, value, key, i, render_fn, flags); item = create_item(
hydrate_node,
state,
prev,
null,
value,
key,
i,
render_fn,
flags,
get_collection
);
state.items.set(key, item); state.items.set(key, item);
prev = item; prev = item;
@ -205,7 +217,16 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
if (!hydrating) { if (!hydrating) {
var effect = /** @type {Effect} */ (active_reaction); var effect = /** @type {Effect} */ (active_reaction);
reconcile(array, state, anchor, render_fn, flags, (effect.f & INERT) !== 0, get_key); reconcile(
array,
state,
anchor,
render_fn,
flags,
(effect.f & INERT) !== 0,
get_key,
get_collection
);
} }
if (fallback_fn !== null) { if (fallback_fn !== null) {
@ -251,9 +272,10 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
* @param {number} flags * @param {number} flags
* @param {boolean} is_inert * @param {boolean} is_inert
* @param {(value: V, index: number) => any} get_key * @param {(value: V, index: number) => any} get_key
* @param {() => V[]} get_collection
* @returns {void} * @returns {void}
*/ */
function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key) { function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, get_collection) {
var is_animated = (flags & EACH_IS_ANIMATED) !== 0; var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0; var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
@ -319,7 +341,8 @@ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key) {
key, key,
i, i,
render_fn, render_fn,
flags flags,
get_collection
); );
items.set(key, prev); items.set(key, prev);
@ -486,9 +509,21 @@ function update_item(item, value, index, type) {
* @param {number} index * @param {number} index
* @param {(anchor: Node, item: V | Source<V>, index: number | Value<number>) => void} render_fn * @param {(anchor: Node, item: V | Source<V>, index: number | Value<number>) => void} render_fn
* @param {number} flags * @param {number} flags
* @param {() => V[]} get_collection
* @returns {EachItem} * @returns {EachItem}
*/ */
function create_item(anchor, state, prev, next, value, key, index, render_fn, flags) { function create_item(
anchor,
state,
prev,
next,
value,
key,
index,
render_fn,
flags,
get_collection
) {
var previous_each_item = current_each_item; var previous_each_item = current_each_item;
var reactive = (flags & EACH_ITEM_REACTIVE) !== 0; var reactive = (flags & EACH_ITEM_REACTIVE) !== 0;
var mutable = (flags & EACH_ITEM_IMMUTABLE) === 0; var mutable = (flags & EACH_ITEM_IMMUTABLE) === 0;
@ -496,6 +531,16 @@ function create_item(anchor, state, prev, next, value, key, index, render_fn, fl
var v = reactive ? (mutable ? mutable_source(value) : source(value)) : value; var v = reactive ? (mutable ? mutable_source(value) : source(value)) : value;
var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index); var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index);
if (DEV && reactive) {
// For tracing purposes, we need to link the source signal we create with the
// collection + index so that tracing works as intended
/** @type {Value} */ (v).debug = () => {
var collection_index = typeof i === 'number' ? index : i.v;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
get_collection()[collection_index];
};
}
/** @type {EachItem} */ /** @type {EachItem} */
var item = { var item = {
i, i,

@ -5,11 +5,10 @@ import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from '../hydr
import { create_fragment_from_html } from '../reconciler.js'; import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js'; import { assign_nodes } from '../template.js';
import * as w from '../../warnings.js'; import * as w from '../../warnings.js';
import { hash } from '../../../../utils.js'; import { hash, sanitize_location } from '../../../../utils.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { dev_current_component_function } from '../../runtime.js'; import { dev_current_component_function } from '../../runtime.js';
import { get_first_child, get_next_sibling } from '../operations.js'; import { get_first_child, get_next_sibling } from '../operations.js';
import { sanitize_location } from '../../dev/location.js';
/** /**
* @param {Element} element * @param {Element} element

@ -21,6 +21,7 @@ import { component_context, active_effect } from '../../runtime.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { EFFECT_TRANSPARENT } from '../../constants.js'; import { EFFECT_TRANSPARENT } from '../../constants.js';
import { assign_nodes } from '../template.js'; import { assign_nodes } from '../template.js';
import { is_raw_text_element } from '../../../../utils.js';
/** /**
* @param {Comment | Element} node * @param {Comment | Element} node
@ -116,6 +117,11 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
assign_nodes(element, element); assign_nodes(element, element);
if (render_fn) { if (render_fn) {
if (hydrating && is_raw_text_element(next_tag)) {
// prevent hydration glitches
element.append(document.createComment(''));
}
// If hydrating, use the existing ssr comment as the anchor so that the // If hydrating, use the existing ssr comment as the anchor so that the
// inner open and close methods can pick up the existing nodes correctly // inner open and close methods can pick up the existing nodes correctly
var child_anchor = /** @type {TemplateNode} */ ( var child_anchor = /** @type {TemplateNode} */ (

@ -358,16 +358,39 @@ export function set_attributes(
} else if (key === '__value' || (key === 'value' && value != null)) { } else if (key === '__value' || (key === 'value' && value != null)) {
// @ts-ignore // @ts-ignore
element.value = element[key] = element.__value = value; element.value = element[key] = element.__value = value;
} else if (key === 'selected' && is_option_element) {
set_selected(/** @type {HTMLOptionElement} */ (element), value);
} else { } else {
var name = key; var name = key;
if (!preserve_attribute_case) { if (!preserve_attribute_case) {
name = normalize_attribute(name); name = normalize_attribute(name);
} }
if (value == null && !is_custom_element) { var is_default = name === 'defaultValue' || name === 'defaultChecked';
if (value == null && !is_custom_element && !is_default) {
attributes[key] = null; attributes[key] = null;
if (name === 'value' || name === 'checked') {
// removing value/checked also removes defaultValue/defaultChecked — preserve
let input = /** @type {HTMLInputElement} */ (element);
if (name === 'value') {
let prev = input.defaultValue;
input.removeAttribute(name);
input.defaultValue = prev;
} else {
let prev = input.defaultChecked;
input.removeAttribute(name);
input.defaultChecked = prev;
}
} else {
element.removeAttribute(key); element.removeAttribute(key);
} else if (setters.includes(name) && (is_custom_element || typeof value !== 'string')) { }
} else if (
is_default ||
(setters.includes(name) && (is_custom_element || typeof value !== 'string'))
) {
// @ts-ignore // @ts-ignore
element[name] = value; element[name] = value;
} else if (typeof value !== 'function') { } else if (typeof value !== 'function') {

@ -12,6 +12,7 @@ export {
skip_ownership_validation skip_ownership_validation
} from './dev/ownership.js'; } from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js'; export { check_target, legacy_api } from './dev/legacy.js';
export { trace } from './dev/tracing.js';
export { inspect } from './dev/inspect.js'; export { inspect } from './dev/inspect.js';
export { await_block as await } from './dom/blocks/await.js'; export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js'; export { if_block as if } from './dom/blocks/if.js';

@ -13,6 +13,7 @@ import { source, set } from './reactivity/sources.js';
import { STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js'; import { STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js';
import { UNINITIALIZED } from '../../constants.js'; import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js'; import * as e from './errors.js';
import { get_stack } from './dev/tracing.js';
/** /**
* @template T * @template T
@ -22,6 +23,11 @@ import * as e from './errors.js';
* @returns {T} * @returns {T}
*/ */
export function proxy(value, parent = null, prev) { export function proxy(value, parent = null, prev) {
/** @type {Error | null} */
var stack = null;
if (DEV) {
stack = get_stack('CreatedAt');
}
// if non-proxyable, or is already a proxy, return `value` // if non-proxyable, or is already a proxy, return `value`
if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) { if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) {
return value; return value;
@ -41,7 +47,7 @@ export function proxy(value, parent = null, prev) {
if (is_proxied_array) { if (is_proxied_array) {
// We need to create the length source eagerly to ensure that // We need to create the length source eagerly to ensure that
// mutations to the array are properly synced with our proxy // mutations to the array are properly synced with our proxy
sources.set('length', source(/** @type {any[]} */ (value).length)); sources.set('length', source(/** @type {any[]} */ (value).length, stack));
} }
/** @type {ProxyMetadata} */ /** @type {ProxyMetadata} */
@ -87,7 +93,7 @@ export function proxy(value, parent = null, prev) {
var s = sources.get(prop); var s = sources.get(prop);
if (s === undefined) { if (s === undefined) {
s = source(descriptor.value); s = source(descriptor.value, stack);
sources.set(prop, s); sources.set(prop, s);
} else { } else {
set(s, proxy(descriptor.value, metadata)); set(s, proxy(descriptor.value, metadata));
@ -101,7 +107,7 @@ export function proxy(value, parent = null, prev) {
if (s === undefined) { if (s === undefined) {
if (prop in target) { if (prop in target) {
sources.set(prop, source(UNINITIALIZED)); sources.set(prop, source(UNINITIALIZED, stack));
} }
} else { } else {
// When working with arrays, we need to also ensure we update the length when removing // When working with arrays, we need to also ensure we update the length when removing
@ -135,7 +141,7 @@ export function proxy(value, parent = null, prev) {
// create a source, but only if it's an own property and not a prototype property // create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) { if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
s = source(proxy(exists ? target[prop] : UNINITIALIZED, metadata)); s = source(proxy(exists ? target[prop] : UNINITIALIZED, metadata), stack);
sources.set(prop, s); sources.set(prop, s);
} }
@ -203,7 +209,7 @@ export function proxy(value, parent = null, prev) {
(active_effect !== null && (!has || get_descriptor(target, prop)?.writable)) (active_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) { ) {
if (s === undefined) { if (s === undefined) {
s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED); s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED, stack);
sources.set(prop, s); sources.set(prop, s);
} }
@ -230,7 +236,7 @@ export function proxy(value, parent = null, prev) {
// If the item exists in the original, we need to create a uninitialized source, // If the item exists in the original, we need to create a uninitialized source,
// else a later read of the property would result in a source being created with // else a later read of the property would result in a source being created with
// the value of the original item at that index. // the value of the original item at that index.
other_s = source(UNINITIALIZED); other_s = source(UNINITIALIZED, stack);
sources.set(i + '', other_s); sources.set(i + '', other_s);
} }
} }
@ -242,7 +248,7 @@ export function proxy(value, parent = null, prev) {
// object property before writing to that property. // object property before writing to that property.
if (s === undefined) { if (s === undefined) {
if (!has || get_descriptor(target, prop)?.writable) { if (!has || get_descriptor(target, prop)?.writable) {
s = source(undefined); s = source(undefined, stack);
set(s, proxy(value, metadata)); set(s, proxy(value, metadata));
sources.set(prop, s); sources.set(prop, s);
} }

@ -24,6 +24,7 @@ import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js'; import * as e from '../errors.js';
import { destroy_effect } from './effects.js'; import { destroy_effect } from './effects.js';
import { inspect_effects, set_inspect_effects } from './sources.js'; import { inspect_effects, set_inspect_effects } from './sources.js';
import { get_stack } from '../dev/tracing.js';
/** /**
* @template V * @template V
@ -61,6 +62,10 @@ export function derived(fn) {
parent: parent_derived ?? active_effect parent: parent_derived ?? active_effect
}; };
if (DEV) {
signal.created = get_stack('CreatedAt');
}
if (parent_derived !== null) { if (parent_derived !== null) {
(parent_derived.children ??= []).push(signal); (parent_derived.children ??= []).push(signal);
} }

@ -1,4 +1,4 @@
/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, Reaction, TemplateNode, TransitionManager } from '#client' */ /** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */
import { import {
check_dirtiness, check_dirtiness,
component_context, component_context,
@ -16,8 +16,7 @@ import {
set_is_flushing_effect, set_is_flushing_effect,
set_signal_status, set_signal_status,
untrack, untrack,
skip_reaction, skip_reaction
capture_signals
} from '../runtime.js'; } from '../runtime.js';
import { import {
DIRTY, DIRTY,
@ -40,13 +39,10 @@ import {
} from '../constants.js'; } from '../constants.js';
import { set } from './sources.js'; import { set } from './sources.js';
import * as e from '../errors.js'; import * as e from '../errors.js';
import * as w from '../warnings.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js'; import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js'; import { get_next_sibling } from '../dom/operations.js';
import { destroy_derived } from './deriveds.js'; import { destroy_derived } from './deriveds.js';
import { FILENAME } from '../../../constants.js';
import { get_location } from '../dev/location.js';
/** /**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune * @param {'$effect' | '$effect.pre' | '$inspect'} rune
@ -248,11 +244,35 @@ export function inspect_effect(fn) {
*/ */
export function effect_root(fn) { export function effect_root(fn) {
const effect = create_effect(ROOT_EFFECT, fn, true); const effect = create_effect(ROOT_EFFECT, fn, true);
return () => { return () => {
destroy_effect(effect); destroy_effect(effect);
}; };
} }
/**
* An effect root whose children can transition out
* @param {() => void} fn
* @returns {(options?: { outro?: boolean }) => Promise<void>}
*/
export function component_root(fn) {
const effect = create_effect(ROOT_EFFECT, fn, true);
return (options = {}) => {
return new Promise((fulfil) => {
if (options.outro) {
pause_effect(effect, () => {
destroy_effect(effect);
fulfil(undefined);
});
} else {
destroy_effect(effect);
fulfil(undefined);
}
});
};
}
/** /**
* @param {() => void | (() => void)} fn * @param {() => void | (() => void)} fn
* @returns {Effect} * @returns {Effect}

@ -33,6 +33,7 @@ import {
} from '../constants.js'; } from '../constants.js';
import * as e from '../errors.js'; import * as e from '../errors.js';
import { legacy_mode_flag } from '../../flags/index.js'; import { legacy_mode_flag } from '../../flags/index.js';
import { get_stack } from '../dev/tracing.js';
export let inspect_effects = new Set(); export let inspect_effects = new Set();
@ -46,16 +47,25 @@ export function set_inspect_effects(v) {
/** /**
* @template V * @template V
* @param {V} v * @param {V} v
* @param {Error | null} [stack]
* @returns {Source<V>} * @returns {Source<V>}
*/ */
export function source(v) { export function source(v, stack) {
return { /** @type {Value} */
var signal = {
f: 0, // TODO ideally we could skip this altogether, but it causes type errors f: 0, // TODO ideally we could skip this altogether, but it causes type errors
v, v,
reactions: null, reactions: null,
equals, equals,
version: 0 version: 0
}; };
if (DEV) {
signal.created = stack ?? get_stack('CreatedAt');
signal.debug = null;
}
return signal;
} }
/** /**
@ -160,6 +170,10 @@ export function internal_set(source, value) {
source.v = value; source.v = value;
source.version = increment_version(); source.version = increment_version();
if (DEV) {
source.updated = get_stack('UpdatedAt');
}
mark_reactions(source, DIRTY); mark_reactions(source, DIRTY);
// If the current signal is running for the first time, it won't have any // If the current signal is running for the first time, it won't have any

@ -14,6 +14,10 @@ export interface Value<V = unknown> extends Signal {
equals: Equals; equals: Equals;
/** The latest value for this signal */ /** The latest value for this signal */
v: V; v: V;
/** Dev only */
created?: Error | null;
updated?: Error | null;
debug?: null | (() => void);
} }
export interface Reaction extends Signal { export interface Reaction extends Signal {

@ -10,7 +10,7 @@ import {
} from './dom/operations.js'; } from './dom/operations.js';
import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js'; import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js';
import { push, pop, component_context, active_effect } from './runtime.js'; import { push, pop, component_context, active_effect } from './runtime.js';
import { effect_root, branch } from './reactivity/effects.js'; import { component_root, branch } from './reactivity/effects.js';
import { import {
hydrate_next, hydrate_next,
hydrate_node, hydrate_node,
@ -204,7 +204,7 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
// @ts-expect-error will be defined because the render effect runs synchronously // @ts-expect-error will be defined because the render effect runs synchronously
var component = undefined; var component = undefined;
var unmount = effect_root(() => { var unmount = component_root(() => {
var anchor_node = anchor ?? target.appendChild(create_text()); var anchor_node = anchor ?? target.appendChild(create_text());
branch(() => { branch(() => {
@ -252,7 +252,7 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
} }
root_event_handles.delete(event_handle); root_event_handles.delete(event_handle);
mounted_components.delete(component);
if (anchor_node !== anchor) { if (anchor_node !== anchor) {
anchor_node.parentNode?.removeChild(anchor_node); anchor_node.parentNode?.removeChild(anchor_node);
} }
@ -271,14 +271,35 @@ let mounted_components = new WeakMap();
/** /**
* Unmounts a component that was previously mounted using `mount` or `hydrate`. * Unmounts a component that was previously mounted using `mount` or `hydrate`.
*
* Since 5.13.0, if `options.outro` is `true`, [transitions](https://svelte.dev/docs/svelte/transition) will play before the component is removed from the DOM.
*
* Returns a `Promise` that resolves after transitions have completed if `options.outro` is true, or immediately otherwise (prior to 5.13.0, returns `void`).
*
* ```js
* import { mount, unmount } from 'svelte';
* import App from './App.svelte';
*
* const app = mount(App, { target: document.body });
*
* // later...
* unmount(app, { outro: true });
* ```
* @param {Record<string, any>} component * @param {Record<string, any>} component
* @param {{ outro?: boolean }} [options]
* @returns {Promise<void>}
*/ */
export function unmount(component) { export function unmount(component, options) {
const fn = mounted_components.get(component); const fn = mounted_components.get(component);
if (fn) { if (fn) {
fn(); mounted_components.delete(component);
} else if (DEV) { return fn(options);
}
if (DEV) {
w.lifecycle_double_unmount(); w.lifecycle_double_unmount();
} }
return Promise.resolve();
} }

@ -35,6 +35,7 @@ import * as e from './errors.js';
import { lifecycle_outside_component } from '../shared/errors.js'; import { lifecycle_outside_component } from '../shared/errors.js';
import { FILENAME } from '../../constants.js'; import { FILENAME } from '../../constants.js';
import { legacy_mode_flag } from '../flags/index.js'; import { legacy_mode_flag } from '../flags/index.js';
import { tracing_expressions, get_stack } from './dev/tracing.js';
const FLUSH_MICROTASK = 0; const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1; const FLUSH_SYNC = 1;
@ -136,6 +137,11 @@ export let skip_reaction = false;
/** @type {Set<Value> | null} */ /** @type {Set<Value> | null} */
export let captured_signals = null; export let captured_signals = null;
/** @param {Set<Value> | null} value */
export function set_captured_signals(value) {
captured_signals = value;
}
// Handling runtime component context // Handling runtime component context
/** @type {ComponentContext | null} */ /** @type {ComponentContext | null} */
export let component_context = null; export let component_context = null;
@ -356,7 +362,7 @@ export function handle_error(error, effect, previous_effect, component_context)
new_lines.push(line); new_lines.push(line);
} }
define_property(error, 'stack', { define_property(error, 'stack', {
value: error.stack + new_lines.join('\n') value: new_lines.join('\n')
}); });
} }
@ -908,6 +914,27 @@ export function get(signal) {
} }
} }
if (
DEV &&
tracing_expressions !== null &&
active_reaction !== null &&
tracing_expressions.reaction === active_reaction
) {
// Used when mapping state between special blocks like `each`
if (signal.debug) {
signal.debug();
} else if (signal.created) {
var entry = tracing_expressions.entries.get(signal);
if (entry === undefined) {
entry = { read: [] };
tracing_expressions.entries.set(signal, entry);
}
entry.read.push(get_stack('TracedAt'));
}
}
return signal.v; return signal.v;
} }

@ -16,7 +16,7 @@ import { DEV } from 'esm-env';
import { current_component, pop, push } from './context.js'; import { current_component, pop, push } from './context.js';
import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
import { validate_store } from '../shared/validate.js'; import { validate_store } from '../shared/validate.js';
import { is_boolean_attribute, is_void } from '../../utils.js'; import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js';
import { reset_elements } from './dev.js'; import { reset_elements } from './dev.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
@ -24,9 +24,6 @@ import { reset_elements } from './dev.js';
const INVALID_ATTR_NAME_CHAR_REGEX = const INVALID_ATTR_NAME_CHAR_REGEX =
/[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u; /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u;
/** List of elements that require raw contents and should not have SSR comments put in them */
const RAW_TEXT_ELEMENTS = ['textarea', 'script', 'style', 'title'];
/** /**
* @param {Payload} to_copy * @param {Payload} to_copy
* @returns {Payload} * @returns {Payload}
@ -65,13 +62,13 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop)
payload.out += '<!---->'; payload.out += '<!---->';
if (tag) { if (tag) {
payload.out += `<${tag} `; payload.out += `<${tag}`;
attributes_fn(); attributes_fn();
payload.out += `>`; payload.out += `>`;
if (!is_void(tag)) { if (!is_void(tag)) {
children_fn(); children_fn();
if (!RAW_TEXT_ELEMENTS.includes(tag)) { if (!is_raw_text_element(tag)) {
payload.out += EMPTY_COMMENT; payload.out += EMPTY_COMMENT;
} }
payload.out += `</${tag}>`; payload.out += `</${tag}>`;

@ -1,6 +1,8 @@
import { on } from '../events/index.js'; import { on } from '../events/index.js';
import { ReactiveValue } from './reactive-value.js'; import { ReactiveValue } from './reactive-value.js';
const parenthesis_regex = /\(.+\)/;
/** /**
* Creates a media query and provides a `current` property that reflects whether or not it matches. * Creates a media query and provides a `current` property that reflects whether or not it matches.
* *
@ -25,7 +27,8 @@ export class MediaQuery extends ReactiveValue {
* @param {boolean} [fallback] Fallback value for the server * @param {boolean} [fallback] Fallback value for the server
*/ */
constructor(query, fallback) { constructor(query, fallback) {
const q = window.matchMedia(`(${query})`); let final_query = parenthesis_regex.test(query) ? query : `(${query})`;
const q = window.matchMedia(final_query);
super( super(
() => q.matches, () => q.matches,
(update) => on(q, 'change', update) (update) => on(q, 'change', update)

@ -431,6 +431,7 @@ const RUNES = /** @type {const} */ ([
'$effect.root', '$effect.root',
'$inspect', '$inspect',
'$inspect().with', '$inspect().with',
'$inspect.trace',
'$host' '$host'
]); ]);
@ -441,3 +442,19 @@ const RUNES = /** @type {const} */ ([
export function is_rune(name) { export function is_rune(name) {
return RUNES.includes(/** @type {RUNES[number]} */ (name)); return RUNES.includes(/** @type {RUNES[number]} */ (name));
} }
/** List of elements that require raw contents and should not have SSR comments put in them */
const RAW_TEXT_ELEMENTS = /** @type {const} */ (['textarea', 'script', 'style', 'title']);
/** @param {string} name */
export function is_raw_text_element(name) {
return RAW_TEXT_ELEMENTS.includes(/** @type {RAW_TEXT_ELEMENTS[number]} */ (name));
}
/**
* Prevent devtools trying to make `location` a clickable link by inserting a zero-width space
* @param {string | undefined} location
*/
export function sanitize_location(location) {
return location?.replace(/\//g, '/\u200b');
}

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version * https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string} * @type {string}
*/ */
export const VERSION = '5.11.2'; export const VERSION = '5.14.0';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -3,6 +3,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import glob from 'tiny-glob/sync.js'; import glob from 'tiny-glob/sync.js';
import { VERSION, compile, compileModule, preprocess } from 'svelte/compiler'; import { VERSION, compile, compileModule, preprocess } from 'svelte/compiler';
import { vi } from 'vitest';
/** /**
* @param {string} file * @param {string} file
@ -176,12 +177,12 @@ export function write(file, contents) {
// Guard because not all test contexts load this with JSDOM // Guard because not all test contexts load this with JSDOM
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
// @ts-expect-error JS DOM doesn't support it // @ts-expect-error JS DOM doesn't support it
Window.prototype.matchMedia = (media) => { Window.prototype.matchMedia = vi.fn((media) => {
return { return {
matches: false, matches: false,
media, media,
addEventListener: () => {}, addEventListener: () => {},
removeEventListener: () => {} removeEventListener: () => {}
}; };
}; });
} }

@ -0,0 +1,11 @@
import { test } from '../../test';
export default test({
snapshot(target) {
const script = target.querySelector('script');
return {
script
};
}
});

@ -0,0 +1 @@
<!--[--><!----><script>{}<!----></script><!----><!--]-->

@ -0,0 +1 @@
<svelte:element this={"script"}>{"{}"}</svelte:element>

@ -1 +1,3 @@
<div class=foo></div> <div class=foo></div>
<a href=/>home</a>
<a href=/foo>home</a>

@ -2,7 +2,7 @@
"html": { "html": {
"type": "Fragment", "type": "Fragment",
"start": 0, "start": 0,
"end": 21, "end": 62,
"children": [ "children": [
{ {
"type": "Element", "type": "Element",
@ -27,6 +27,84 @@
} }
], ],
"children": [] "children": []
},
{
"type": "Text",
"start": 21,
"end": 22,
"raw": "\n",
"data": "\n"
},
{
"type": "Element",
"start": 22,
"end": 40,
"name": "a",
"attributes": [
{
"type": "Attribute",
"start": 25,
"end": 31,
"name": "href",
"value": [
{
"start": 30,
"end": 31,
"type": "Text",
"raw": "/",
"data": "/"
}
]
}
],
"children": [
{
"type": "Text",
"start": 32,
"end": 36,
"raw": "home",
"data": "home"
}
]
},
{
"type": "Text",
"start": 40,
"end": 41,
"raw": "\n",
"data": "\n"
},
{
"type": "Element",
"start": 41,
"end": 62,
"name": "a",
"attributes": [
{
"type": "Attribute",
"start": 44,
"end": 53,
"name": "href",
"value": [
{
"start": 49,
"end": 53,
"type": "Text",
"raw": "/foo",
"data": "/foo"
}
]
}
],
"children": [
{
"type": "Text",
"start": 54,
"end": 58,
"raw": "home",
"data": "home"
}
]
} }
] ]
} }

@ -0,0 +1,13 @@
{#if }
{:else if }
{:else }
{/if}
{#each }
{/each}
{#snippet }
{/snippet}
{#snippet foo}
{/snippet}

@ -0,0 +1,107 @@
{
"html": {
"type": "Fragment",
"start": 0,
"end": 102,
"children": [
{
"type": "IfBlock",
"start": 0,
"end": 33,
"expression": {
"type": "Identifier",
"start": 5,
"end": 5,
"name": ""
},
"children": [],
"else": {
"type": "ElseBlock",
"start": 18,
"end": 28,
"children": [
{
"type": "IfBlock",
"start": 18,
"end": 33,
"expression": {
"type": "Identifier",
"start": 17,
"end": 17,
"name": ""
},
"children": [],
"else": {
"type": "ElseBlock",
"start": 27,
"end": 28,
"children": []
},
"elseif": true
}
]
}
},
{
"type": "Text",
"start": 33,
"end": 35,
"raw": "\n\n",
"data": "\n\n"
},
{
"type": "EachBlock",
"start": 35,
"end": 51,
"children": [],
"context": null,
"expression": {
"type": "Identifier",
"start": 42,
"end": 42,
"name": ""
}
},
{
"type": "Text",
"start": 51,
"end": 53,
"raw": "\n\n",
"data": "\n\n"
},
{
"type": "SnippetBlock",
"start": 53,
"end": 75,
"expression": {
"type": "Identifier",
"start": 63,
"end": 63,
"name": ""
},
"parameters": [],
"children": []
},
{
"type": "Text",
"start": 75,
"end": 77,
"raw": "\n\n",
"data": "\n\n"
},
{
"type": "SnippetBlock",
"start": 77,
"end": 102,
"expression": {
"type": "Identifier",
"start": 87,
"end": 90,
"name": "foo"
},
"parameters": [],
"children": []
}
]
}
}

@ -0,0 +1,11 @@
<div {}></div>
<div foo={}></div>
<div foo={a.}></div>
<div foo={'hi}'.}></div>
<Component onclick={() => x.} />
<input bind:value={a.} />
asd{a.}asd
{foo[bar.]}

@ -0,0 +1,242 @@
{
"html": {
"type": "Fragment",
"start": 0,
"end": 164,
"children": [
{
"type": "Element",
"start": 0,
"end": 14,
"name": "div",
"attributes": [
{
"type": "Attribute",
"start": 5,
"end": 7,
"name": "",
"value": [
{
"type": "AttributeShorthand",
"start": 6,
"end": 6,
"expression": {
"start": 6,
"end": 6,
"type": "Identifier",
"name": ""
}
}
]
}
],
"children": []
},
{
"type": "Text",
"start": 14,
"end": 15,
"raw": "\n",
"data": "\n"
},
{
"type": "Element",
"start": 15,
"end": 33,
"name": "div",
"attributes": [
{
"type": "Attribute",
"start": 20,
"end": 26,
"name": "foo",
"value": [
{
"type": "MustacheTag",
"start": 24,
"end": 26,
"expression": {
"type": "Identifier",
"start": 25,
"end": 25,
"name": ""
}
}
]
}
],
"children": []
},
{
"type": "Text",
"start": 33,
"end": 35,
"raw": "\n\n",
"data": "\n\n"
},
{
"type": "Element",
"start": 35,
"end": 55,
"name": "div",
"attributes": [
{
"type": "Attribute",
"start": 40,
"end": 48,
"name": "foo",
"value": [
{
"type": "MustacheTag",
"start": 44,
"end": 48,
"expression": {
"type": "Identifier",
"start": 45,
"end": 47,
"name": ""
}
}
]
}
],
"children": []
},
{
"type": "Text",
"start": 55,
"end": 56,
"raw": "\n",
"data": "\n"
},
{
"type": "Element",
"start": 56,
"end": 80,
"name": "div",
"attributes": [
{
"type": "Attribute",
"start": 61,
"end": 73,
"name": "foo",
"value": [
{
"type": "MustacheTag",
"start": 65,
"end": 73,
"expression": {
"type": "Identifier",
"start": 66,
"end": 72,
"name": ""
}
}
]
}
],
"children": []
},
{
"type": "Text",
"start": 80,
"end": 81,
"raw": "\n",
"data": "\n"
},
{
"type": "InlineComponent",
"start": 81,
"end": 113,
"name": "Component",
"attributes": [
{
"type": "Attribute",
"start": 92,
"end": 110,
"name": "onclick",
"value": [
{
"type": "MustacheTag",
"start": 100,
"end": 110,
"expression": {
"type": "Identifier",
"start": 101,
"end": 109,
"name": ""
}
}
]
}
],
"children": []
},
{
"type": "Text",
"start": 113,
"end": 115,
"raw": "\n\n",
"data": "\n\n"
},
{
"type": "Element",
"start": 115,
"end": 140,
"name": "input",
"attributes": [
{
"start": 122,
"end": 137,
"type": "Binding",
"name": "value",
"expression": {
"type": "Identifier",
"start": 134,
"end": 136,
"name": ""
},
"modifiers": []
}
],
"children": []
},
{
"type": "Text",
"start": 140,
"end": 145,
"raw": "\n\nasd",
"data": "\n\nasd"
},
{
"type": "MustacheTag",
"start": 145,
"end": 149,
"expression": {
"type": "Identifier",
"start": 146,
"end": 148,
"name": ""
}
},
{
"type": "Text",
"start": 149,
"end": 153,
"raw": "asd\n",
"data": "asd\n"
},
{
"type": "MustacheTag",
"start": 153,
"end": 164,
"expression": {
"type": "Identifier",
"start": 154,
"end": 163,
"name": ""
}
}
]
}
}

@ -0,0 +1,20 @@
<div>
{#if foo}
</div>
<Comp>
{#key bar}
</Comp>
<div>
{#if foo}
{#if bar}
{/if}
</div>
{#if foo}
{#key bar}
{/if}
{#each x as y}
<p>hi</p>

@ -0,0 +1,276 @@
{
"html": {
"type": "Fragment",
"start": 0,
"end": 150,
"children": [
{
"type": "Element",
"start": 0,
"end": 23,
"name": "div",
"attributes": [],
"children": [
{
"type": "Text",
"start": 5,
"end": 7,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "IfBlock",
"start": 7,
"end": 17,
"expression": {
"type": "Identifier",
"start": 12,
"end": 15,
"loc": {
"start": {
"line": 2,
"column": 6
},
"end": {
"line": 2,
"column": 9
}
},
"name": "foo"
},
"children": []
}
]
},
{
"type": "Text",
"start": 23,
"end": 25,
"raw": "\n\n",
"data": "\n\n"
},
{
"type": "InlineComponent",
"start": 25,
"end": 51,
"name": "Comp",
"attributes": [],
"children": [
{
"type": "Text",
"start": 31,
"end": 33,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "KeyBlock",
"start": 33,
"end": 44,
"expression": {
"type": "Identifier",
"start": 39,
"end": 42,
"loc": {
"start": {
"line": 6,
"column": 7
},
"end": {
"line": 6,
"column": 10
}
},
"name": "bar"
},
"children": []
}
]
},
{
"type": "Text",
"start": 51,
"end": 53,
"raw": "\n\n",
"data": "\n\n"
},
{
"type": "Element",
"start": 53,
"end": 95,
"name": "div",
"attributes": [],
"children": [
{
"type": "Text",
"start": 58,
"end": 60,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "IfBlock",
"start": 60,
"end": 89,
"expression": {
"type": "Identifier",
"start": 65,
"end": 68,
"loc": {
"start": {
"line": 10,
"column": 6
},
"end": {
"line": 10,
"column": 9
}
},
"name": "foo"
},
"children": [
{
"type": "IfBlock",
"start": 72,
"end": 88,
"expression": {
"type": "Identifier",
"start": 77,
"end": 80,
"loc": {
"start": {
"line": 11,
"column": 7
},
"end": {
"line": 11,
"column": 10
}
},
"name": "bar"
},
"children": []
}
]
}
]
},
{
"type": "Text",
"start": 95,
"end": 97,
"raw": "\n\n",
"data": "\n\n"
},
{
"type": "IfBlock",
"start": 97,
"end": 124,
"expression": {
"type": "Identifier",
"start": 102,
"end": 105,
"loc": {
"start": {
"line": 15,
"column": 5
},
"end": {
"line": 15,
"column": 8
}
},
"name": "foo"
},
"children": [
{
"type": "KeyBlock",
"start": 108,
"end": 119,
"expression": {
"type": "Identifier",
"start": 114,
"end": 117,
"loc": {
"start": {
"line": 16,
"column": 7
},
"end": {
"line": 16,
"column": 10
}
},
"name": "bar"
},
"children": []
}
]
},
{
"type": "Text",
"start": 124,
"end": 126,
"raw": "\n\n",
"data": "\n\n"
},
{
"type": "EachBlock",
"start": 126,
"end": 150,
"children": [
{
"type": "Element",
"start": 141,
"end": 150,
"name": "p",
"attributes": [],
"children": [
{
"type": "Text",
"start": 144,
"end": 146,
"raw": "hi",
"data": "hi"
}
]
}
],
"context": {
"type": "Identifier",
"name": "y",
"start": 138,
"loc": {
"start": {
"line": 19,
"column": 12,
"character": 138
},
"end": {
"line": 19,
"column": 13,
"character": 139
}
},
"end": 139
},
"expression": {
"type": "Identifier",
"start": 133,
"end": 134,
"loc": {
"start": {
"line": 19,
"column": 7
},
"end": {
"line": 19,
"column": 8
}
},
"name": "x"
}
}
]
}
}

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

Loading…
Cancel
Save