Merge branch 'main' into shadow-root-options

pull/17088/head
Bladesheng 3 weeks ago committed by GitHub
commit a8774c70d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: change title only after any pending work has completed

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: preserve symbols when creating derived rest properties

@ -25,6 +25,10 @@ packages/svelte/tests/**/_output
packages/svelte/tests/**/shards/*.test.js
packages/svelte/tests/hydration/samples/*/_expected.html
packages/svelte/tests/hydration/samples/*/_override.html
packages/svelte/tests/parser-legacy/samples/*/_actual.json
packages/svelte/tests/parser-legacy/samples/*/output.json
packages/svelte/tests/parser-modern/samples/*/_actual.json
packages/svelte/tests/parser-modern/samples/*/output.json
packages/svelte/types
packages/svelte/compiler/index.js
playgrounds/sandbox/src/*

@ -23,9 +23,10 @@ There are also [plugins for other bundlers](/packages#bundler-plugins), but we r
## Editor tooling
The Svelte team maintains a [VS Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), and there are integrations with various other [editors](https://sveltesociety.dev/resources#editor-support) and tools as well.
The Svelte team maintains a [VS Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), and there are integrations with various other [editors](https://sveltesociety.dev/collection/editor-support-c85c080efc292a34) and tools as well.
You can also check your code from the command line using [`sv check`](https://github.com/sveltejs/cli).
You can also check your code from the command line using [sv check](https://github.com/sveltejs/cli).
## Getting help

@ -4,7 +4,7 @@ title: $bindable
Ordinarily, props go one way, from parent to child. This makes it easy to understand how data flows around your app.
In Svelte, component props can be _bound_, which means that data can also flow _up_ from child to parent. This isn't something you should do often, but it can simplify your code if used sparingly and carefully.
In Svelte, component props can be _bound_, which means that data can also flow _up_ from child to parent. This isn't something you should do often — overuse can make your data flow unpredictable and your components harder to maintain — but it can simplify your code if used sparingly and carefully.
It also means that a state proxy can be _mutated_ in the child.

@ -94,7 +94,7 @@ Svelte 4 contained hooks that ran before and after the component as a whole was
</script>
```
Instead of `beforeUpdate` use `$effect.pre` and instead of `afterUpdate` use `$effect` instead - these runes offer more granular control and only react to the changes you're actually interested in.
Instead of `beforeUpdate` use `$effect.pre` and instead of `afterUpdate` use `$effect` instead these runes offer more granular control and only react to the changes you're actually interested in.
### Chat window example

@ -0,0 +1,65 @@
---
title: Hydratable data
---
In Svelte, when you want to render asynchronous content data on the server, you can simply `await` it. This is great! However, it comes with a pitfall: when hydrating that content on the client, Svelte has to redo the asynchronous work, which blocks hydration for however long it takes:
```svelte
<script>
import { getUser } from 'my-database-library';
// This will get the user on the server, render the user's name into the h1,
// and then, during hydration on the client, it will get the user _again_,
// blocking hydration until it's done.
const user = await getUser();
</script>
<h1>{user.name}</h1>
```
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
To fix the example above:
```svelte
<script>
import { hydratable } from 'svelte';
import { getUser } from 'my-database-library';
// During server rendering, this will serialize and stash the result of `getUser`, associating
// it with the provided key and baking it into the `head` content. During hydration, it will
// look for the serialized version, returning it instead of running `getUser`. After hydration
// is done, if it's called again, it'll simply invoke `getUser`.
const user = await hydratable('user', () => getUser());
</script>
<h1>{user.name}</h1>
```
This API can also be used to provide access to random or time-based values that are stable between server rendering and hydration. For example, to get a random number that doesn't update on hydration:
```ts
import { hydratable } from 'svelte';
const rand = hydratable('random', () => Math.random());
```
If you're a library author, be sure to prefix the keys of your `hydratable` values with the name of your library so that your keys don't conflict with other libraries.
## Serialization
All data returned from a `hydratable` function must be serializable. But this doesn't mean you're limited to JSON — Svelte uses [`devalue`](https://npmjs.com/package/devalue), which can serialize all sorts of things including `Map`, `Set`, `URL`, and `BigInt`. Check the documentation page for a full list. In addition to these, thanks to some Svelte magic, you can also fearlessly use promises:
```svelte
<script>
import { hydratable } from 'svelte';
const promises = hydratable('random', () => {
return {
one: Promise.resolve(1),
two: Promise.resolve(2)
}
});
</script>
{await promises.one}
{await promises.two}
```

@ -294,7 +294,7 @@ E2E (short for 'end to end') tests allow you to test your full application throu
You can use the Svelte CLI to [setup Playwright](/docs/cli/playwright) either during project creation or later on. You can also [set it up with `npm init playwright`](https://playwright.dev/docs/intro). Additionally, you may also want to install an IDE plugin such as [the VS Code extension](https://playwright.dev/docs/getting-started-vscode) to be able to execute tests from inside your IDE.
If you've run `npm init playwright` or are not using Vite, you may need to adjust the Playwright config to tell Playwright what to do before running the tests - mainly starting your application at a certain port. For example:
If you've run `npm init playwright` or are not using Vite, you may need to adjust the Playwright config to tell Playwright what to do before running the tests mainly starting your application at a certain port. For example:
```js
/// file: playwright.config.js

@ -137,7 +137,7 @@ Transitions are now local by default to prevent confusion around page navigation
{/if}
```
To make transitions global, add the `|global` modifier - then they will play when _any_ control flow block above is created/destroyed. The migration script will do this automatically for you. ([#6686](https://github.com/sveltejs/svelte/issues/6686))
To make transitions global, add the `|global` modifier then they will play when _any_ control flow block above is created/destroyed. The migration script will do this automatically for you. ([#6686](https://github.com/sveltejs/svelte/issues/6686))
## Default slot bindings
@ -150,10 +150,10 @@ Default slot bindings are no longer exposed to named slots and vice versa:
<Nested let:count>
<p>
count in default slot - is available: {count}
count in default slot is available: {count}
</p>
<p slot="bar">
count in bar slot - is not available: {count}
count in bar slot is not available: {count}
</p>
</Nested>
```

@ -4,7 +4,7 @@ title: Svelte 5 migration guide
Version 5 comes with an overhauled syntax and reactivity system. While it may look different at first, you'll soon notice many similarities. This guide goes over the changes in detail and shows you how to upgrade. Along with it, we also provide information on _why_ we did these changes.
You don't have to migrate to the new syntax right away - Svelte 5 still supports the old Svelte 4 syntax, and you can mix and match components using the new syntax with components using the old and vice versa. We expect many people to be able to upgrade with only a few lines of code changed initially. There's also a [migration script](#Migration-script) that helps you with many of these steps automatically.
You don't have to migrate to the new syntax right away Svelte 5 still supports the old Svelte 4 syntax, and you can mix and match components using the new syntax with components using the old and vice versa. We expect many people to be able to upgrade with only a few lines of code changed initially. There's also a [migration script](#Migration-script) that helps you with many of these steps automatically.
## Reactivity syntax changes
@ -23,7 +23,7 @@ In Svelte 4, a `let` declaration at the top level of a component was implicitly
Nothing else changes. `count` is still the number itself, and you read and write directly to it, without a wrapper like `.value` or `getCount()`.
> [!DETAILS] Why we did this
> `let` being implicitly reactive at the top level worked great, but it meant that reactivity was constrained - a `let` declaration anywhere else was not reactive. This forced you to resort to using stores when refactoring code out of the top level of components for reuse. This meant you had to learn an entirely separate reactivity model, and the result often wasn't as nice to work with. Because reactivity is more explicit in Svelte 5, you can keep using the same API outside the top level of components. Head to [the tutorial](/tutorial) to learn more.
> `let` being implicitly reactive at the top level worked great, but it meant that reactivity was constrained a `let` declaration anywhere else was not reactive. This forced you to resort to using stores when refactoring code out of the top level of components for reuse. This meant you had to learn an entirely separate reactivity model, and the result often wasn't as nice to work with. Because reactivity is more explicit in Svelte 5, you can keep using the same API outside the top level of components. Head to [the tutorial](/tutorial) to learn more.
### $: → $derived/$effect
@ -120,7 +120,7 @@ In Svelte 5, the `$props` rune makes this straightforward without any additional
## Event changes
Event handlers have been given a facelift in Svelte 5. Whereas in Svelte 4 we use the `on:` directive to attach an event listener to an element, in Svelte 5 they are properties like any other (in other words - remove the colon):
Event handlers have been given a facelift in Svelte 5. Whereas in Svelte 4 we use the `on:` directive to attach an event listener to an element, in Svelte 5 they are properties like any other (in other words remove the colon):
```svelte
<script>
@ -154,7 +154,7 @@ Since they're just properties, you can use the normal shorthand syntax...
In Svelte 4, components could emit events by creating a dispatcher with `createEventDispatcher`.
This function is deprecated in Svelte 5. Instead, components should accept _callback props_ - which means you then pass functions as properties to these components:
This function is deprecated in Svelte 5. Instead, components should accept _callback props_ which means you then pass functions as properties to these components:
```svelte
<!--- file: App.svelte --->
@ -462,7 +462,7 @@ In Svelte 4, you would pass data to a `<slot />` and then retrieve it with `let:
## Migration script
By now you should have a pretty good understanding of the before/after and how the old syntax relates to the new syntax. It probably also became clear that a lot of these migrations are rather technical and repetitive - something you don't want to do by hand.
By now you should have a pretty good understanding of the before/after and how the old syntax relates to the new syntax. It probably also became clear that a lot of these migrations are rather technical and repetitive something you don't want to do by hand.
We thought the same, which is why we provide a migration script to do most of the migration automatically. You can upgrade your project by using `npx sv migrate svelte-5`. This will do the following things:

@ -18,7 +18,7 @@ There are online forums and chats which are a great place for discussion about b
## Are there any third-party resources?
Svelte Society maintains a [list of books and videos](https://sveltesociety.dev/resources).
Svelte Society maintains a [list of books and videos](https://sveltesociety.dev/collection/a-list-of-books-and-courses-ac01dd10363184fa).
## How can I get VS Code to syntax-highlight my .svelte files?
@ -69,7 +69,7 @@ There are several [UI component libraries](/packages#component-libraries) as wel
## How do I test Svelte apps?
How your application is structured and where logic is defined will determine the best way to ensure it is properly tested. It is important to note that not all logic belongs within a component - this includes concerns such as data transformation, cross-component state management, and logging, among others. Remember that the Svelte library has its own test suite, so you do not need to write tests to validate implementation details provided by Svelte.
How your application is structured and where logic is defined will determine the best way to ensure it is properly tested. It is important to note that not all logic belongs within a component this includes concerns such as data transformation, cross-component state management, and logging, among others. Remember that the Svelte library has its own test suite, so you do not need to write tests to validate implementation details provided by Svelte.
A Svelte application will typically have three different types of tests: Unit, Component, and End-to-End (E2E).

@ -130,12 +130,6 @@ $effect(() => {
Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency.
### experimental_async_fork
```
Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
```
### flush_sync_in_effect
```
@ -164,6 +158,25 @@ Cannot create a fork inside an effect or when state changes are pending
`getAbortSignal()` can only be called inside an effect or derived
```
### hydratable_missing_but_required
```
Expected to find a hydratable with key `%key%` during hydration, but did not.
```
This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
```svelte
<script>
import { hydratable } from 'svelte';
if (BROWSER) {
// bad! nothing can become interactive until this asynchronous work is done
await hydratable('foo', get_slow_random_number);
}
</script>
```
### hydration_failed
```

@ -140,6 +140,25 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
%handler% should be a function. Did you mean to %suggestion%?
```
### hydratable_missing_but_expected
```
Expected to find a hydratable with key `%key%` during hydration, but did not.
```
This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
```svelte
<script>
import { hydratable } from 'svelte';
if (BROWSER) {
// bad! nothing can become interactive until this asynchronous work is done
await hydratable('foo', get_slow_random_number);
}
</script>
```
### hydration_attribute_changed
```
@ -218,7 +237,7 @@ Hydration failed because the initial UI does not match what was rendered on the
This warning is thrown when Svelte encounters an error while hydrating the HTML from the server. During hydration, Svelte walks the DOM, expecting a certain structure. If that structure is different (for example because the HTML was repaired by the DOM because of invalid HTML), then Svelte will run into issues, resulting in this warning.
During development, this error is often preceeded by a `console.error` detailing the offending HTML, which needs fixing.
During development, this error is often preceded by a `console.error` detailing the offending HTML, which needs fixing.
### invalid_raw_snippet_render

@ -525,6 +525,12 @@ Expected an identifier
Expected identifier or destructure pattern
```
### expected_tag
```
Expected 'html', 'render', 'attach', 'const', or 'debug'
```
### expected_token
```
@ -561,6 +567,12 @@ Cannot use `await` in deriveds and template expressions, or at the top level of
`$host()` can only be used inside custom element component instances
```
### illegal_await_expression
```
`use:`, `transition:` and `animate:` directives, attachments and bindings do not support await expressions
```
### illegal_element_attribute
```

@ -1,5 +1,13 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### async_local_storage_unavailable
```
The node API `AsyncLocalStorage` is not available, but is required to use async server rendering.
```
Some platforms require configuration flags to enable this API. Consult your platform's documentation.
### await_invalid
```
@ -14,6 +22,39 @@ You (or the framework you're using) called [`render(...)`](svelte-server#render)
The `html` property of server render results has been deprecated. Use `body` instead.
```
### hydratable_clobbering
```
Attempted to set `hydratable` with key `%key%` twice with different values.
%stack%
```
This error occurs when using `hydratable` multiple times with the same key. To avoid this, you can:
- Ensure all invocations with the same key result in the same value
- Update the keys to make both instances unique
```svelte
<script>
import { hydratable } from 'svelte';
// which one should "win" and be serialized in the rendered response?
const one = hydratable('not-unique', () => 1);
const two = hydratable('not-unique', () => 2);
</script>
```
### hydratable_serialization_failed
```
Failed to serialize `hydratable` data for key `%key%`.
`hydratable` can serialize anything [`uneval` from `devalue`](https://npmjs.com/package/uneval) can, plus Promises.
Cause:
%stack%
```
### lifecycle_function_unavailable
```
@ -21,3 +62,11 @@ The `html` property of server render results has been deprecated. Use `body` ins
```
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.
### server_context_required
```
Could not resolve `render` context.
```
Certain functions such as `hydratable` cannot be invoked outside of a `render(...)` call, such as at the top level of a module.

@ -0,0 +1,34 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### unresolved_hydratable
```
A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
The `hydratable` was initialized in:
%stack%
```
The most likely cause of this is creating a `hydratable` in the `script` block of your component and then `await`ing
the result inside a `svelte:boundary` with a `pending` snippet:
```svelte
<script>
import { hydratable } from 'svelte';
import { getUser } from '$lib/get-user.js';
const user = hydratable('user', getUser);
</script>
<svelte:boundary>
<h1>{(await user).name}</h1>
{#snippet pending()}
<div>Loading...</div>
{/snippet}
</svelte:boundary>
```
Consider inlining the `hydratable` call inside the boundary so that it's not called on the server.
Note that this can also happen when a `hydratable` contains multiple promises and some but not all of them have been used.

@ -1,5 +1,11 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### experimental_async_required
```
Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true`
```
### invalid_default_snippet
```

@ -43,7 +43,7 @@ The following modifiers are available:
- `preventDefault` — calls `event.preventDefault()` before running the handler
- `stopPropagation` — calls `event.stopPropagation()`, preventing the event reaching the next element
- `stopImmediatePropagation` - calls `event.stopImmediatePropagation()`, preventing other listeners of the same event from being fired.
- `stopImmediatePropagation` calls `event.stopImmediatePropagation()`, preventing other listeners of the same event from being fired.
- `passive` — improves scrolling performance on touch/wheel events (Svelte will add it automatically where it's safe to do so)
- `nonpassive` — explicitly set `passive: false`
- `capture` — fires the handler during the _capture_ phase instead of the _bubbling_ phase

@ -26,7 +26,7 @@
"bench:debug": "node --allow-natives-syntax --inspect-brk ./benchmarking/run.js"
},
"devDependencies": {
"@changesets/cli": "^2.29.7",
"@changesets/cli": "^2.29.8",
"@sveltejs/eslint-config": "^8.3.3",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
@ -41,7 +41,7 @@
"prettier-plugin-svelte": "^3.4.0",
"svelte": "workspace:^",
"typescript": "^5.5.4",
"typescript-eslint": "^8.24.0",
"typescript-eslint": "^8.48.1",
"v8-natives": "^1.2.5",
"vitest": "^2.1.9"
}

@ -1,5 +1,209 @@
# svelte
## 5.45.8
### Patch Changes
- fix: set AST `root.start` to `0` and `root.end` to `template.length` ([#17125](https://github.com/sveltejs/svelte/pull/17125))
- fix: prevent erroneous `state_referenced_locally` warnings on prop fallbacks ([#17329](https://github.com/sveltejs/svelte/pull/17329))
## 5.45.7
### Patch Changes
- fix: Add `<textarea wrap="off">` as a valid attribute value ([#17326](https://github.com/sveltejs/svelte/pull/17326))
- fix: add more css selectors to `print()` ([#17330](https://github.com/sveltejs/svelte/pull/17330))
- fix: don't crash on `hydratable` serialization failure ([#17315](https://github.com/sveltejs/svelte/pull/17315))
## 5.45.6
### Patch Changes
- fix: don't issue a11y warning for `<video>` without captions if it has no `src` ([#17311](https://github.com/sveltejs/svelte/pull/17311))
- fix: add `srcObject` to permitted `<audio>`/`<video>` attributes ([#17310](https://github.com/sveltejs/svelte/pull/17310))
## 5.45.5
### Patch Changes
- fix: correctly reconcile each blocks after outroing branches are resumed ([#17258](https://github.com/sveltejs/svelte/pull/17258))
- fix: destroy each items after siblings are resumed ([#17258](https://github.com/sveltejs/svelte/pull/17258))
## 5.45.4
### Patch Changes
- chore: move DOM-related effect properties to `effect.nodes` ([#17293](https://github.com/sveltejs/svelte/pull/17293))
- fix: allow `$props.id()` to occur after an `await` ([#17285](https://github.com/sveltejs/svelte/pull/17285))
- fix: keep reactions up to date even when read outside of effect ([#17295](https://github.com/sveltejs/svelte/pull/17295))
## 5.45.3
### Patch Changes
- add props to state_referenced_locally ([#17266](https://github.com/sveltejs/svelte/pull/17266))
- fix: preserve node locations for better sourcemaps ([#17269](https://github.com/sveltejs/svelte/pull/17269))
- fix: handle cross-realm Promises in `hydratable` ([#17284](https://github.com/sveltejs/svelte/pull/17284))
## 5.45.2
### Patch Changes
- fix: array destructuring after await ([#17254](https://github.com/sveltejs/svelte/pull/17254))
- fix: throw on invalid `{@tag}`s ([#17256](https://github.com/sveltejs/svelte/pull/17256))
## 5.45.1
### Patch Changes
- fix: link offscreen items and last effect in each block correctly ([#17240](https://github.com/sveltejs/svelte/pull/17240))
## 5.45.0
### Minor Changes
- feat: add `print(...)` function ([#16188](https://github.com/sveltejs/svelte/pull/16188))
## 5.44.1
### Patch Changes
- fix: await blockers before initialising const ([#17226](https://github.com/sveltejs/svelte/pull/17226))
- fix: link offscreen items and last effect in each block correctly ([#17244](https://github.com/sveltejs/svelte/pull/17244))
- fix: generate correct code for simple destructurings ([#17237](https://github.com/sveltejs/svelte/pull/17237))
- fix: ensure each block animations don't mess with transitions ([#17238](https://github.com/sveltejs/svelte/pull/17238))
## 5.44.0
### Minor Changes
- feat: `hydratable` API ([#17154](https://github.com/sveltejs/svelte/pull/17154))
## 5.43.15
### Patch Changes
- fix: don't execute attachments and attribute effects eagerly ([#17208](https://github.com/sveltejs/svelte/pull/17208))
- chore: lift "flushSync cannot be called in effects" restriction ([#17139](https://github.com/sveltejs/svelte/pull/17139))
- fix: store forked derived values ([#17212](https://github.com/sveltejs/svelte/pull/17212))
## 5.43.14
### Patch Changes
- fix: correctly migrate named self closing slots ([#17199](https://github.com/sveltejs/svelte/pull/17199))
- fix: error at compile time instead of at runtime on await expressions inside bindings/transitions/animations/attachments ([#17198](https://github.com/sveltejs/svelte/pull/17198))
- fix: take async blockers into account for bindings/transitions/animations/attachments ([#17198](https://github.com/sveltejs/svelte/pull/17198))
## 5.43.13
### Patch Changes
- fix: don't set derived values during time traveling ([#17200](https://github.com/sveltejs/svelte/pull/17200))
## 5.43.12
### Patch Changes
- fix: maintain correct linked list of effects when updating each blocks ([#17191](https://github.com/sveltejs/svelte/pull/17191))
## 5.43.11
### Patch Changes
- perf: don't use tracing overeager during dev ([#17183](https://github.com/sveltejs/svelte/pull/17183))
- fix: don't cancel transition of already outroing elements ([#17186](https://github.com/sveltejs/svelte/pull/17186))
## 5.43.10
### Patch Changes
- fix: avoid other batches running with queued root effects of main batch ([#17145](https://github.com/sveltejs/svelte/pull/17145))
## 5.43.9
### Patch Changes
- fix: correctly handle functions when determining async blockers ([#17137](https://github.com/sveltejs/svelte/pull/17137))
- fix: keep deriveds reactive after their original parent effect was destroyed ([#17171](https://github.com/sveltejs/svelte/pull/17171))
- fix: ensure eager effects don't break reactions chain ([#17138](https://github.com/sveltejs/svelte/pull/17138))
- fix: ensure async `@const` in boundary hydrates correctly ([#17165](https://github.com/sveltejs/svelte/pull/17165))
- fix: take blockers into account when creating `#await` blocks ([#17137](https://github.com/sveltejs/svelte/pull/17137))
- fix: parallelize async `@const`s in the template ([#17165](https://github.com/sveltejs/svelte/pull/17165))
## 5.43.8
### Patch Changes
- fix: each block losing reactivity when items removed while promise pending ([#17150](https://github.com/sveltejs/svelte/pull/17150))
## 5.43.7
### Patch Changes
- fix: properly defer document title until async work is complete ([#17158](https://github.com/sveltejs/svelte/pull/17158))
- fix: ensure deferred effects can be rescheduled later on ([#17147](https://github.com/sveltejs/svelte/pull/17147))
- fix: take blockers of components into account ([#17153](https://github.com/sveltejs/svelte/pull/17153))
## 5.43.6
### Patch Changes
- fix: don't deactivate other batches ([#17132](https://github.com/sveltejs/svelte/pull/17132))
## 5.43.5
### Patch Changes
- fix: ensure async static props/attributes are awaited ([#17120](https://github.com/sveltejs/svelte/pull/17120))
- fix: wait on dependencies of async bindings ([#17120](https://github.com/sveltejs/svelte/pull/17120))
- fix: await dependencies of style directives ([#17120](https://github.com/sveltejs/svelte/pull/17120))
## 5.43.4
### Patch Changes
- chore: simplify connection/disconnection logic ([#17105](https://github.com/sveltejs/svelte/pull/17105))
- fix: reconnect deriveds to effect tree when time-travelling ([#17105](https://github.com/sveltejs/svelte/pull/17105))
## 5.43.3
### Patch Changes
- fix: ensure fork always accesses correct values ([#17098](https://github.com/sveltejs/svelte/pull/17098))
- fix: change title only after any pending work has completed ([#17061](https://github.com/sveltejs/svelte/pull/17061))
- fix: preserve symbols when creating derived rest properties ([#17096](https://github.com/sveltejs/svelte/pull/17096))
## 5.43.2
### Patch Changes

@ -1239,6 +1239,7 @@ export interface HTMLMediaAttributes<T extends HTMLMediaElement> extends HTMLAtt
playsinline?: boolean | undefined | null;
preload?: 'auto' | 'none' | 'metadata' | '' | undefined | null;
src?: string | undefined | null;
srcobject?: MediaStream | MediaSource | File | Blob;
/**
* a value between 0 and 1
*/
@ -1421,7 +1422,7 @@ export interface HTMLTextareaAttributes extends HTMLAttributes<HTMLTextAreaEleme
// needs both casing variants because language tools does lowercase names of non-shorthand attributes
defaultValue?: string | string[] | number | undefined | null;
defaultvalue?: string | string[] | number | undefined | null;
wrap?: 'hard' | 'soft' | undefined | null;
wrap?: 'hard' | 'soft' | 'off' | undefined | null;
'on:change'?: ChangeEventHandler<HTMLTextAreaElement> | undefined | null;
onchange?: ChangeEventHandler<HTMLTextAreaElement> | undefined | null;

@ -100,10 +100,6 @@ $effect(() => {
Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency.
## experimental_async_fork
> Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
## flush_sync_in_effect
> Cannot use `flushSync` inside an effect
@ -124,6 +120,23 @@ This restriction only applies when using the `experimental.async` option, which
> `getAbortSignal()` can only be called inside an effect or derived
## hydratable_missing_but_required
> Expected to find a hydratable with key `%key%` during hydration, but did not.
This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
```svelte
<script>
import { hydratable } from 'svelte';
if (BROWSER) {
// bad! nothing can become interactive until this asynchronous work is done
await hydratable('foo', get_slow_random_number);
}
</script>
```
## hydration_failed
> Failed to hydrate the application

@ -124,6 +124,23 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
> %handler% should be a function. Did you mean to %suggestion%?
## hydratable_missing_but_expected
> Expected to find a hydratable with key `%key%` during hydration, but did not.
This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
```svelte
<script>
import { hydratable } from 'svelte';
if (BROWSER) {
// bad! nothing can become interactive until this asynchronous work is done
await hydratable('foo', get_slow_random_number);
}
</script>
```
## hydration_attribute_changed
> The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value
@ -192,7 +209,7 @@ To fix this, either silence the warning with a [`svelte-ignore`](basic-markup#Co
This warning is thrown when Svelte encounters an error while hydrating the HTML from the server. During hydration, Svelte walks the DOM, expecting a certain structure. If that structure is different (for example because the HTML was repaired by the DOM because of invalid HTML), then Svelte will run into issues, resulting in this warning.
During development, this error is often preceeded by a `console.error` detailing the offending HTML, which needs fixing.
During development, this error is often preceded by a `console.error` detailing the offending HTML, which needs fixing.
## invalid_raw_snippet_render

@ -223,6 +223,10 @@ The same applies to components:
> Expected identifier or destructure pattern
## expected_tag
> Expected 'html', 'render', 'attach', 'const', or 'debug'
## expected_token
> Expected token %token%
@ -231,6 +235,10 @@ The same applies to components:
> Expected whitespace
## illegal_await_expression
> `use:`, `transition:` and `animate:` directives, attachments and bindings do not support await expressions
## illegal_element_attribute
> `<%name%>` does not support non-event attributes or spread attributes

@ -1,3 +1,9 @@
## async_local_storage_unavailable
> The node API `AsyncLocalStorage` is not available, but is required to use async server rendering.
Some platforms require configuration flags to enable this API. Consult your platform's documentation.
## await_invalid
> Encountered asynchronous work while rendering synchronously.
@ -8,8 +14,43 @@ You (or the framework you're using) called [`render(...)`](svelte-server#render)
> The `html` property of server render results has been deprecated. Use `body` instead.
## hydratable_clobbering
> Attempted to set `hydratable` with key `%key%` twice with different values.
>
> %stack%
This error occurs when using `hydratable` multiple times with the same key. To avoid this, you can:
- Ensure all invocations with the same key result in the same value
- Update the keys to make both instances unique
```svelte
<script>
import { hydratable } from 'svelte';
// which one should "win" and be serialized in the rendered response?
const one = hydratable('not-unique', () => 1);
const two = hydratable('not-unique', () => 2);
</script>
```
## hydratable_serialization_failed
> Failed to serialize `hydratable` data for key `%key%`.
>
> `hydratable` can serialize anything [`uneval` from `devalue`](https://npmjs.com/package/uneval) can, plus Promises.
>
> Cause:
> %stack%
## lifecycle_function_unavailable
> `%name%(...)` is not available on the server
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.
## server_context_required
> Could not resolve `render` context.
Certain functions such as `hydratable` cannot be invoked outside of a `render(...)` call, such as at the top level of a module.

@ -0,0 +1,30 @@
## unresolved_hydratable
> A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
>
> The `hydratable` was initialized in:
> %stack%
The most likely cause of this is creating a `hydratable` in the `script` block of your component and then `await`ing
the result inside a `svelte:boundary` with a `pending` snippet:
```svelte
<script>
import { hydratable } from 'svelte';
import { getUser } from '$lib/get-user.js';
const user = hydratable('user', getUser);
</script>
<svelte:boundary>
<h1>{(await user).name}</h1>
{#snippet pending()}
<div>Loading...</div>
{/snippet}
</svelte:boundary>
```
Consider inlining the `hydratable` call inside the boundary so that it's not called on the server.
Note that this can also happen when a `hydratable` contains multiple promises and some but not all of them have been used.

@ -1,3 +1,7 @@
## experimental_async_required
> Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true`
## invalid_default_snippet
> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.43.2",
"version": "5.45.8",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -174,8 +174,9 @@
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.5.0",
"esm-env": "^1.2.1",
"esrap": "^2.1.0",
"esrap": "^2.2.1",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

@ -1129,6 +1129,15 @@ export function expected_pattern(node) {
e(node, 'expected_pattern', `Expected identifier or destructure pattern\nhttps://svelte.dev/e/expected_pattern`);
}
/**
* Expected 'html', 'render', 'attach', 'const', or 'debug'
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function expected_tag(node) {
e(node, 'expected_tag', `Expected 'html', 'render', 'attach', 'const', or 'debug'\nhttps://svelte.dev/e/expected_tag`);
}
/**
* Expected token %token%
* @param {null | number | NodeLike} node
@ -1148,6 +1157,15 @@ export function expected_whitespace(node) {
e(node, 'expected_whitespace', `Expected whitespace\nhttps://svelte.dev/e/expected_whitespace`);
}
/**
* `use:`, `transition:` and `animate:` directives, attachments and bindings do not support await expressions
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function illegal_await_expression(node) {
e(node, 'illegal_await_expression', `\`use:\`, \`transition:\` and \`animate:\` directives, attachments and bindings do not support await expressions\nhttps://svelte.dev/e/illegal_await_expression`);
}
/**
* `<%name%>` does not support non-event attributes or spread attributes
* @param {null | number | NodeLike} node

@ -10,6 +10,7 @@ import { transform_component, transform_module } from './phases/3-transform/inde
import { validate_component_options, validate_module_options } from './validate-options.js';
import * as state from './state.js';
export { default as preprocess } from './preprocess/index.js';
export { print } from './print/index.js';
/**
* `compile` converts your `.svelte` source code into a JavaScript module that exports a component

@ -604,7 +604,7 @@ const instance_script = {
'Encountered an export declaration pattern that is not supported for automigration.'
);
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
// means that foo and bar are the props (i.e. the leaves are the prop names), not x and z.
// const tmp = b.id(state.scope.generate('tmp'));
// const paths = extract_paths(declarator.id, tmp);
// state.props_pre.push(
@ -1058,8 +1058,6 @@ const template = {
handle_identifier(node, state, path);
},
RegularElement(node, { state, path, next }) {
migrate_slot_usage(node, path, state);
handle_events(node, state);
// Strip off any namespace from the beginning of the node name.
const node_name = node.name.replace(/[a-zA-Z-]*:/g, '');
@ -1067,8 +1065,12 @@ const template = {
let trimmed_position = node.end - 2;
while (state.str.original.charAt(trimmed_position - 1) === ' ') trimmed_position--;
state.str.remove(trimmed_position, node.end - 1);
state.str.appendRight(node.end, `</${node.name}>`);
state.str.appendLeft(node.end, `</${node.name}>`);
}
migrate_slot_usage(node, path, state);
handle_events(node, state);
next();
},
SvelteSelf(node, { state, next }) {
@ -1810,7 +1812,7 @@ function handle_events(element, state) {
}
/**
* Returns start and end of the node. If the start is preceeded with white-space-only before a line break,
* Returns start and end of the node. If the start is preceded with white-space-only before a line break,
* the start will be the start of the line.
* @param {string} source
* @param {LabeledStatement} node

@ -1,4 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { Location } from 'locate-character' */
/** @import * as ESTree from 'estree' */
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';
@ -115,21 +117,8 @@ export class Parser {
e.unexpected_eof(this.index);
}
if (this.root.fragment.nodes.length) {
let start = /** @type {number} */ (this.root.fragment.nodes[0].start);
while (regex_whitespace.test(template[start])) start += 1;
let end = /** @type {number} */ (
this.root.fragment.nodes[this.root.fragment.nodes.length - 1].end
);
while (regex_whitespace.test(template[end - 1])) end -= 1;
this.root.start = start;
this.root.end = end;
} else {
// @ts-ignore
this.root.start = this.root.end = null;
}
this.root.start = 0;
this.root.end = template.length;
const options_index = this.root.fragment.nodes.findIndex(
/** @param {any} thing */
@ -218,31 +207,45 @@ export class Parser {
return result;
}
/** @param {any} allow_reserved */
read_identifier(allow_reserved = false) {
/**
* @returns {ESTree.Identifier & { start: number, end: number, loc: { start: Location, end: Location } }}
*/
read_identifier() {
const start = this.index;
let end = start;
let name = '';
let i = this.index;
const code = /** @type {number} */ (this.template.codePointAt(this.index));
const code = /** @type {number} */ (this.template.codePointAt(i));
if (!isIdentifierStart(code, true)) return null;
if (isIdentifierStart(code, true)) {
let i = this.index;
end += code <= 0xffff ? 1 : 2;
i += code <= 0xffff ? 1 : 2;
while (end < this.template.length) {
const code = /** @type {number} */ (this.template.codePointAt(end));
while (i < this.template.length) {
const code = /** @type {number} */ (this.template.codePointAt(i));
if (!isIdentifierChar(code, true)) break;
i += code <= 0xffff ? 1 : 2;
}
if (!isIdentifierChar(code, true)) break;
end += code <= 0xffff ? 1 : 2;
}
const identifier = this.template.slice(this.index, (this.index = i));
name = this.template.slice(start, end);
this.index = end;
if (!allow_reserved && is_reserved(identifier)) {
e.unexpected_reserved_word(start, identifier);
if (is_reserved(name)) {
e.unexpected_reserved_word(start, name);
}
}
return identifier;
return {
type: 'Identifier',
name,
start,
end,
loc: {
start: state.locator(start),
end: state.locator(end)
}
};
}
/** @param {RegExp} pattern */

@ -1,11 +1,9 @@
/** @import { Location } from 'locate-character' */
/** @import { Pattern } from 'estree' */
/** @import { Parser } from '../index.js' */
import { match_bracket } from '../utils/bracket.js';
import { parse_expression_at } from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
import { locator } from '../../../state.js';
/**
* @param {Parser} parser
@ -15,20 +13,13 @@ export default function read_pattern(parser) {
const start = parser.index;
let i = parser.index;
const name = parser.read_identifier();
const id = parser.read_identifier();
if (name !== null) {
if (id.name !== '') {
const annotation = read_type_annotation(parser);
return {
type: 'Identifier',
name,
start,
loc: {
start: /** @type {Location} */ (locator(start)),
end: /** @type {Location} */ (locator(parser.index))
},
end: parser.index,
...id,
typeAnnotation: annotation
};
}

@ -27,6 +27,9 @@ const visitors = {
delete n.typeArguments;
delete n.returnType;
delete n.accessibility;
delete n.readonly;
delete n.definite;
delete n.override;
},
Decorator(node) {
e.typescript_invalid_feature(node, 'decorators (related TSC proposal is not stage 4 yet)');
@ -132,7 +135,14 @@ const visitors = {
if (node.declare) {
return b.empty;
}
delete node.abstract;
delete node.implements;
delete node.superTypeArguments;
return context.next();
},
ClassExpression(node, context) {
delete node.implements;
delete node.superTypeArguments;
return context.next();
},
MethodDefinition(node, context) {

@ -1,4 +1,5 @@
/** @import { Expression } from 'estree' */
/** @import { Expression, Identifier, SourceLocation } from 'estree' */
/** @import { Location } from 'locate-character' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { is_void } from '../../../../utils.js';
@ -13,6 +14,8 @@ import { create_attribute, ExpressionMetadata, is_element_node } from '../../nod
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';
import { locator } from '../../../state.js';
import * as b from '#compiler/builders';
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
@ -67,10 +70,9 @@ export default function element(parser) {
return;
}
const is_closing_tag = parser.eat('/');
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
if (parser.eat('/')) {
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
if (is_closing_tag) {
parser.allow_whitespace();
parser.eat('>', true);
@ -125,39 +127,41 @@ export default function element(parser) {
return;
}
if (name.startsWith('svelte:') && !meta_tags.has(name)) {
const bounds = { start: start + 1, end: start + 1 + name.length };
const tag = read_tag(parser, regex_whitespace_or_slash_or_closing_tag);
if (tag.name.startsWith('svelte:') && !meta_tags.has(tag.name)) {
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
}
if (!regex_valid_element_name.test(name) && !regex_valid_component_name.test(name)) {
if (!regex_valid_element_name.test(tag.name) && !regex_valid_component_name.test(tag.name)) {
// <div. -> in the middle of typing -> allow in loose mode
if (!parser.loose || !name.endsWith('.')) {
const bounds = { start: start + 1, end: start + 1 + name.length };
if (!parser.loose || !tag.name.endsWith('.')) {
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
e.tag_invalid_name(bounds);
}
}
if (root_only_meta_tags.has(name)) {
if (name in parser.meta_tags) {
e.svelte_meta_duplicate(start, name);
if (root_only_meta_tags.has(tag.name)) {
if (tag.name in parser.meta_tags) {
e.svelte_meta_duplicate(start, tag.name);
}
if (parent.type !== 'Root') {
e.svelte_meta_invalid_placement(start, name);
e.svelte_meta_invalid_placement(start, tag.name);
}
parser.meta_tags[name] = true;
parser.meta_tags[tag.name] = true;
}
const type = meta_tags.has(name)
? meta_tags.get(name)
: regex_valid_component_name.test(name) || (parser.loose && name.endsWith('.'))
const type = meta_tags.has(tag.name)
? meta_tags.get(tag.name)
: regex_valid_component_name.test(tag.name) || (parser.loose && tag.name.endsWith('.'))
? 'Component'
: name === 'title' && parent_is_head(parser.stack)
: tag.name === 'title' && parent_is_head(parser.stack)
? 'TitleElement'
: // TODO Svelte 6/7: once slots are removed in favor of snippets, always keep slot as a regular element
name === 'slot' && !parent_is_shadowroot_template(parser.stack)
tag.name === 'slot' && !parent_is_shadowroot_template(parser.stack)
? 'SlotElement'
: 'RegularElement';
@ -168,7 +172,8 @@ export default function element(parser) {
type,
start,
end: -1,
name,
name: tag.name,
name_loc: tag.loc,
attributes: [],
fragment: create_fragment(true),
metadata: {
@ -184,7 +189,8 @@ export default function element(parser) {
type,
start,
end: -1,
name,
name: tag.name,
name_loc: tag.loc,
attributes: [],
fragment: create_fragment(true),
metadata: {
@ -194,14 +200,14 @@ export default function element(parser) {
parser.allow_whitespace();
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, tag.name)) {
const end = parent.fragment.nodes[0]?.start ?? start;
w.element_implicitly_closed({ start: parent.start, end }, `<${name}>`, `</${parent.name}>`);
w.element_implicitly_closed({ start: parent.start, end }, `<${tag.name}>`, `</${parent.name}>`);
parent.end = start;
parser.pop();
parser.last_auto_closed_tag = {
tag: parent.name,
reason: name,
reason: tag.name,
depth: parser.stack.length
};
}
@ -211,7 +217,7 @@ export default function element(parser) {
const current = parser.current();
const is_top_level_script_or_style =
(name === 'script' || name === 'style') && current.type === 'Root';
(tag.name === 'script' || tag.name === 'style') && current.type === 'Root';
const read = is_top_level_script_or_style ? read_static_attribute : read_attribute;
@ -242,6 +248,10 @@ export default function element(parser) {
parser.allow_whitespace();
}
if (element.type === 'Component') {
element.metadata.expression = new ExpressionMetadata();
}
if (element.type === 'SvelteComponent') {
const index = element.attributes.findIndex(
/** @param {any} attr */
@ -257,6 +267,7 @@ export default function element(parser) {
}
element.expression = get_attribute_expression(definition);
element.metadata.expression = new ExpressionMetadata();
}
if (element.type === 'SvelteElement') {
@ -319,7 +330,7 @@ export default function element(parser) {
}
}
if (name === 'script') {
if (tag.name === 'script') {
const content = read_script(parser, start, element.attributes);
if (prev_comment) {
// We take advantage of the fact that the root will never have leadingComments set,
@ -347,7 +358,7 @@ export default function element(parser) {
parser.append(element);
const self_closing = parser.eat('/') || is_void(name);
const self_closing = parser.eat('/') || is_void(tag.name);
const closed = parser.eat('>', true, false);
// Loose parsing mode
@ -377,7 +388,7 @@ export default function element(parser) {
if (self_closing || !closed) {
// don't push self-closing elements onto the stack
element.end = parser.index;
} else if (name === 'textarea') {
} else if (tag.name === 'textarea') {
// special case
element.fragment.nodes = read_sequence(
parser,
@ -386,10 +397,10 @@ export default function element(parser) {
);
parser.read(regex_closing_textarea_tag);
element.end = parser.index;
} else if (name === 'script' || name === 'style') {
} else if (tag.name === 'script' || tag.name === 'style') {
// special case
const start = parser.index;
const data = parser.read_until(new RegExp(`</${name}>`));
const data = parser.read_until(new RegExp(`</${tag.name}>`));
const end = parser.index;
/** @type {AST.Text} */
@ -402,7 +413,7 @@ export default function element(parser) {
};
element.fragment.nodes.push(node);
parser.eat(`</${name}>`, true);
parser.eat(`</${tag.name}>`, true);
element.end = parser.index;
} else {
parser.stack.push(element);
@ -445,8 +456,8 @@ function parent_is_shadowroot_template(stack) {
function read_static_attribute(parser) {
const start = parser.index;
const name = parser.read_until(regex_token_ending_character);
if (!name) return null;
const tag = read_tag(parser, regex_token_ending_character);
if (!tag.name) return null;
/** @type {true | Array<AST.Text | AST.ExpressionTag>} */
let value = true;
@ -480,7 +491,7 @@ function read_static_attribute(parser) {
e.expected_token(parser.index, '=');
}
return create_attribute(name, start, parser.index, value);
return create_attribute(tag.name, tag.loc, start, parser.index, value);
}
/**
@ -533,10 +544,9 @@ function read_attribute(parser) {
return spread;
} else {
const value_start = parser.index;
let name = parser.read_identifier();
const id = parser.read_identifier();
if (name === null) {
if (id.name === '') {
if (
parser.loose &&
(parser.match('#') || parser.match('/') || parser.match('@') || parser.match(':'))
@ -546,7 +556,6 @@ function read_attribute(parser) {
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);
}
@ -558,32 +567,28 @@ function read_attribute(parser) {
/** @type {AST.ExpressionTag} */
const expression = {
type: 'ExpressionTag',
start: value_start,
end: value_start + name.length,
expression: {
start: value_start,
end: value_start + name.length,
type: 'Identifier',
name
},
start: id.start,
end: id.end,
expression: id,
metadata: {
expression: new ExpressionMetadata()
}
};
return create_attribute(name, start, parser.index, expression);
return create_attribute(id.name, id.loc, start, parser.index, expression);
}
}
const name = parser.read_until(regex_token_ending_character);
if (!name) return null;
const tag = read_tag(parser, regex_token_ending_character);
if (!tag.name) return null;
let end = parser.index;
parser.allow_whitespace();
const colon_index = name.indexOf(':');
const type = colon_index !== -1 && get_directive_type(name.slice(0, colon_index));
const colon_index = tag.name.indexOf(':');
const type = colon_index !== -1 && get_directive_type(tag.name.slice(0, colon_index));
/** @type {true | AST.ExpressionTag | Array<AST.Text | AST.ExpressionTag>} */
let value = true;
@ -612,10 +617,10 @@ function read_attribute(parser) {
}
if (type) {
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
const [directive_name, ...modifiers] = tag.name.slice(colon_index + 1).split('|');
if (directive_name === '') {
e.directive_missing_name({ start, end: start + colon_index + 1 }, name);
e.directive_missing_name({ start, end: start + colon_index + 1 }, tag.name);
}
if (type === 'StyleDirective') {
@ -624,6 +629,7 @@ function read_attribute(parser) {
end,
type,
name: directive_name,
name_loc: tag.loc,
modifiers: /** @type {Array<'important'>} */ (modifiers),
value,
metadata: {
@ -649,23 +655,23 @@ function read_attribute(parser) {
}
}
/** @type {AST.Directive} */
const directive = {
const directive = /** @type {AST.Directive} */ ({
start,
end,
type,
name: directive_name,
name_loc: tag.loc,
expression,
metadata: {
expression: new ExpressionMetadata()
}
};
});
// @ts-expect-error we do this separately from the declaration to avoid upsetting typescript
directive.modifiers = modifiers;
if (directive.type === 'TransitionDirective') {
const direction = name.slice(0, colon_index);
const direction = tag.name.slice(0, colon_index);
directive.intro = direction === 'in' || direction === 'transition';
directive.outro = direction === 'out' || direction === 'transition';
}
@ -686,7 +692,7 @@ function read_attribute(parser) {
return directive;
}
return create_attribute(name, start, end, value);
return create_attribute(tag.name, tag.loc, start, end, value);
}
/**
@ -847,3 +853,25 @@ function read_sequence(parser, done, location) {
e.unexpected_eof(parser.template.length);
}
}
/**
* @param {Parser} parser
* @param {RegExp} regex
* @returns {Identifier & { start: number, end: number, loc: SourceLocation }}
*/
function read_tag(parser, regex) {
const start = parser.index;
const name = parser.read_until(regex);
const end = parser.index;
return {
type: 'Identifier',
name,
start,
end,
loc: {
start: locator(start),
end: locator(end)
}
};
}

@ -177,7 +177,7 @@ function open(parser) {
if (parser.eat(',')) {
parser.allow_whitespace();
index = parser.read_identifier();
index = parser.read_identifier().name;
if (!index) {
e.expected_identifier(parser.index);
}
@ -347,16 +347,10 @@ function open(parser) {
if (parser.eat('snippet')) {
parser.require_whitespace();
const name_start = parser.index;
let name = parser.read_identifier();
const name_end = parser.index;
const id = parser.read_identifier();
if (name === null) {
if (parser.loose) {
name = '';
} else {
e.expected_identifier(parser.index);
}
if (id.name === '' && !parser.loose) {
e.expected_identifier(parser.index);
}
parser.allow_whitespace();
@ -415,12 +409,7 @@ function open(parser) {
type: 'SnippetBlock',
start,
end: -1,
expression: {
type: 'Identifier',
start: name_start,
end: name_end,
name
},
expression: id,
typeParams: type_params,
parameters: function_expression.params,
body: create_fragment(),
@ -724,6 +713,7 @@ function special(parser) {
expression: new ExpressionMetadata()
}
});
return;
}
if (parser.eat('render')) {
@ -755,5 +745,7 @@ function special(parser) {
snippets: new Set()
}
});
return;
}
e.expected_tag(parser.index);
}

@ -10,8 +10,7 @@ export function create_fragment(transparent = false) {
nodes: [],
metadata: {
transparent,
dynamic: false,
has_await: false
dynamic: false
}
};
}

@ -24,6 +24,7 @@ import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AnimateDirective } from './visitors/AnimateDirective.js';
import { AttachTag } from './visitors/AttachTag.js';
import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
@ -142,6 +143,7 @@ const visitors = {
pop_ignore();
}
},
AnimateDirective,
ArrowFunctionExpression,
AssignmentExpression,
AttachTag,
@ -687,193 +689,7 @@ export function analyze_component(root, source, options) {
}
}
/**
* @param {ESTree.Node} expression
* @param {Scope} scope
* @param {Set<Binding>} touched
* @param {Set<ESTree.Node>} seen
*/
const touch = (expression, scope, touched, seen = new Set()) => {
if (seen.has(expression)) return;
seen.add(expression);
walk(
expression,
{ scope },
{
ImportDeclaration(node) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (binding) {
touched.add(binding);
for (const assignment of binding.assignments) {
touch(assignment.value, assignment.scope, touched, seen);
}
}
}
}
}
);
};
/**
* @param {ESTree.Node} node
* @param {Set<ESTree.Node>} seen
* @param {Set<Binding>} reads
* @param {Set<Binding>} writes
*/
const trace_references = (node, reads, writes, seen = new Set()) => {
if (seen.has(node)) return;
seen.add(node);
/**
* @param {ESTree.Pattern} node
* @param {Scope} scope
*/
function update(node, scope) {
for (const pattern of unwrap_pattern(node)) {
const node = object(pattern);
if (!node) return;
const binding = scope.get(node.name);
if (!binding) return;
writes.add(binding);
}
}
walk(
node,
{ scope: instance.scope },
{
_(node, context) {
const scope = scopes.get(node);
if (scope) {
context.next({ scope });
} else {
context.next();
}
},
AssignmentExpression(node, context) {
update(node.left, context.state.scope);
},
UpdateExpression(node, context) {
update(
/** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument),
context.state.scope
);
},
CallExpression(node, context) {
// for now, assume everything touched by the callee ends up mutating the object
// TODO optimise this better
// special case — no need to peek inside effects as they only run once async work has completed
const rune = get_rune(node, context.state.scope);
if (rune === '$effect') return;
/** @type {Set<Binding>} */
const touched = new Set();
touch(node, context.state.scope, touched);
for (const b of touched) {
writes.add(b);
}
},
// don't look inside functions until they are called
ArrowFunctionExpression(_, context) {},
FunctionDeclaration(_, context) {},
FunctionExpression(_, context) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (binding) {
reads.add(binding);
}
}
}
}
);
};
let awaited = false;
// TODO this should probably be attached to the scope?
var promises = b.id('$$promises');
/**
* @param {ESTree.Identifier} id
* @param {ESTree.Expression} blocker
*/
function push_declaration(id, blocker) {
analysis.instance_body.declarations.push(id);
const binding = /** @type {Binding} */ (instance.scope.get(id.name));
binding.blocker = blocker;
}
for (let node of instance.ast.body) {
if (node.type === 'ImportDeclaration') {
analysis.instance_body.hoisted.push(node);
continue;
}
if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
// these can't exist inside `<script>` but TypeScript doesn't know that
continue;
}
if (node.type === 'ExportNamedDeclaration') {
if (node.declaration) {
node = node.declaration;
} else {
continue;
}
}
const has_await = has_await_expression(node);
awaited ||= has_await;
if (awaited && node.type !== 'FunctionDeclaration') {
/** @type {Set<Binding>} */
const reads = new Set(); // TODO we're not actually using this yet
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(node, reads, writes);
const blocker = b.member(promises, b.literal(analysis.instance_body.async.length), true);
for (const binding of writes) {
binding.blocker = blocker;
}
if (node.type === 'VariableDeclaration') {
for (const declarator of node.declarations) {
for (const id of extract_identifiers(declarator.id)) {
push_declaration(id, blocker);
}
// one declarator per declaration, makes things simpler
analysis.instance_body.async.push({
node: declarator,
has_await
});
}
} else if (node.type === 'ClassDeclaration') {
push_declaration(node.id, blocker);
analysis.instance_body.async.push({ node, has_await });
} else {
analysis.instance_body.async.push({ node, has_await });
}
} else {
analysis.instance_body.sync.push(node);
}
}
calculate_blockers(instance, scopes, analysis);
if (analysis.runes) {
const props_refs = module.scope.references.get('$$props');
@ -1084,7 +900,7 @@ export function analyze_component(root, source, options) {
// We need an empty class to generate the set_class() or class="" correctly
if (!has_spread && !has_class && (node.metadata.scoped || has_class_directive)) {
node.attributes.push(
create_attribute('class', -1, -1, [
create_attribute('class', null, -1, -1, [
{
type: 'Text',
data: '',
@ -1099,7 +915,7 @@ export function analyze_component(root, source, options) {
// We need an empty style to generate the set_style() correctly
if (!has_spread && !has_style && has_style_directive) {
node.attributes.push(
create_attribute('style', -1, -1, [
create_attribute('style', null, -1, -1, [
{
type: 'Text',
data: '',
@ -1118,6 +934,287 @@ export function analyze_component(root, source, options) {
return analysis;
}
/**
* Analyzes the instance's top level statements to calculate which bindings need to wait on which
* top level statements. This includes indirect blockers such as functions referencing async top level statements.
*
* @param {Js} instance
* @param {Map<AST.SvelteNode, Scope>} scopes
* @param {ComponentAnalysis} analysis
* @returns {void}
*/
function calculate_blockers(instance, scopes, analysis) {
/**
* @param {ESTree.Node} expression
* @param {Scope} scope
* @param {Set<Binding>} touched
* @param {Set<ESTree.Node>} seen
*/
const touch = (expression, scope, touched, seen = new Set()) => {
if (seen.has(expression)) return;
seen.add(expression);
walk(
expression,
{ scope },
{
ImportDeclaration(node) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (binding) {
touched.add(binding);
for (const assignment of binding.assignments) {
touch(assignment.value, assignment.scope, touched, seen);
}
}
}
}
}
);
};
/**
* @param {ESTree.Node} node
* @param {Set<ESTree.Node>} seen
* @param {Set<Binding>} reads
* @param {Set<Binding>} writes
*/
const trace_references = (node, reads, writes, seen = new Set()) => {
if (seen.has(node)) return;
seen.add(node);
/**
* @param {ESTree.Pattern} node
* @param {Scope} scope
*/
function update(node, scope) {
for (const pattern of unwrap_pattern(node)) {
const node = object(pattern);
if (!node) return;
const binding = scope.get(node.name);
if (!binding) return;
writes.add(binding);
}
}
walk(
node,
{ scope: instance.scope },
{
_(node, context) {
const scope = scopes.get(node);
if (scope) {
context.next({ scope });
} else {
context.next();
}
},
AssignmentExpression(node, context) {
update(node.left, context.state.scope);
},
UpdateExpression(node, context) {
update(
/** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument),
context.state.scope
);
},
CallExpression(node, context) {
// for now, assume everything touched by the callee ends up mutating the object
// TODO optimise this better
// special case — no need to peek inside effects as they only run once async work has completed
const rune = get_rune(node, context.state.scope);
if (rune === '$effect') return;
/** @type {Set<Binding>} */
const touched = new Set();
touch(node, context.state.scope, touched);
for (const b of touched) {
writes.add(b);
}
},
// don't look inside functions until they are called
ArrowFunctionExpression(_, context) {},
FunctionDeclaration(_, context) {},
FunctionExpression(_, context) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (binding) {
reads.add(binding);
}
}
}
}
);
};
let awaited = false;
// TODO this should probably be attached to the scope?
const promises = b.id('$$promises');
/**
* @param {ESTree.Identifier} id
* @param {NonNullable<Binding['blocker']>} blocker
*/
function push_declaration(id, blocker) {
analysis.instance_body.declarations.push(id);
const binding = /** @type {Binding} */ (instance.scope.get(id.name));
binding.blocker = blocker;
}
/**
* Analysis of blockers for functions is deferred until we know which statements are async/blockers
* @type {Array<ESTree.FunctionDeclaration | ESTree.VariableDeclarator>}
*/
const functions = [];
for (let node of instance.ast.body) {
if (node.type === 'ImportDeclaration') {
analysis.instance_body.hoisted.push(node);
continue;
}
if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
// these can't exist inside `<script>` but TypeScript doesn't know that
continue;
}
if (node.type === 'ExportNamedDeclaration') {
if (node.declaration) {
node = node.declaration;
} else {
continue;
}
}
const has_await = has_await_expression(node);
awaited ||= has_await;
if (node.type === 'FunctionDeclaration') {
analysis.instance_body.sync.push(node);
functions.push(node);
} else if (node.type === 'VariableDeclaration') {
for (const declarator of node.declarations) {
if (get_rune(declarator.init, instance.scope) === '$props.id') {
// special case
continue;
}
if (
declarator.init?.type === 'ArrowFunctionExpression' ||
declarator.init?.type === 'FunctionExpression'
) {
// One declarator per declaration, makes things simpler. The ternary ensures more accurate source maps in the common case
analysis.instance_body.sync.push(
node.declarations.length === 1 ? node : b.declaration(node.kind, [declarator])
);
functions.push(declarator);
} else if (!awaited) {
// One declarator per declaration, makes things simpler. The ternary ensures more accurate source maps in the common case
analysis.instance_body.sync.push(
node.declarations.length === 1 ? node : b.declaration(node.kind, [declarator])
);
} else {
/** @type {Set<Binding>} */
const reads = new Set(); // TODO we're not actually using this yet
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(declarator, reads, writes);
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
b.member(promises, b.literal(analysis.instance_body.async.length), true)
);
for (const binding of writes) {
binding.blocker = blocker;
}
for (const id of extract_identifiers(declarator.id)) {
push_declaration(id, blocker);
}
// one declarator per declaration, makes things simpler
analysis.instance_body.async.push({
node: declarator,
has_await
});
}
}
} else if (awaited) {
/** @type {Set<Binding>} */
const reads = new Set(); // TODO we're not actually using this yet
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(node, reads, writes);
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
b.member(promises, b.literal(analysis.instance_body.async.length), true)
);
for (const binding of writes) {
binding.blocker = blocker;
}
if (node.type === 'ClassDeclaration') {
push_declaration(node.id, blocker);
analysis.instance_body.async.push({ node, has_await });
} else {
analysis.instance_body.async.push({ node, has_await });
}
} else {
analysis.instance_body.sync.push(node);
}
}
for (const fn of functions) {
/** @type {Set<Binding>} */
const reads_writes = new Set();
const body =
fn.type === 'VariableDeclarator'
? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init).body
: fn.body;
trace_references(body, reads_writes, reads_writes);
const max = [...reads_writes].reduce((max, binding) => {
if (binding.blocker) {
let property = /** @type {ESTree.SimpleLiteral & { value: number }} */ (
binding.blocker.property
);
return Math.max(property.value, max);
}
return max;
}, -1);
if (max === -1) continue;
const blocker = b.member(promises, b.literal(max), true);
const binding = /** @type {Binding} */ (
fn.type === 'FunctionDeclaration'
? instance.scope.get(fn.id.name)
: instance.scope.get(/** @type {ESTree.Identifier} */ (fn.id).name)
);
binding.blocker = /** @type {typeof binding['blocker']} */ (blocker);
}
}
/**
* @param {Map<import('estree').LabeledStatement, ReactiveStatement>} unsorted_reactive_declarations
*/

@ -0,0 +1,15 @@
/** @import { Context } from '../types' */
/** @import { AST } from '#compiler'; */
import * as e from '../../../errors.js';
/**
* @param {AST.AnimateDirective} node
* @param {Context} context
*/
export function AnimateDirective(node, context) {
context.next({ ...context.state, expression: node.metadata.expression });
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
}

@ -1,7 +1,7 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
import * as e from '../../../errors.js';
/**
* @param {AST.AttachTag} node
@ -10,4 +10,8 @@ import { mark_subtree_dynamic } from './shared/fragment.js';
export function AttachTag(node, context) {
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression });
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
}

@ -26,10 +26,6 @@ export function AwaitExpression(node, context) {
if (context.state.expression) {
context.state.expression.has_await = true;
if (context.state.fragment && context.path.some((node) => node.type === 'ConstTag')) {
context.state.fragment.metadata.has_await = true;
}
suspend = true;
}

@ -159,6 +159,22 @@ export function BindDirective(node, context) {
mark_subtree_dynamic(context.path);
const [get, set] = node.expression.expressions;
// We gotta jump across the getter/setter functions to avoid the expression metadata field being reset to null
// as we want to collect the functions' blocker/async info
context.visit(get.type === 'ArrowFunctionExpression' ? get.body : get, {
...context.state,
expression: node.metadata.expression
});
context.visit(set.type === 'ArrowFunctionExpression' ? set.body : set, {
...context.state,
expression: node.metadata.expression
});
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
return;
}
@ -247,7 +263,8 @@ export function BindDirective(node, context) {
node.metadata = {
binding_group_name: group_name,
parent_each_blocks: each_blocks
parent_each_blocks: each_blocks,
expression: node.metadata.expression
};
}
@ -255,5 +272,9 @@ export function BindDirective(node, context) {
w.bind_invalid_each_rest(binding.node, binding.node.name);
}
context.next();
context.next({ ...context.state, expression: node.metadata.expression });
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
}

@ -16,5 +16,11 @@ export function Component(node, context) {
binding !== null &&
(binding.kind !== 'normal' || node.name.includes('.'));
if (binding) {
node.metadata.expression.has_state = node.metadata.dynamic;
node.metadata.expression.dependencies.add(binding);
node.metadata.expression.references.add(binding);
}
visit_component(node, context);
}

@ -114,7 +114,8 @@ export function Identifier(node, context) {
binding.initial.arguments[0].type !== 'SpreadElement' &&
!should_proxy(binding.initial.arguments[0], context.state.scope)))) ||
binding.kind === 'raw_state' ||
binding.kind === 'derived') &&
binding.kind === 'derived' ||
binding.kind === 'prop') &&
// We're only concerned with reads here
(parent.type !== 'AssignmentExpression' || parent.left !== node) &&
parent.type !== 'UpdateExpression'

@ -48,8 +48,9 @@ export function RegularElement(node, context) {
node.attributes.push(
create_attribute(
'value',
/** @type {AST.Text} */ (node.fragment.nodes.at(0)).start,
/** @type {AST.Text} */ (node.fragment.nodes.at(-1)).end,
null,
-1,
-1,
// @ts-ignore
node.fragment.nodes
)

@ -23,6 +23,9 @@ export function StyleDirective(node, context) {
if (binding.kind !== 'normal') {
node.metadata.expression.has_state = true;
}
if (binding.blocker) {
node.metadata.expression.dependencies.add(binding);
}
}
} else {
context.next();
@ -30,9 +33,7 @@ export function StyleDirective(node, context) {
for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue;
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
node.metadata.expression.has_await ||= chunk.metadata.expression.has_await;
node.metadata.expression.merge(chunk.metadata.expression);
}
}
}

@ -12,7 +12,7 @@ export function SvelteComponent(node, context) {
w.svelte_component_deprecated(node);
}
context.visit(node.expression);
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
visit_component(node, context);
}

@ -1,5 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
@ -10,5 +11,9 @@ import { mark_subtree_dynamic } from './shared/fragment.js';
export function TransitionDirective(node, context) {
mark_subtree_dynamic(context.path);
context.next();
context.next({ ...context.state, expression: node.metadata.expression });
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
}

@ -1,6 +1,7 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
import * as e from '../../../errors.js';
/**
* @param {AST.UseDirective} node
@ -8,5 +9,10 @@ import { mark_subtree_dynamic } from './shared/fragment.js';
*/
export function UseDirective(node, context) {
mark_subtree_dynamic(context.path);
context.next();
context.next({ ...context.state, expression: node.metadata.expression });
if (node.metadata.expression.has_await) {
e.illegal_await_expression(node);
}
}

@ -146,5 +146,15 @@ export function VariableDeclarator(node, context) {
}
}
context.next();
if (node.init && get_rune(node.init, context.state.scope) === '$props') {
// prevent erroneous `state_referenced_locally` warnings on prop fallbacks
context.visit(node.id, {
...context.state,
function_depth: context.state.function_depth + 1
});
context.visit(node.init);
} else {
context.next();
}
}

@ -486,9 +486,17 @@ export function check_element(node, context) {
case 'video': {
const aria_hidden_attribute = attribute_map.get('aria-hidden');
const aria_hidden_exist = aria_hidden_attribute && get_static_value(aria_hidden_attribute);
if (attribute_map.has('muted') || aria_hidden_exist === 'true' || has_spread) {
return;
}
if (!attribute_map.has('src')) {
// don't warn about missing captions if `<video>` has no `src` —
// could e.g. be playing a MediaStream
return;
}
let has_caption = false;
const track = /** @type {AST.RegularElement | undefined} */ (
node.fragment.nodes.find((i) => i.type === 'RegularElement' && i.name === 'track')

@ -1,4 +1,3 @@
/** @import { Location } from 'locate-character' */
/** @import { Namespace } from '#compiler' */
/** @import { ComponentClientTransformState } from '../types.js' */
/** @import { Node } from './types.js' */
@ -15,7 +14,7 @@ function build_locations(nodes) {
for (const node of nodes) {
if (node.type !== 'element') continue;
const { line, column } = /** @type {Location} */ (locator(node.start));
const { line, column } = locator(node.start);
const expression = b.array([b.literal(line), b.literal(column)]);
const children = build_locations(node.children);

@ -51,6 +51,11 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly after_update: Statement[];
/** Transformed `{@const }` declarations */
readonly consts: Statement[];
/** Transformed async `{@const }` declarations (if any) and those coming after them */
async_consts?: {
id: Identifier;
thunks: Expression[];
};
/** Transformed `let:` directives */
readonly let_directives: Statement[];
/** Memoized expressions */

@ -15,14 +15,24 @@ export function AnimateDirective(node, context) {
: b.thunk(/** @type {Expression} */ (context.visit(node.expression)));
// in after_update to ensure it always happens after bind:this
context.state.after_update.push(
b.stmt(
b.call(
'$.animation',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(parse_directive_name(node.name)))),
expression
)
let statement = b.stmt(
b.call(
'$.animation',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(parse_directive_name(node.name)))),
expression
)
);
if (node.metadata.expression.is_async()) {
statement = b.stmt(
b.call(
'$.run_after_blockers',
node.metadata.expression.blockers(),
b.thunk(b.block([statement]))
)
);
}
context.state.after_update.push(statement);
}

@ -9,6 +9,18 @@ import { build_expression } from './shared/utils.js';
*/
export function AttachTag(node, context) {
const expression = build_expression(context, node.expression, node.metadata.expression);
context.state.init.push(b.stmt(b.call('$.attach', context.state.node, b.thunk(expression))));
let statement = b.stmt(b.call('$.attach', context.state.node, b.thunk(expression)));
if (node.metadata.expression.is_async()) {
statement = b.stmt(
b.call(
'$.run_after_blockers',
node.metadata.expression.blockers(),
b.thunk(b.block([statement]))
)
);
}
context.state.init.push(statement);
context.next();
}

@ -56,22 +56,36 @@ export function AwaitBlock(node, context) {
catch_block = b.arrow(args, b.block([...declarations, ...block.body]));
}
context.state.init.push(
add_svelte_meta(
b.call(
'$.await',
context.state.node,
expression,
node.pending
? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.pending)))
: b.null,
then_block,
catch_block
),
node,
'await'
)
const stmt = add_svelte_meta(
b.call(
'$.await',
context.state.node,
expression,
node.pending
? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.pending)))
: b.null,
then_block,
catch_block
),
node,
'await'
);
if (node.metadata.expression.has_blockers()) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
b.array([]),
b.arrow([context.state.node], b.block([stmt]))
)
)
);
} else {
context.state.init.push(stmt);
}
}
/**

@ -40,22 +40,38 @@ export function BindDirective(node, context) {
validate_binding(context.state, node, expression);
}
get = b.thunk(expression);
const assignment = /** @type {Expression} */ (
context.visit(b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value')))
);
/** @type {Expression | undefined} */
set = b.unthunk(
b.arrow(
if (dev) {
// in dev, create named functions, so that `$inspect(...)` delivers
// useful stack traces
get = b.function(b.id('get', node.name_loc), [], b.block([b.return(expression)]));
set = b.function(
b.id('set', node.name_loc),
[b.id('$$value')],
/** @type {Expression} */ (
context.visit(
b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value'))
b.block([b.stmt(assignment)])
);
} else {
// in prod, optimise for brevity
get = b.thunk(expression);
/** @type {Expression | undefined} */
set = b.unthunk(
b.arrow(
[b.id('$$value')],
/** @type {Expression} */ (
context.visit(
b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value'))
)
)
)
)
);
);
if (get === set) {
set = undefined;
if (get === set) {
set = undefined;
}
}
}
@ -250,10 +266,13 @@ export function BindDirective(node, context) {
let statement = defer ? b.stmt(b.call('$.effect', b.thunk(call))) : b.stmt(call);
// TODO this doesn't account for function bindings
if (node.metadata.binding?.blocker) {
if (node.metadata.expression.is_async()) {
statement = b.stmt(
b.call(b.member(node.metadata.binding.blocker, b.id('then')), b.thunk(b.block([statement])))
b.call(
'$.run_after_blockers',
node.metadata.expression.blockers(),
b.thunk(b.block([statement]))
)
);
}

@ -39,7 +39,8 @@ export function CallExpression(node, context) {
}
}
return b.call('$.state', value);
const callee = b.id('$.state', node.callee.loc);
return b.call(callee, value);
}
case '$derived':

@ -1,6 +1,5 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { regex_is_valid_identifier } from '../../../patterns.js';
import { build_component } from './shared/component.js';
/**
@ -8,6 +7,6 @@ import { build_component } from './shared/component.js';
* @param {ComponentContext} context
*/
export function Component(node, context) {
const component = build_component(node, node.name, context);
const component = build_component(node, node.name, node.name_loc, context);
context.state.init.push(component);
}

@ -1,6 +1,7 @@
/** @import { Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/** @import { ExpressionMetadata } from '../../../nodes.js' */
import { dev } from '../../../../state.js';
import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
@ -24,15 +25,15 @@ export function ConstTag(node, context) {
expression = b.call('$.tag', expression, b.literal(declaration.id.name));
}
context.state.consts.push(b.const(declaration.id, expression));
context.state.transform[declaration.id.name] = { read: get_value };
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
context.state.consts.push(b.stmt(b.call('$.get', declaration.id)));
}
add_const_declaration(
context.state,
declaration.id,
expression,
node.metadata.expression,
context.state.scope.get_bindings(declaration)
);
} else {
const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(context.state.scope.generate('computed_const'));
@ -69,13 +70,13 @@ export function ConstTag(node, context) {
expression = b.call('$.tag', expression, b.literal('[@const]'));
}
context.state.consts.push(b.const(tmp, expression));
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
context.state.consts.push(b.stmt(b.call('$.get', tmp)));
}
add_const_declaration(
context.state,
tmp,
expression,
node.metadata.expression,
context.state.scope.get_bindings(declaration)
);
for (const node of identifiers) {
context.state.transform[node.name] = {
@ -84,3 +85,44 @@ export function ConstTag(node, context) {
}
}
}
/**
* @param {ComponentContext['state']} state
* @param {import('estree').Identifier} id
* @param {import('estree').Expression} expression
* @param {ExpressionMetadata} metadata
* @param {import('#compiler').Binding[]} bindings
*/
function add_const_declaration(state, id, expression, metadata, bindings) {
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
const after = dev ? [b.stmt(b.call('$.get', id))] : [];
const has_await = metadata.has_await;
const blockers = [...metadata.dependencies].map((dep) => dep.blocker).filter((b) => b !== null);
if (has_await || state.async_consts || blockers.length > 0) {
const run = (state.async_consts ??= {
id: b.id(state.scope.generate('promises')),
thunks: []
});
state.consts.push(b.let(id));
const assignment = b.assignment('=', id, expression);
const body = after.length === 0 ? assignment : b.block([b.stmt(assignment), ...after]);
if (blockers.length > 0) run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers))));
run.thunks.push(b.thunk(body, has_await));
const blocker = b.member(run.id, b.literal(run.thunks.length - 1), true);
for (const binding of bindings) {
binding.blocker = blocker;
}
} else {
state.consts.push(b.const(id, expression));
state.consts.push(...after);
}
}

@ -48,8 +48,6 @@ export function Fragment(node, context) {
const is_single_child_not_needing_template =
trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
const has_await = context.state.init !== null && (node.metadata.has_await || false);
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
/** @type {Statement[]} */
@ -72,7 +70,8 @@ export function Fragment(node, context) {
metadata: {
namespace,
bound_contenteditable: context.state.metadata.bound_contenteditable
}
},
async_consts: undefined
};
for (const node of hoisted) {
@ -82,7 +81,7 @@ export function Fragment(node, context) {
if (is_single_element) {
const element = /** @type {AST.RegularElement} */ (trimmed[0]);
const id = b.id(context.state.scope.generate(element.name));
const id = b.id(context.state.scope.generate(element.name), element.name_loc);
context.visit(element, {
...state,
@ -153,8 +152,8 @@ export function Fragment(node, context) {
body.push(...state.let_directives, ...state.consts);
if (has_await) {
body.push(b.if(b.call('$.aborted'), b.return()));
if (state.async_consts && state.async_consts.thunks.length > 0) {
body.push(b.var(state.async_consts.id, b.call('$.run', b.array(state.async_consts.thunks))));
}
if (is_text_first) {
@ -177,13 +176,5 @@ export function Fragment(node, context) {
body.push(close);
}
if (has_await) {
return b.block([
b.stmt(
b.call('$.async_body', b.id('$$anchor'), b.arrow([b.id('$$anchor')], b.block(body), true))
)
]);
} else {
return b.block(body);
}
return b.block(body);
}

@ -407,6 +407,7 @@ export function RegularElement(node, context) {
const synthetic_node = node.metadata.synthetic_value_node;
const synthetic_attribute = create_attribute(
'value',
null,
synthetic_node.start,
synthetic_node.end,
[synthetic_node]
@ -484,21 +485,6 @@ function setup_select_synchronization(value_binding, context) {
);
}
/**
* @param {ExpressionMetadata} target
* @param {ExpressionMetadata} source
*/
function merge_metadata(target, source) {
target.has_assignment ||= source.has_assignment;
target.has_await ||= source.has_await;
target.has_call ||= source.has_call;
target.has_member_expression ||= source.has_member_expression;
target.has_state ||= source.has_state;
for (const r of source.references) target.references.add(r);
for (const b of source.dependencies) target.dependencies.add(b);
}
/**
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
@ -514,7 +500,7 @@ export function build_class_directives_object(
const metadata = new ExpressionMetadata();
for (const d of class_directives) {
merge_metadata(metadata, d.metadata.expression);
metadata.merge(d.metadata.expression);
const expression = /** @type Expression */ (context.visit(d.expression));
properties.push(b.init(d.name, expression));
@ -541,7 +527,7 @@ export function build_style_directives_object(
const metadata = new ExpressionMetadata();
for (const d of style_directives) {
merge_metadata(metadata, d.metadata.expression);
metadata.merge(d.metadata.expression);
const expression =
d.value === true

@ -14,8 +14,6 @@ export function SnippetBlock(node, context) {
// TODO hoist where possible
/** @type {(Identifier | AssignmentPattern)[]} */
const args = [b.id('$$anchor')];
const has_await = node.body.metadata.has_await || false;
/** @type {BlockStatement} */
let body;
@ -78,12 +76,8 @@ export function SnippetBlock(node, context) {
// in dev we use a FunctionExpression (not arrow function) so we can use `arguments`
let snippet = dev
? b.call(
'$.wrap_snippet',
b.id(context.state.analysis.name),
b.function(null, args, body, has_await)
)
: b.arrow(args, body, has_await);
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body))
: b.arrow(args, body);
const declaration = b.const(node.expression, snippet);

@ -48,7 +48,11 @@ export function SvelteBoundary(node, context) {
if (child.type === 'ConstTag') {
has_const = true;
if (!context.state.options.experimental.async) {
context.visit(child, { ...context.state, consts: const_tags });
context.visit(child, {
...context.state,
consts: const_tags,
scope: context.state.scopes.get(node.fragment) ?? context.state.scope
});
}
}
}
@ -101,7 +105,13 @@ export function SvelteBoundary(node, context) {
nodes.push(child);
}
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));
const block = /** @type {BlockStatement} */ (
context.visit(
{ ...node.fragment, nodes },
// Since we're creating a new fragment the reference in scopes can't match, so we gotta attach the right scope manually
{ ...context.state, scope: context.state.scopes.get(node.fragment) ?? context.state.scope }
)
);
if (!context.state.options.experimental.async) {
block.body.unshift(...const_tags);

@ -1,12 +1,13 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { build_component } from './shared/component.js';
import * as b from '#compiler/builders';
/**
* @param {AST.SvelteComponent} node
* @param {ComponentContext} context
*/
export function SvelteComponent(node, context) {
const component = build_component(node, '$$component', context);
const component = build_component(node, '$$component', null, context);
context.state.init.push(component);
}

@ -14,7 +14,7 @@ export function SvelteHead(node, context) {
context.state.init.push(
b.stmt(
b.call(
'$.head',
b.id('$.head', node.name_loc),
b.literal(hash(filename)),
b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)))
)

@ -1,5 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { component_name } from '../../../../state.js';
import { build_component } from './shared/component.js';
/**
@ -7,6 +8,6 @@ import { build_component } from './shared/component.js';
* @param {ComponentContext} context
*/
export function SvelteSelf(node, context) {
const component = build_component(node, context.state.analysis.name, context);
const component = build_component(node, component_name, node.name_loc, context);
context.state.init.push(component);
}

@ -20,7 +20,7 @@ export function TitleElement(node, context) {
const statement = b.stmt(
b.assignment(
'=',
b.id('$.document.title'),
b.member(b.id('$.document'), b.id('title', node.name_loc)),
evaluated.is_known
? b.literal(evaluated.value)
: evaluated.is_defined
@ -29,17 +29,16 @@ export function TitleElement(node, context) {
)
);
// Always in an $effect so it only changes the title once async work is done
// Make sure it only changes the title once async work is done
if (has_state) {
context.state.after_update.push(
b.stmt(
b.call(
'$.template_effect',
'$.deferred_template_effect',
b.arrow(memoizer.apply(), b.block([statement])),
memoizer.sync_values(),
memoizer.async_values(),
memoizer.blockers(),
b.true
memoizer.blockers()
)
)
);

@ -25,5 +25,17 @@ export function TransitionDirective(node, context) {
}
// in after_update to ensure it always happens after bind:this
context.state.after_update.push(b.stmt(b.call('$.transition', ...args)));
let statement = b.stmt(b.call('$.transition', ...args));
if (node.metadata.expression.is_async()) {
statement = b.stmt(
b.call(
'$.run_after_blockers',
node.metadata.expression.blockers(),
b.thunk(b.block([statement]))
)
);
}
context.state.after_update.push(statement);
}

@ -32,6 +32,18 @@ export function UseDirective(node, context) {
}
// actions need to run after attribute updates in order with bindings/events
context.state.init.push(b.stmt(b.call('$.action', ...args)));
let statement = b.stmt(b.call('$.action', ...args));
if (node.metadata.expression.is_async()) {
statement = b.stmt(
b.call(
'$.run_after_blockers',
node.metadata.expression.blockers(),
b.thunk(b.block([statement]))
)
);
}
context.state.init.push(statement);
context.next();
}

@ -136,7 +136,8 @@ export function VariableDeclaration(node, context) {
}
if (is_state) {
value = b.call('$.state', value);
const callee = b.id('$.state', /** @type {CallExpression} */ (init).callee.loc);
value = b.call(callee, value);
if (dev) {
value = b.call('$.tag', value, b.literal(id.name));
@ -305,7 +306,7 @@ export function VariableDeclaration(node, context) {
if (has_props) {
if (declarator.id.type !== 'Identifier') {
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
// means that foo and bar are the props (i.e. the leaves are the prop names), not x and z.
const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, SourceLocation, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
import { dev, is_ignored } from '../../../../../state.js';
@ -12,10 +12,11 @@ import { determine_slot } from '../../../../../utils/slot.js';
/**
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
* @param {string} component_name
* @param {SourceLocation | null} loc
* @param {ComponentContext} context
* @returns {Statement}
*/
export function build_component(node, component_name, context) {
export function build_component(node, component_name, loc, context) {
/** @type {Expression} */
const anchor = context.state.node;
@ -171,8 +172,6 @@ export function build_component(node, component_name, context) {
attribute.value,
context,
(value, metadata) => {
if (!metadata.has_state && !metadata.has_await) return value;
// When we have a non-simple computation, anything other than an Identifier or Member expression,
// then there's a good chance it needs to be memoized to avoid over-firing when read within the
// child component (e.g. `active={i === index}`)
@ -198,7 +197,12 @@ export function build_component(node, component_name, context) {
push_prop(b.init(attribute.name, value));
}
} else if (attribute.type === 'BindDirective') {
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
const expression = /** @type {Expression} */ (
context.visit(attribute.expression, { ...context.state, memoizer })
);
// Bindings are a bit special: we don't want to add them to (async) deriveds but we need to check if they have blockers
memoizer.check_blockers(attribute.metadata.expression);
if (
dev &&
@ -256,15 +260,9 @@ export function build_component(node, component_name, context) {
attribute.expression.type === 'Identifier' &&
context.state.scope.get(attribute.expression.name)?.kind === 'store_sub';
// Delay prop pushes so bindings come at the end, to avoid spreads overwriting them
if (is_store_sub) {
push_prop(
b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]),
true
);
} else {
push_prop(b.get(attribute.name, [b.return(expression)]), true);
}
const get = is_store_sub
? b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)])
: b.get(attribute.name, [b.return(expression)]);
const assignment = b.assignment(
'=',
@ -272,10 +270,16 @@ export function build_component(node, component_name, context) {
b.id('$$value')
);
push_prop(
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]),
true
);
const set = b.set(attribute.name, [
b.stmt(/** @type {Expression} */ (context.visit(assignment)))
]);
get.key.loc = attribute.name_loc;
set.key.loc = attribute.name_loc;
// Delay prop pushes so bindings come at the end, to avoid spreads overwriting them
push_prop(get, true);
push_prop(set, true);
}
}
} else if (attribute.type === 'AttachTag') {
@ -293,6 +297,9 @@ export function build_component(node, component_name, context) {
);
}
// TODO also support await expressions here?
memoizer.check_blockers(attribute.metadata.expression);
push_prop(b.prop('init', b.call('$.attachment'), expression, true));
}
}
@ -428,16 +435,17 @@ export function build_component(node, component_name, context) {
/** @param {Expression} node_id */
let fn = (node_id) => {
return b.call(
// TODO We can remove this ternary once we remove legacy mode, since in runes mode dynamic components
// will be handled separately through the `$.component` function, and then the component name will
// always be referenced through just the identifier here.
is_component_dynamic
? intermediate_name
: /** @type {Expression} */ (context.visit(b.member_id(component_name))),
node_id,
props_expression
);
// TODO We can remove this ternary once we remove legacy mode, since in runes mode dynamic components
// will be handled separately through the `$.component` function, and then the component name will
// always be referenced through just the identifier here.
const callee = is_component_dynamic
? b.id(intermediate_name)
: /** @type {Expression} */ (context.visit(b.member_id(component_name)));
// line up the `Foo` in `Foo(...)` and `<Foo>` for usable stack traces
callee.loc = loc;
return b.call(callee, node_id, props_expression);
};
if (bind_this !== null) {
@ -448,6 +456,11 @@ export function build_component(node, component_name, context) {
};
}
if (node.type !== 'SvelteSelf') {
// Component name itself could be blocked on async values
memoizer.check_blockers(node.metadata.expression);
}
const statements = [...snippet_declarations, ...memoizer.deriveds(context.state.analysis.runes)];
if (is_component_dynamic) {

@ -122,7 +122,7 @@ export function build_attribute_value(value, context, memoize = (value) => value
return {
value: memoize(expression, chunk.metadata.expression),
has_state: chunk.metadata.expression.has_state || chunk.metadata.expression.has_await
has_state: chunk.metadata.expression.has_state || chunk.metadata.expression.is_async()
};
}
@ -170,7 +170,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
if (class_directives.length) {
next = build_class_directives_object(class_directives, context);
has_state ||= class_directives.some(
(d) => d.metadata.expression.has_state || d.metadata.expression.has_await
(d) => d.metadata.expression.has_state || d.metadata.expression.is_async()
);
if (has_state) {
@ -240,7 +240,7 @@ export function build_set_style(node_id, attribute, style_directives, context) {
if (style_directives.length) {
next = build_style_directives_object(style_directives, context);
has_state ||= style_directives.some(
(d) => d.metadata.expression.has_state || d.metadata.expression.has_await
(d) => d.metadata.expression.has_state || d.metadata.expression.is_async()
);
if (has_state) {

@ -32,7 +32,13 @@ export function visit_event_attribute(node, context) {
}
context.state.init.push(
b.stmt(b.assignment('=', b.member(context.state.node, '__' + event_name), handler))
b.stmt(
b.assignment(
'=',
b.member(context.state.node, b.id('__' + event_name, node.name_loc)),
handler
)
)
);
} else {
const statement = b.stmt(
@ -140,7 +146,7 @@ export function build_event_handler(node, metadata, context) {
b.this,
b.id('$$args'),
b.id(context.state.analysis.name),
loc && b.array([b.literal(loc.line), b.literal(loc.column)]),
b.array([b.literal(loc.line), b.literal(loc.column)]),
has_side_effects(node) && b.true,
remove_parens && b.true
);

@ -1,4 +1,4 @@
/** @import { Expression } from 'estree' */
/** @import { Expression, Identifier, SourceLocation } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import { cannot_be_set_statically } from '../../../../../../utils.js';
@ -42,13 +42,14 @@ export function process_children(nodes, initial, is_element, context) {
/**
* @param {boolean} is_text
* @param {string} name
* @param {SourceLocation | null} [loc]
*/
function flush_node(is_text, name) {
function flush_node(is_text, name, loc) {
const expression = get_node(is_text);
let id = expression;
if (id.type !== 'Identifier') {
id = b.id(context.state.scope.generate(name));
id = b.id(context.state.scope.generate(name), loc);
context.state.init.push(b.var(id, expression));
}
@ -105,11 +106,16 @@ export function process_children(nodes, initial, is_element, context) {
is_element &&
// In case it's wrapped in async the async logic will want to skip sibling nodes up until the end, hence we cannot make this controlled
// TODO switch this around and instead optimize for elements with a single block child and not require extra comments (neither for async nor normally)
!(node.body.metadata.has_await || node.metadata.expression.is_async())
!node.metadata.expression.is_async()
) {
node.metadata.is_controlled = true;
} else {
const id = flush_node(false, node.type === 'RegularElement' ? node.name : 'node');
const id = flush_node(
false,
node.type === 'RegularElement' ? node.name : 'node',
node.type === 'RegularElement' ? node.name_loc : null
);
child_state = { ...context.state, node: id };
}

@ -31,11 +31,7 @@ export class Memoizer {
* @param {boolean} memoize_if_state
*/
add(expression, metadata, memoize_if_state = false) {
for (const binding of metadata.dependencies) {
if (binding.blocker) {
this.#blockers.add(binding.blocker);
}
}
this.check_blockers(metadata);
const should_memoize =
metadata.has_call || metadata.has_await || (memoize_if_state && metadata.has_state);
@ -52,6 +48,17 @@ export class Memoizer {
return id;
}
/**
* @param {ExpressionMetadata} metadata
*/
check_blockers(metadata) {
for (const binding of metadata.dependencies) {
if (binding.blocker) {
this.#blockers.add(binding.blocker);
}
}
}
apply() {
return [...this.#sync, ...this.#async].map((memo, i) => {
memo.id.name = `$${i}`;
@ -348,6 +355,7 @@ export function validate_binding(state, binding, expression) {
b.call(
'$.validate_binding',
b.literal(state.analysis.source.slice(binding.start, binding.end)),
binding.metadata.expression.blockers(),
b.thunk(
state.store_to_invalidate ? b.sequence([b.call('$.mark_store_binding'), obj]) : obj
),
@ -358,8 +366,8 @@ export function validate_binding(state, binding, expression) {
: b.literal(/** @type {Identifier} */ (expression.property).name)
)
),
loc && b.literal(loc.line),
loc && b.literal(loc.column)
b.literal(loc.line),
b.literal(loc.column)
)
)
);

@ -1,4 +1,10 @@
import type { Expression, Statement, ModuleDeclaration, LabeledStatement } from 'estree';
import type {
Expression,
Statement,
ModuleDeclaration,
LabeledStatement,
Identifier
} from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
@ -21,6 +27,11 @@ export interface ComponentServerTransformState extends ServerTransformState {
readonly namespace: Namespace;
readonly preserve_whitespace: boolean;
readonly skip_hydration_boundaries: boolean;
/** Transformed async `{@const }` declarations (if any) and those coming after them */
async_consts?: {
id: Identifier;
thunks: Expression[];
};
}
export type Context = import('zimmerframe').Context<AST.SvelteNode, ServerTransformState>;

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { extract_identifiers } from '../../../../utils/ast.js';
/**
* @param {AST.ConstTag} node
@ -11,6 +12,36 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
const id = /** @type {Pattern} */ (context.visit(declaration.id));
const init = /** @type {Expression} */ (context.visit(declaration.init));
const has_await = node.metadata.expression.has_await;
const blockers = [...node.metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null);
context.state.init.push(b.const(id, init));
if (has_await || context.state.async_consts || blockers.length > 0) {
const run = (context.state.async_consts ??= {
id: b.id(context.state.scope.generate('promises')),
thunks: []
});
const identifiers = extract_identifiers(declaration.id);
const bindings = context.state.scope.get_bindings(declaration);
for (const identifier of identifiers) {
context.state.init.push(b.let(identifier.name));
}
if (blockers.length > 0) {
run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers))));
}
const assignment = b.assignment('=', id, init);
run.thunks.push(b.thunk(b.block([b.stmt(assignment)]), has_await));
const blocker = b.member(run.id, b.literal(run.thunks.length - 1), true);
for (const binding of bindings) {
binding.blocker = blocker;
}
} else {
context.state.init.push(b.const(id, init));
}
}

@ -34,11 +34,7 @@ export function EachBlock(node, context) {
const new_body = /** @type {BlockStatement} */ (context.visit(node.body)).body;
if (node.body)
each.push(
// TODO get rid of fragment.has_await
...(node.body.metadata.has_await ? [create_async_block(b.block(new_body))] : new_body)
);
if (node.body) each.push(...new_body);
const for_loop = b.for(
b.declaration('let', [
@ -61,7 +57,7 @@ export function EachBlock(node, context) {
b.if(
b.binary('!==', b.member(array_id, 'length'), b.literal(0)),
b.block([open, for_loop]),
node.fallback.metadata.has_await ? create_async_block(fallback) : fallback
fallback
)
);
} else {

@ -28,7 +28,8 @@ export function Fragment(node, context) {
init: [],
template: [],
namespace,
skip_hydration_boundaries: is_standalone
skip_hydration_boundaries: is_standalone,
async_consts: undefined
};
for (const node of hoisted) {
@ -42,5 +43,11 @@ export function Fragment(node, context) {
process_children(trimmed, { ...context, state });
if (state.async_consts && state.async_consts.thunks.length > 0) {
state.init.push(
b.var(state.async_consts.id, b.call('$$renderer.run', b.array(state.async_consts.thunks)))
);
}
return b.block([...state.init, ...build_template(state.template)]);
}

@ -25,11 +25,7 @@ export function IfBlock(node, context) {
const is_async = node.metadata.expression.is_async();
const has_await =
node.metadata.expression.has_await ||
// TODO get rid of this stuff
node.consequent.metadata.has_await ||
node.alternate?.metadata.has_await;
const has_await = node.metadata.expression.has_await;
if (is_async || has_await) {
statement = create_async_block(

@ -99,7 +99,7 @@ export function RegularElement(node, context) {
}
if (dev) {
const location = /** @type {Location} */ (locator(node.start));
const location = locator(node.start);
state.template.push(
b.stmt(
b.call(

@ -3,7 +3,6 @@
/** @import { ComponentContext } from '../types.js' */
import { dev } from '../../../../state.js';
import * as b from '#compiler/builders';
import { create_async_block } from './shared/utils.js';
/**
* @param {AST.SnippetBlock} node
@ -16,10 +15,6 @@ export function SnippetBlock(node, context) {
/** @type {BlockStatement} */ (context.visit(node.body))
);
if (node.body.metadata.has_await) {
fn.body = b.block([create_async_block(fn.body)]);
}
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;

@ -7,8 +7,7 @@ import {
block_open,
block_open_else,
build_attribute_value,
build_template,
create_async_block
build_template
} from './shared/utils.js';
/**
@ -43,14 +42,11 @@ export function SvelteBoundary(node, context) {
);
const pending = b.call(callee, b.id('$$renderer'));
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
const statement = node.fragment.metadata.has_await
? create_async_block(b.block([block]))
: block;
context.state.template.push(
b.if(
callee,
b.block(build_template([block_open_else, b.stmt(pending), block_close])),
b.block(build_template([block_open, statement, block_close]))
b.block(build_template([block_open, block, block_close]))
)
);
} else {
@ -70,9 +66,6 @@ export function SvelteBoundary(node, context) {
}
} else {
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
const statement = node.fragment.metadata.has_await
? create_async_block(b.block([block]))
: block;
context.state.template.push(block_open, statement, block_close);
context.state.template.push(block_open, block, block_close);
}
}

@ -50,7 +50,7 @@ export function SvelteElement(node, context) {
build_element_attributes(node, { ...context, state }, optimiser.transform);
if (dev) {
const location = /** @type {Location} */ (locator(node.start));
const location = locator(node.start);
statements.push(
b.stmt(
b.call(

@ -119,7 +119,7 @@ export function VariableDeclaration(node, context) {
if (has_props) {
if (declarator.id.type !== 'Identifier') {
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
// means that foo and bar are the props (i.e. the leaves are the prop names), not x and z.
const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);

@ -107,6 +107,9 @@ export function build_inline_component(node, expression, context) {
push_prop(b.prop('init', b.key(attribute.name), value));
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
// Bindings are a bit special: we don't want to add them to (async) deriveds but we need to check if they have blockers
optimiser.check_blockers(attribute.metadata.expression);
if (attribute.expression.type === 'SequenceExpression') {
const [get, set] = /** @type {SequenceExpression} */ (context.visit(attribute.expression))
.expressions;
@ -139,6 +142,10 @@ export function build_inline_component(node, expression, context) {
true
);
}
} else if (attribute.type === 'AttachTag') {
// While we don't run attachments on the server, on the client they might generate a surrounding blocker function which generates
// extra comments, and to prevent hydration mismatches we therefore have to account for them here to generate similar comments on the server.
optimiser.check_blockers(attribute.metadata.expression);
}
}
@ -241,12 +248,7 @@ export function build_inline_component(node, expression, context) {
params.push(pattern);
}
const slot_fn = b.arrow(
params,
node.fragment.metadata.has_await
? b.block([create_async_block(b.block(block.body))])
: b.block(block.body)
);
const slot_fn = b.arrow(params, b.block(block.body));
if (slot_name === 'default' && !has_children_prop) {
if (
@ -322,17 +324,26 @@ export function build_inline_component(node, expression, context) {
);
}
if (node.type !== 'SvelteSelf') {
// Component name itself could be blocked on async values
optimiser.check_blockers(node.metadata.expression);
}
const is_async = optimiser.is_async();
if (is_async) {
statement = create_async_block(
b.block([optimiser.apply(), statement]),
b.block([
optimiser.apply(),
dynamic && custom_css_props.length === 0
? b.stmt(b.call('$$renderer.push', empty_comment))
: b.empty,
statement
]),
optimiser.blockers(),
optimiser.has_await
);
}
if (dynamic && custom_css_props.length === 0) {
} else if (dynamic && custom_css_props.length === 0) {
context.state.template.push(empty_comment);
}

@ -121,6 +121,8 @@ export function build_element_attributes(node, context, transform) {
expression = b.call(expression.expressions[0]);
}
expression = transform(expression, attribute.metadata.expression);
if (is_content_editable_binding(attribute.name)) {
content = expression;
} else if (attribute.name === 'value' && node.name === 'textarea') {
@ -140,7 +142,7 @@ export function build_element_attributes(node, context, transform) {
);
attributes.push(
create_attribute('checked', -1, -1, [
create_attribute('checked', null, -1, -1, [
{
type: 'ExpressionTag',
start: -1,
@ -163,7 +165,7 @@ export function build_element_attributes(node, context, transform) {
);
} else {
attributes.push(
create_attribute(attribute.name, -1, -1, [
create_attribute(attribute.name, null, -1, -1, [
{
type: 'ExpressionTag',
start: -1,

@ -347,16 +347,11 @@ export class PromiseOptimiser {
#blockers = new Set();
/**
*
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
*/
transform = (expression, metadata) => {
for (const binding of metadata.dependencies) {
if (binding.blocker) {
this.#blockers.add(binding.blocker);
}
}
this.check_blockers(metadata);
if (metadata.has_await) {
this.has_await = true;
@ -368,6 +363,17 @@ export class PromiseOptimiser {
return expression;
};
/**
* @param {ExpressionMetadata} metadata
*/
check_blockers(metadata) {
for (const binding of metadata.dependencies) {
if (binding.blocker) {
this.#blockers.add(binding.blocker);
}
}
}
apply() {
if (this.expressions.length === 0) {
return b.empty;

@ -35,7 +35,7 @@ export function transform_body(instance_body, runner, transform) {
(node) => /** @type {ESTree.Statement | ESTree.VariableDeclaration} */ (transform(node))
);
// Declarations for the await expressions (they will asign to them; need to be hoisted to be available in whole instance scope)
// Declarations for the await expressions (they will assign to them; need to be hoisted to be available in whole instance scope)
if (instance_body.declarations.length > 0) {
statements.push(
b.declaration(
@ -53,23 +53,25 @@ export function transform_body(instance_body, runner, transform) {
transform(b.var(s.node.id, s.node.init))
);
if (visited.declarations.length === 1) {
return b.thunk(
b.assignment('=', visited.declarations[0].id, visited.declarations[0].init ?? b.void0),
s.has_await
);
const statements = visited.declarations.map((node) => {
if (
node.id.type === 'Identifier' &&
(node.id.name.startsWith('$$d') || node.id.name.startsWith('$$array'))
) {
// this is an intermediate declaration created in VariableDeclaration.js;
// subsequent statements depend on it
return b.var(node.id, node.init);
}
return b.stmt(b.assignment('=', node.id, node.init ?? b.void0));
});
if (statements.length === 1) {
const statement = /** @type {ESTree.ExpressionStatement} */ (statements[0]);
return b.thunk(statement.expression, s.has_await);
}
// if we have multiple declarations, it indicates destructuring
return b.thunk(
b.block([
b.var(visited.declarations[0].id, visited.declarations[0].init),
...visited.declarations
.slice(1)
.map((d) => b.stmt(b.assignment('=', d.id, d.init ?? b.void0)))
]),
s.has_await
);
return b.thunk(b.block(statements), s.has_await);
}
if (s.node.type === 'ClassDeclaration') {

@ -1,4 +1,4 @@
/** @import { Expression, PrivateIdentifier } from 'estree' */
/** @import { Expression, PrivateIdentifier, SourceLocation } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
import * as b from '#compiler/builders';
@ -47,17 +47,19 @@ export function is_custom_element_node(node) {
/**
* @param {string} name
* @param {SourceLocation | null} name_loc
* @param {number} start
* @param {number} end
* @param {AST.Attribute['value']} value
* @returns {AST.Attribute}
*/
export function create_attribute(name, start, end, value) {
export function create_attribute(name, name_loc, start, end, value) {
return {
type: 'Attribute',
start,
end,
name,
name_loc,
value,
metadata: {
delegated: false,
@ -112,9 +114,28 @@ export class ExpressionMetadata {
return b.array([...this.#get_blockers()]);
}
has_blockers() {
return this.#get_blockers().size > 0;
}
is_async() {
return this.has_await || this.#get_blockers().size > 0;
}
/**
* @param {ExpressionMetadata} source
*/
merge(source) {
this.has_state ||= source.has_state;
this.has_call ||= source.has_call;
this.has_await ||= source.has_await;
this.has_member_expression ||= source.has_member_expression;
this.has_assignment ||= source.has_assignment;
this.#blockers = null; // so that blockers are recalculated
for (const r of source.references) this.references.add(r);
for (const b of source.dependencies) this.dependencies.add(b);
}
}
/**

@ -1,4 +1,4 @@
/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super } from 'estree' */
/** @import { BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super, SimpleLiteral, FunctionExpression, ArrowFunctionExpression } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference';
@ -108,7 +108,10 @@ export class Binding {
/** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */
references = [];
/** @type {Array<{ value: Expression; scope: Scope }>} */
/**
* (Re)assignments of this binding. Includes declarations such as `function x() {}`.
* @type {Array<{ value: Expression; scope: Scope }>}
*/
assignments = [];
/**
@ -135,9 +138,10 @@ export class Binding {
/**
* Instance-level declarations may follow (or contain) a top-level `await`. In these cases,
* any reads that occur in the template must wait for the corresponding promise to resolve
* otherwise the initial value will not have been assigned
* otherwise the initial value will not have been assigned.
* It is a member expression of the form `$$blockers[n]`.
* TODO the blocker is set during transform which feels a bit grubby
* @type {Expression | null}
* @type {MemberExpression | null}
*/
blocker = null;

@ -53,6 +53,7 @@ export interface Analysis {
/** @deprecated use `runes` from `state.js` instead */
runes: boolean;
immutable: boolean;
/** True if `$inspect.trace` is used */
tracing: boolean;
comments: AST.JSComment[];

@ -0,0 +1,889 @@
/** @import { AST } from '#compiler'; */
/** @import { Context, Visitors } from 'esrap' */
import * as esrap from 'esrap';
import ts from 'esrap/languages/ts';
import { is_void } from '../../utils.js';
/** Threshold for when content should be formatted on separate lines */
const LINE_BREAK_THRESHOLD = 50;
/**
* `print` converts a Svelte AST node back into Svelte source code.
* It is primarily intended for tools that parse and transform components using the compilers modern AST representation.
*
* `print(ast)` requires an AST node produced by parse with modern: true, or any sub-node within that modern AST.
* The result contains the generated source and a corresponding source map.
* The output is valid Svelte, but formatting details such as whitespace or quoting may differ from the original.
* @param {AST.SvelteNode} ast
* @param {import('./types.js').Options | undefined} options
*/
export function print(ast, options = undefined) {
return esrap.print(
ast,
/** @type {Visitors<AST.SvelteNode>} */ ({
...ts({
comments: ast.type === 'Root' ? ast.comments : [],
getLeadingComments: options?.getLeadingComments,
getTrailingComments: options?.getTrailingComments
}),
...svelte_visitors,
...css_visitors
})
);
}
/**
* @param {Context} context
* @param {AST.SvelteNode} node
* @param {boolean} allow_inline
*/
function block(context, node, allow_inline = false) {
const child_context = context.new();
child_context.visit(node);
if (child_context.empty()) {
return;
}
if (allow_inline && !child_context.multiline) {
context.append(child_context);
} else {
context.indent();
context.newline();
context.append(child_context);
context.dedent();
context.newline();
}
}
/**
* @param {AST.BaseElement['attributes']} attributes
* @param {Context} context
* @returns {boolean} true if attributes were formatted on multiple lines
*/
function attributes(attributes, context) {
if (attributes.length === 0) {
return false;
}
// Measure total width of all attributes when rendered inline
const child_context = context.new();
for (const attribute of attributes) {
child_context.write(' ');
child_context.visit(attribute);
}
const multiline = child_context.measure() > LINE_BREAK_THRESHOLD;
if (multiline) {
context.indent();
for (const attribute of attributes) {
context.newline();
context.visit(attribute);
}
context.dedent();
context.newline();
} else {
context.append(child_context);
}
return multiline;
}
/**
* @param {AST.BaseElement} node
* @param {Context} context
*/
function base_element(node, context) {
const child_context = context.new();
child_context.write('<' + node.name);
// Handle special Svelte components/elements that need 'this' attribute
if (node.type === 'SvelteComponent') {
child_context.write(' this={');
child_context.visit(/** @type {AST.SvelteComponent} */ (node).expression);
child_context.write('}');
} else if (node.type === 'SvelteElement') {
child_context.write(' this={');
child_context.visit(/** @type {AST.SvelteElement} */ (node).tag);
child_context.write('}');
}
const multiline_attributes = attributes(node.attributes, child_context);
const is_self_closing =
is_void(node.name) || (node.type === 'Component' && node.fragment.nodes.length === 0);
let multiline_content = false;
if (is_self_closing) {
child_context.write(`${multiline_attributes ? '' : ' '}/>`);
} else {
child_context.write('>');
// Process the element's content in a separate context for measurement
const content_context = child_context.new();
const allow_inline_content = child_context.measure() < LINE_BREAK_THRESHOLD;
block(content_context, node.fragment, allow_inline_content);
// Determine if content should be formatted on multiple lines
multiline_content = content_context.measure() > LINE_BREAK_THRESHOLD;
if (multiline_content) {
child_context.newline();
// Only indent if attributes are inline and content itself isn't already multiline
const should_indent = !multiline_attributes && !content_context.multiline;
if (should_indent) {
child_context.indent();
}
child_context.append(content_context);
if (should_indent) {
child_context.dedent();
}
child_context.newline();
} else {
child_context.append(content_context);
}
child_context.write(`</${node.name}>`);
}
const break_line_after = child_context.measure() > LINE_BREAK_THRESHOLD;
if ((multiline_content || multiline_attributes) && !context.empty()) {
context.newline();
}
context.append(child_context);
if (is_self_closing) return;
if (multiline_content || multiline_attributes || break_line_after) {
context.newline();
}
}
/** @type {Visitors<AST.SvelteNode>} */
const css_visitors = {
Atrule(node, context) {
context.write(`@${node.name}`);
if (node.prelude) context.write(` ${node.prelude}`);
if (node.block) {
context.write(' ');
context.visit(node.block);
} else {
context.write(';');
}
},
AttributeSelector(node, context) {
context.write(`[${node.name}`);
if (node.matcher) {
context.write(node.matcher);
context.write(`"${node.value}"`);
if (node.flags) {
context.write(` ${node.flags}`);
}
}
context.write(']');
},
Block(node, context) {
context.write('{');
if (node.children.length > 0) {
context.indent();
context.newline();
let started = false;
for (const child of node.children) {
if (started) {
context.newline();
}
context.visit(child);
started = true;
}
context.dedent();
context.newline();
}
context.write('}');
},
ClassSelector(node, context) {
context.write(`.${node.name}`);
},
ComplexSelector(node, context) {
for (const selector of node.children) {
context.visit(selector);
}
},
Declaration(node, context) {
context.write(`${node.property}: ${node.value};`);
},
IdSelector(node, context) {
context.write(`#${node.name}`);
},
NestingSelector(node, context) {
context.write('&');
},
Nth(node, context) {
context.write(node.value);
},
Percentage(node, context) {
context.write(`${node.value}%`);
},
PseudoClassSelector(node, context) {
context.write(`:${node.name}`);
if (node.args) {
context.write('(');
let started = false;
for (const arg of node.args.children) {
if (started) {
context.write(', ');
}
context.visit(arg);
started = true;
}
context.write(')');
}
},
PseudoElementSelector(node, context) {
context.write(`::${node.name}`);
},
RelativeSelector(node, context) {
if (node.combinator) {
if (node.combinator.name === ' ') {
context.write(' ');
} else {
context.write(` ${node.combinator.name} `);
}
}
for (const selector of node.selectors) {
context.visit(selector);
}
},
Rule(node, context) {
let started = false;
for (const selector of node.prelude.children) {
if (started) {
context.write(',');
context.newline();
}
context.visit(selector);
started = true;
}
context.write(' ');
context.visit(node.block);
},
SelectorList(node, context) {
let started = false;
for (const selector of node.children) {
if (started) {
context.write(', ');
}
context.visit(selector);
started = true;
}
},
TypeSelector(node, context) {
context.write(node.name);
}
};
/** @type {Visitors<AST.SvelteNode>} */
const svelte_visitors = {
Root(node, context) {
if (node.options) {
context.write('<svelte:options');
for (const attribute of node.options.attributes) {
context.write(' ');
context.visit(attribute);
}
context.write(' />');
}
let started = false;
for (const item of [node.module, node.instance, node.fragment, node.css]) {
if (!item) continue;
if (started) {
context.margin();
context.newline();
}
context.visit(item);
started = true;
}
},
Script(node, context) {
context.write('<script');
attributes(node.attributes, context);
context.write('>');
block(context, node.content);
context.write('</script>');
},
Fragment(node, context) {
/** @type {AST.SvelteNode[][]} */
const items = [];
/** @type {AST.SvelteNode[]} */
let sequence = [];
const flush = () => {
items.push(sequence);
sequence = [];
};
for (let i = 0; i < node.nodes.length; i += 1) {
let child_node = node.nodes[i];
const prev = node.nodes[i - 1];
const next = node.nodes[i + 1];
if (child_node.type === 'Text') {
child_node = { ...child_node }; // always clone, so we can safely mutate
child_node.data = child_node.data.replace(/[^\S]+/g, ' ');
// trim fragment
if (i === 0) {
child_node.data = child_node.data.trimStart();
}
if (i === node.nodes.length - 1) {
child_node.data = child_node.data.trimEnd();
}
if (child_node.data === '') {
continue;
}
if (child_node.data.startsWith(' ') && prev && prev.type !== 'ExpressionTag') {
flush();
child_node.data = child_node.data.trimStart();
}
if (child_node.data !== '') {
sequence.push({ ...child_node, data: child_node.data });
if (child_node.data.endsWith(' ') && next && next.type !== 'ExpressionTag') {
flush();
child_node.data = child_node.data.trimStart();
}
}
} else {
sequence.push(child_node);
}
}
flush();
let multiline = false;
let width = 0;
const child_contexts = items.map((sequence) => {
const child_context = context.new();
for (const node of sequence) {
child_context.visit(node);
multiline ||= child_context.multiline;
}
width += child_context.measure();
return child_context;
});
multiline ||= width > LINE_BREAK_THRESHOLD;
for (let i = 0; i < child_contexts.length; i += 1) {
const prev = child_contexts[i];
const next = child_contexts[i + 1];
context.append(prev);
if (next) {
if (prev.multiline || next.multiline) {
context.margin();
context.newline();
} else if (multiline) {
context.newline();
}
}
}
},
AnimateDirective(node, context) {
context.write(`animate:${node.name}`);
if (
node.expression !== null &&
!(node.expression.type === 'Identifier' && node.expression.name === node.name)
) {
context.write('={');
context.visit(node.expression);
context.write('}');
}
},
AttachTag(node, context) {
context.write('{@attach ');
context.visit(node.expression);
context.write('}');
},
Attribute(node, context) {
context.write(node.name);
if (node.value === true) return;
context.write('=');
if (Array.isArray(node.value)) {
if (node.value.length > 1 || node.value[0].type === 'Text') {
context.write('"');
}
for (const chunk of node.value) {
context.visit(chunk);
}
if (node.value.length > 1 || node.value[0].type === 'Text') {
context.write('"');
}
} else {
context.visit(node.value);
}
},
AwaitBlock(node, context) {
context.write(`{#await `);
context.visit(node.expression);
if (node.pending) {
context.write('}');
block(context, node.pending);
context.write('{:');
} else {
context.write(' ');
}
if (node.then) {
context.write(node.value ? 'then ' : 'then');
if (node.value) context.visit(node.value);
context.write('}');
block(context, node.then);
if (node.catch) {
context.write('{:');
}
}
if (node.catch) {
context.write(node.value ? 'catch ' : 'catch');
if (node.error) context.visit(node.error);
context.write('}');
block(context, node.catch);
}
context.write('{/await}');
},
BindDirective(node, context) {
context.write(`bind:${node.name}`);
if (node.expression.type === 'Identifier' && node.expression.name === node.name) {
// shorthand
return;
}
context.write('={');
if (node.expression.type === 'SequenceExpression') {
context.visit(node.expression.expressions[0]);
context.write(', ');
context.visit(node.expression.expressions[1]);
} else {
context.visit(node.expression);
}
context.write('}');
},
ClassDirective(node, context) {
context.write(`class:${node.name}`);
if (
node.expression !== null &&
!(node.expression.type === 'Identifier' && node.expression.name === node.name)
) {
context.write('={');
context.visit(node.expression);
context.write('}');
}
},
Comment(node, context) {
context.write('<!--' + node.data + '-->');
},
Component(node, context) {
base_element(node, context);
},
ConstTag(node, context) {
context.write('{@');
context.visit(node.declaration);
context.write('}');
},
DebugTag(node, context) {
context.write('{@debug ');
let started = false;
for (const identifier of node.identifiers) {
if (started) {
context.write(', ');
}
context.visit(identifier);
started = true;
}
context.write('}');
},
EachBlock(node, context) {
context.write('{#each ');
context.visit(node.expression);
if (node.context) {
context.write(' as ');
context.visit(node.context);
}
if (node.index) {
context.write(`, ${node.index}`);
}
if (node.key) {
context.write(' (');
context.visit(node.key);
context.write(')');
}
context.write('}');
block(context, node.body);
if (node.fallback) {
context.write('{:else}');
block(context, node.fallback);
}
context.write('{/each}');
},
ExpressionTag(node, context) {
context.write('{');
context.visit(node.expression);
context.write('}');
},
HtmlTag(node, context) {
context.write('{@html ');
context.visit(node.expression);
context.write('}');
},
IfBlock(node, context) {
if (node.elseif) {
context.write('{:else if ');
context.visit(node.test);
context.write('}');
block(context, node.consequent);
} else {
context.write('{#if ');
context.visit(node.test);
context.write('}');
block(context, node.consequent);
}
if (node.alternate !== null) {
if (
!(
node.alternate.nodes.length === 1 &&
node.alternate.nodes[0].type === 'IfBlock' &&
node.alternate.nodes[0].elseif
)
) {
context.write('{:else}');
block(context, node.alternate);
} else {
context.visit(node.alternate);
}
}
if (!node.elseif) {
context.write('{/if}');
}
},
KeyBlock(node, context) {
context.write('{#key ');
context.visit(node.expression);
context.write('}');
block(context, node.fragment);
context.write('{/key}');
},
LetDirective(node, context) {
context.write(`let:${node.name}`);
if (
node.expression !== null &&
!(node.expression.type === 'Identifier' && node.expression.name === node.name)
) {
context.write('={');
context.visit(node.expression);
context.write('}');
}
},
OnDirective(node, context) {
context.write(`on:${node.name}`);
for (const modifier of node.modifiers) {
context.write(`|${modifier}`);
}
if (
node.expression !== null &&
!(node.expression.type === 'Identifier' && node.expression.name === node.name)
) {
context.write('={');
context.visit(node.expression);
context.write('}');
}
},
RegularElement(node, context) {
base_element(node, context);
},
RenderTag(node, context) {
context.write('{@render ');
context.visit(node.expression);
context.write('}');
},
SlotElement(node, context) {
base_element(node, context);
},
SnippetBlock(node, context) {
context.write('{#snippet ');
context.visit(node.expression);
if (node.typeParams) {
context.write(`<${node.typeParams}>`);
}
context.write('(');
for (let i = 0; i < node.parameters.length; i += 1) {
if (i > 0) context.write(', ');
context.visit(node.parameters[i]);
}
context.write(')}');
block(context, node.body);
context.write('{/snippet}');
},
SpreadAttribute(node, context) {
context.write('{...');
context.visit(node.expression);
context.write('}');
},
StyleDirective(node, context) {
context.write(`style:${node.name}`);
for (const modifier of node.modifiers) {
context.write(`|${modifier}`);
}
if (node.value === true) {
return;
}
context.write('=');
if (Array.isArray(node.value)) {
context.write('"');
for (const tag of node.value) {
context.visit(tag);
}
context.write('"');
} else {
context.visit(node.value);
}
},
StyleSheet(node, context) {
context.write('<style');
attributes(node.attributes, context);
context.write('>');
if (node.children.length > 0) {
context.indent();
context.newline();
let started = false;
for (const child of node.children) {
if (started) {
context.margin();
context.newline();
}
context.visit(child);
started = true;
}
context.dedent();
context.newline();
}
context.write('</style>');
},
SvelteBoundary(node, context) {
base_element(node, context);
},
SvelteComponent(node, context) {
context.write('<svelte:component');
context.write(' this={');
context.visit(node.expression);
context.write('}');
attributes(node.attributes, context);
if (node.fragment && node.fragment.nodes.length > 0) {
context.write('>');
block(context, node.fragment, true);
context.write(`</svelte:component>`);
} else {
context.write(' />');
}
},
SvelteDocument(node, context) {
base_element(node, context);
},
SvelteElement(node, context) {
context.write('<svelte:element ');
context.write('this={');
context.visit(node.tag);
context.write('}');
attributes(node.attributes, context);
if (node.fragment && node.fragment.nodes.length > 0) {
context.write('>');
block(context, node.fragment);
context.write(`</svelte:element>`);
} else {
context.write(' />');
}
},
SvelteFragment(node, context) {
base_element(node, context);
},
SvelteHead(node, context) {
base_element(node, context);
},
SvelteSelf(node, context) {
base_element(node, context);
},
SvelteWindow(node, context) {
base_element(node, context);
},
Text(node, context) {
context.write(node.data);
},
TitleElement(node, context) {
base_element(node, context);
},
TransitionDirective(node, context) {
const directive = node.intro && node.outro ? 'transition' : node.intro ? 'in' : 'out';
context.write(`${directive}:${node.name}`);
for (const modifier of node.modifiers) {
context.write(`|${modifier}`);
}
if (
node.expression !== null &&
!(node.expression.type === 'Identifier' && node.expression.name === node.name)
) {
context.write('={');
context.visit(node.expression);
context.write('}');
}
},
UseDirective(node, context) {
context.write(`use:${node.name}`);
if (
node.expression !== null &&
!(node.expression.type === 'Identifier' && node.expression.name === node.name)
) {
context.write('={');
context.visit(node.expression);
context.write('}');
}
}
};

@ -0,0 +1,7 @@
import 'esrap'; // This import is required to make typescript happy when `skipLibCheck` is enabled
import type ts from 'esrap/languages/ts';
export type Options = {
getLeadingComments?: NonNullable<Parameters<typeof ts>[0]>['getLeadingComments'] | undefined;
getTrailingComments?: NonNullable<Parameters<typeof ts>[0]>['getTrailingComments'] | undefined;
};

@ -40,19 +40,28 @@ export let dev;
export let runes = false;
export let locator = getLocator('', { offsetLine: 1 });
/** @type {(index: number) => Location} */
export let locator;
/** @param {string} value */
export function set_source(value) {
source = value;
locator = getLocator(source, { offsetLine: 1 });
const l = getLocator(source, { offsetLine: 1 });
locator = (i) => {
const loc = l(i);
if (!loc) throw new Error('An impossible situation occurred');
return loc;
};
}
/**
* @param {AST.SvelteNode & { start?: number | undefined }} node
*/
export function locate_node(node) {
const loc = /** @type {Location} */ (locator(/** @type {number} */ (node.start)));
const loc = locator(/** @type {number} */ (node.start));
return `${sanitize_location(filename)}:${loc?.line}:${loc.column}`;
}
@ -103,7 +112,6 @@ export function reset(state) {
runes = false;
component_name = UNKNOWN_FILENAME;
source = '';
locator = () => undefined;
filename = (state.filename ?? UNKNOWN_FILENAME).replace(/\\/g, '/');
warning_filter = state.warning ?? (() => true);
warnings = [];

@ -13,7 +13,8 @@ import type {
Program,
ChainExpression,
SimpleCallExpression,
SequenceExpression
SequenceExpression,
SourceLocation
} from 'estree';
import type { Scope } from '../phases/scope';
import type { _CSS } from './css';
@ -48,8 +49,6 @@ export namespace AST {
* Whether or not we need to traverse into the fragment during mount/hydrate
*/
dynamic: boolean;
/** @deprecated we should get rid of this in favour of the `$$renderer.run` mechanism */
has_await: boolean;
};
}
@ -190,16 +189,20 @@ export namespace AST {
}
/** An `animate:` directive */
export interface AnimateDirective extends BaseNode {
export interface AnimateDirective extends BaseAttribute {
type: 'AnimateDirective';
/** The 'x' in `animate:x` */
name: string;
/** The y in `animate:x={y}` */
expression: null | Expression;
/** @internal */
metadata: {
expression: ExpressionMetadata;
};
}
/** A `bind:` directive */
export interface BindDirective extends BaseNode {
export interface BindDirective extends BaseAttribute {
type: 'BindDirective';
/** The 'x' in `bind:x` */
name: string;
@ -210,11 +213,12 @@ export namespace AST {
binding?: Binding | null;
binding_group_name: Identifier;
parent_each_blocks: EachBlock[];
expression: ExpressionMetadata;
};
}
/** A `class:` directive */
export interface ClassDirective extends BaseNode {
export interface ClassDirective extends BaseAttribute {
type: 'ClassDirective';
/** The 'x' in `class:x` */
name: 'class';
@ -227,7 +231,7 @@ export namespace AST {
}
/** A `let:` directive */
export interface LetDirective extends BaseNode {
export interface LetDirective extends BaseAttribute {
type: 'LetDirective';
/** The 'x' in `let:x` */
name: string;
@ -236,7 +240,7 @@ export namespace AST {
}
/** An `on:` directive */
export interface OnDirective extends BaseNode {
export interface OnDirective extends BaseAttribute {
type: 'OnDirective';
/** The 'x' in `on:x` */
name: string;
@ -260,7 +264,7 @@ export namespace AST {
}
/** A `style:` directive */
export interface StyleDirective extends BaseNode {
export interface StyleDirective extends BaseAttribute {
type: 'StyleDirective';
/** The 'x' in `style:x` */
name: string;
@ -275,7 +279,7 @@ export namespace AST {
// TODO have separate in/out/transition directives
/** A `transition:`, `in:` or `out:` directive */
export interface TransitionDirective extends BaseNode {
export interface TransitionDirective extends BaseAttribute {
type: 'TransitionDirective';
/** The 'x' in `transition:x` */
name: string;
@ -286,19 +290,28 @@ export namespace AST {
intro: boolean;
/** True if this is a `transition:` or `out:` directive */
outro: boolean;
/** @internal */
metadata: {
expression: ExpressionMetadata;
};
}
/** A `use:` directive */
export interface UseDirective extends BaseNode {
export interface UseDirective extends BaseAttribute {
type: 'UseDirective';
/** The 'x' in `use:x` */
name: string;
/** The 'y' in `use:x={y}` */
expression: null | Expression;
/** @internal */
metadata: {
expression: ExpressionMetadata;
};
}
interface BaseElement extends BaseNode {
export interface BaseElement extends BaseNode {
name: string;
name_loc: SourceLocation;
attributes: Array<Attribute | SpreadAttribute | Directive | AttachTag>;
fragment: Fragment;
}
@ -307,6 +320,7 @@ export namespace AST {
type: 'Component';
/** @internal */
metadata: {
expression: ExpressionMetadata;
scopes: Record<string, Scope>;
dynamic: boolean;
/** The set of locally-defined snippets that this component tag could render,
@ -354,6 +368,7 @@ export namespace AST {
expression: Expression;
/** @internal */
metadata: {
expression: ExpressionMetadata;
scopes: Record<string, Scope>;
/** The set of locally-defined snippets that this component tag could render,
* used for CSS pruning purposes */
@ -515,9 +530,13 @@ export namespace AST {
};
}
export interface Attribute extends BaseNode {
type: 'Attribute';
export interface BaseAttribute extends BaseNode {
name: string;
name_loc: SourceLocation | null;
}
export interface Attribute extends BaseAttribute {
type: 'Attribute';
/**
* Quoted/string values are represented by an array, even if they contain a single expression like `"{x}"`
*/

@ -181,7 +181,7 @@ export function logical(operator, left, right) {
}
/**
* @param {'const' | 'let' | 'var'} kind
* @param {ESTree.VariableDeclaration['kind']} kind
* @param {ESTree.VariableDeclarator[]} declarations
* @returns {ESTree.VariableDeclaration}
*/
@ -262,10 +262,14 @@ export function get(name, body) {
/**
* @param {string} name
* @param {ESTree.SourceLocation | null} [loc]
* @returns {ESTree.Identifier}
*/
export function id(name) {
return { type: 'Identifier', name };
export function id(name, loc) {
const node = /** @type {ESTree.Identifier} */ ({ type: 'Identifier', name });
if (loc) node.loc = loc;
return node;
}
/**

@ -249,6 +249,7 @@ export {
hasContext,
setContext
} from './internal/client/context.js';
export { hydratable } from './internal/client/hydratable.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
export { tick, untrack, settled } from './internal/client/runtime.js';
export { createRawSnippet } from './internal/client/dom/blocks/snippet.js';

@ -51,4 +51,6 @@ export {
setContext
} from './internal/server/context.js';
export { hydratable } from './internal/server/hydratable.js';
export { createRawSnippet } from './internal/server/blocks/snippet.js';

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

Loading…
Cancel
Save