mirror of https://github.com/sveltejs/svelte
commit
f64cf42f0c
@ -1,5 +0,0 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
chore: simplify internal component `pop()`
|
@ -1,6 +1,3 @@
|
||||
{
|
||||
"search.exclude": {
|
||||
"sites/svelte-5-preview/static/*": true
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
@ -0,0 +1,144 @@
|
||||
---
|
||||
title: await
|
||||
---
|
||||
|
||||
As of Svelte 5.36, you can use the `await` keyword inside your components in three places where it was previously unavailable:
|
||||
|
||||
- at the top level of your component's `<script>`
|
||||
- inside `$derived(...)` declarations
|
||||
- inside your markup
|
||||
|
||||
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](/docs/kit/configuration) Svelte, usually `svelte.config.js`:
|
||||
|
||||
```js
|
||||
/// file: svelte.config.js
|
||||
export default {
|
||||
compilerOptions: {
|
||||
experimental: {
|
||||
async: true
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The experimental flag will be removed in Svelte 6.
|
||||
|
||||
## Boundaries
|
||||
|
||||
Currently, you can only use `await` inside a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet:
|
||||
|
||||
```svelte
|
||||
<svelte:boundary>
|
||||
<MyApp />
|
||||
|
||||
{#snippet pending()}
|
||||
<p>loading...</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
```
|
||||
|
||||
This restriction will be lifted once Svelte supports asynchronous server-side rendering (see [caveats](#Caveats)).
|
||||
|
||||
> [!NOTE] In the [playground](/playground), your app is rendered inside a boundary with an empty pending snippet, so that you can use `await` without having to create one.
|
||||
|
||||
## Synchronized updates
|
||||
|
||||
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like [this](/playground/untitled#H4sIAAAAAAAAE42QsWrDQBBEf2VZUkhYRE4gjSwJ0qVMkS6XYk9awcFpJe5Wdoy4fw-ycdykSPt2dpiZFYVGxgrf2PsJTlPwPWTcO-U-xwIH5zli9bminudNtwEsbl-v8_wYj-x1Y5Yi_8W7SZRFI1ZYxy64WVsjRj0rEDTwEJWUs6f8cKP2Tp8vVIxSPEsHwyKdukmA-j6jAmwO63Y1SidyCsIneA_T6CJn2ZBD00Jk_XAjT4tmQwEv-32eH6AsgYK6wXWOPPTs6Xy1CaxLECDYgb3kSUbq8p5aaifzorCt0RiUZbQcDIJ10ldH8gs3K6X2Xzqbro5zu1KCHaw2QQPrtclvwVSXc2sEC1T-Vqw0LJy-ClRy_uSkx2ogHzn9ADZ1CubKAQAA)...
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let a = $state(1);
|
||||
let b = $state(2);
|
||||
|
||||
async function add(a, b) {
|
||||
await new Promise((f) => setTimeout(f, 500)); // artificial delay
|
||||
return a + b;
|
||||
}
|
||||
</script>
|
||||
|
||||
<input type="number" bind:value={a}>
|
||||
<input type="number" bind:value={b}>
|
||||
|
||||
<p>{a} + {b} = {await add(a, b)}</p>
|
||||
```
|
||||
|
||||
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
|
||||
|
||||
```html
|
||||
<p>2 + 2 = 3</p>
|
||||
```
|
||||
|
||||
— instead, the text will update to `2 + 2 = 4` when `add(a, b)` resolves.
|
||||
|
||||
Updates can overlap — a fast update will be reflected in the UI while an earlier slow update is still ongoing.
|
||||
|
||||
## Concurrency
|
||||
|
||||
Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup...
|
||||
|
||||
```svelte
|
||||
<p>{await one()}</p>
|
||||
<p>{await two()}</p>
|
||||
```
|
||||
|
||||
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
|
||||
|
||||
This does not apply to sequential `await` expressions inside your `<script>` or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent `$derived` expressions will update independently, even though they will run sequentially when they are first created:
|
||||
|
||||
```js
|
||||
async function one() { return 1; }
|
||||
async function two() { return 2; }
|
||||
// ---cut---
|
||||
// these will run sequentially the first time,
|
||||
// but will update independently
|
||||
let a = $derived(await one());
|
||||
let b = $derived(await two());
|
||||
```
|
||||
|
||||
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning
|
||||
|
||||
## Indicating loading states
|
||||
|
||||
In addition to the nearest boundary's [`pending`](svelte-boundary#Properties-pending) snippet, you can indicate that asynchronous work is ongoing with [`$effect.pending()`]($effect#$effect.pending).
|
||||
|
||||
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
|
||||
|
||||
```js
|
||||
let color = 'red';
|
||||
let answer = -1;
|
||||
let updating = false;
|
||||
// ---cut---
|
||||
import { tick, settled } from 'svelte';
|
||||
|
||||
async function onclick() {
|
||||
updating = true;
|
||||
|
||||
// without this, the change to `updating` will be
|
||||
// grouped with the other changes, meaning it
|
||||
// won't be reflected in the UI
|
||||
await tick();
|
||||
|
||||
color = 'octarine';
|
||||
answer = 42;
|
||||
|
||||
await settled();
|
||||
|
||||
// any updates affected by `color` or `answer`
|
||||
// have now been applied
|
||||
updating = false;
|
||||
}
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
|
||||
|
||||
## Caveats
|
||||
|
||||
As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum.
|
||||
|
||||
Currently, server-side rendering is synchronous. If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, only the `pending` snippet will be rendered.
|
||||
|
||||
## Breaking changes
|
||||
|
||||
Effects run in a slightly different order when the `experimental.async` option is `true`. Specifically, _block_ effects like `{#if ...}` and `{#each ...}` now run before an `$effect.pre` or `beforeUpdate` in the same component, which means that in [very rare situations](/playground/untitled?#H4sIAAAAAAAAE22R3VLDIBCFX2WLvUhnTHsf0zre-Q7WmfwtFV2BgU1rJ5N3F0jaOuoVcPbw7VkYhK4_URTiGYkMnIyjDjLsFGO3EvdCKkIvipdB8NlGXxSCPt96snbtj0gctab2-J_eGs2oOWBE6VunLO_2es-EDKZ5x5ZhC0vPNWM2gHXGouNzAex6hHH1cPHil_Lsb95YT9VQX6KUAbS2DrNsBdsdDFHe8_XSYjH1SrhELTe3MLpsemajweiWVPuxHSbKNd-8eQTdE0EBf4OOaSg2hwNhhE_ABB_ulJzjj9FULvIcqgm5vnAqUB7wWFMfhuugQWkcAr8hVD-mq8D12kOep24J_IszToOXdveGDsuNnZwbJUNlXsKnhJdhUcTo42s41YpOSneikDV5HL8BktM6yRcCAAA=) it is possible to update a block that should no longer exist, but only if you update state inside an effect, [which you should avoid]($effect#When-not-to-use-$effect).
|
@ -0,0 +1,30 @@
|
||||
/** @import { AwaitExpression } from 'estree' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as e from '../../../errors.js';
|
||||
|
||||
/**
|
||||
* @param {AwaitExpression} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function AwaitExpression(node, context) {
|
||||
let suspend = context.state.ast_type === 'instance' && context.state.function_depth === 1;
|
||||
|
||||
if (context.state.expression) {
|
||||
context.state.expression.has_await = true;
|
||||
suspend = true;
|
||||
}
|
||||
|
||||
// disallow top-level `await` or `await` in template expressions
|
||||
// unless a) in runes mode and b) opted into `experimental.async`
|
||||
if (suspend) {
|
||||
if (!context.state.options.experimental.async) {
|
||||
e.experimental_async(node);
|
||||
}
|
||||
|
||||
if (!context.state.analysis.runes) {
|
||||
e.legacy_await_invalid(node);
|
||||
}
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
@ -0,0 +1,319 @@
|
||||
/** @import { ARIARoleRelationConcept } from 'aria-query' */
|
||||
import { roles as roles_map, elementRoles } from 'aria-query';
|
||||
// @ts-expect-error package doesn't provide typings
|
||||
import { AXObjects, elementAXObjects } from 'axobject-query';
|
||||
|
||||
export const aria_attributes =
|
||||
'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(
|
||||
' '
|
||||
);
|
||||
|
||||
/** @type {Record<string, string[]>} */
|
||||
export const a11y_required_attributes = {
|
||||
a: ['href'],
|
||||
area: ['alt', 'aria-label', 'aria-labelledby'],
|
||||
// html-has-lang
|
||||
html: ['lang'],
|
||||
// iframe-has-title
|
||||
iframe: ['title'],
|
||||
img: ['alt'],
|
||||
object: ['title', 'aria-label', 'aria-labelledby']
|
||||
};
|
||||
|
||||
export const a11y_distracting_elements = ['blink', 'marquee'];
|
||||
|
||||
// this excludes `<a>` and `<button>` because they are handled separately
|
||||
export const a11y_required_content = [
|
||||
// heading-has-content
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6'
|
||||
];
|
||||
|
||||
export const a11y_labelable = [
|
||||
'button',
|
||||
'input',
|
||||
'keygen',
|
||||
'meter',
|
||||
'output',
|
||||
'progress',
|
||||
'select',
|
||||
'textarea'
|
||||
];
|
||||
|
||||
export const a11y_interactive_handlers = [
|
||||
// Keyboard events
|
||||
'keypress',
|
||||
'keydown',
|
||||
'keyup',
|
||||
// Click events
|
||||
'click',
|
||||
'contextmenu',
|
||||
'dblclick',
|
||||
'drag',
|
||||
'dragend',
|
||||
'dragenter',
|
||||
'dragexit',
|
||||
'dragleave',
|
||||
'dragover',
|
||||
'dragstart',
|
||||
'drop',
|
||||
'mousedown',
|
||||
'mouseenter',
|
||||
'mouseleave',
|
||||
'mousemove',
|
||||
'mouseout',
|
||||
'mouseover',
|
||||
'mouseup'
|
||||
];
|
||||
|
||||
export const a11y_recommended_interactive_handlers = [
|
||||
'click',
|
||||
'mousedown',
|
||||
'mouseup',
|
||||
'keypress',
|
||||
'keydown',
|
||||
'keyup'
|
||||
];
|
||||
|
||||
export const a11y_nested_implicit_semantics = new Map([
|
||||
['header', 'banner'],
|
||||
['footer', 'contentinfo']
|
||||
]);
|
||||
|
||||
export const a11y_implicit_semantics = new Map([
|
||||
['a', 'link'],
|
||||
['area', 'link'],
|
||||
['article', 'article'],
|
||||
['aside', 'complementary'],
|
||||
['body', 'document'],
|
||||
['button', 'button'],
|
||||
['datalist', 'listbox'],
|
||||
['dd', 'definition'],
|
||||
['dfn', 'term'],
|
||||
['dialog', 'dialog'],
|
||||
['details', 'group'],
|
||||
['dt', 'term'],
|
||||
['fieldset', 'group'],
|
||||
['figure', 'figure'],
|
||||
['form', 'form'],
|
||||
['h1', 'heading'],
|
||||
['h2', 'heading'],
|
||||
['h3', 'heading'],
|
||||
['h4', 'heading'],
|
||||
['h5', 'heading'],
|
||||
['h6', 'heading'],
|
||||
['hr', 'separator'],
|
||||
['img', 'img'],
|
||||
['li', 'listitem'],
|
||||
['link', 'link'],
|
||||
['main', 'main'],
|
||||
['menu', 'list'],
|
||||
['meter', 'progressbar'],
|
||||
['nav', 'navigation'],
|
||||
['ol', 'list'],
|
||||
['option', 'option'],
|
||||
['optgroup', 'group'],
|
||||
['output', 'status'],
|
||||
['progress', 'progressbar'],
|
||||
['section', 'region'],
|
||||
['summary', 'button'],
|
||||
['table', 'table'],
|
||||
['tbody', 'rowgroup'],
|
||||
['textarea', 'textbox'],
|
||||
['tfoot', 'rowgroup'],
|
||||
['thead', 'rowgroup'],
|
||||
['tr', 'row'],
|
||||
['ul', 'list']
|
||||
]);
|
||||
|
||||
export const menuitem_type_to_implicit_role = new Map([
|
||||
['command', 'menuitem'],
|
||||
['checkbox', 'menuitemcheckbox'],
|
||||
['radio', 'menuitemradio']
|
||||
]);
|
||||
|
||||
export const input_type_to_implicit_role = new Map([
|
||||
['button', 'button'],
|
||||
['image', 'button'],
|
||||
['reset', 'button'],
|
||||
['submit', 'button'],
|
||||
['checkbox', 'checkbox'],
|
||||
['radio', 'radio'],
|
||||
['range', 'slider'],
|
||||
['number', 'spinbutton'],
|
||||
['email', 'textbox'],
|
||||
['search', 'searchbox'],
|
||||
['tel', 'textbox'],
|
||||
['text', 'textbox'],
|
||||
['url', 'textbox']
|
||||
]);
|
||||
|
||||
/**
|
||||
* Exceptions to the rule which follows common A11y conventions
|
||||
* TODO make this configurable by the user
|
||||
* @type {Record<string, string[]>}
|
||||
*/
|
||||
export const a11y_non_interactive_element_to_interactive_role_exceptions = {
|
||||
ul: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
|
||||
ol: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
|
||||
li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
|
||||
table: ['grid'],
|
||||
td: ['gridcell'],
|
||||
fieldset: ['radiogroup', 'presentation']
|
||||
};
|
||||
|
||||
export const combobox_if_list = ['email', 'search', 'tel', 'text', 'url'];
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls:-the-autocomplete-attribute
|
||||
export const address_type_tokens = ['shipping', 'billing'];
|
||||
|
||||
export const autofill_field_name_tokens = [
|
||||
'',
|
||||
'on',
|
||||
'off',
|
||||
'name',
|
||||
'honorific-prefix',
|
||||
'given-name',
|
||||
'additional-name',
|
||||
'family-name',
|
||||
'honorific-suffix',
|
||||
'nickname',
|
||||
'username',
|
||||
'new-password',
|
||||
'current-password',
|
||||
'one-time-code',
|
||||
'organization-title',
|
||||
'organization',
|
||||
'street-address',
|
||||
'address-line1',
|
||||
'address-line2',
|
||||
'address-line3',
|
||||
'address-level4',
|
||||
'address-level3',
|
||||
'address-level2',
|
||||
'address-level1',
|
||||
'country',
|
||||
'country-name',
|
||||
'postal-code',
|
||||
'cc-name',
|
||||
'cc-given-name',
|
||||
'cc-additional-name',
|
||||
'cc-family-name',
|
||||
'cc-number',
|
||||
'cc-exp',
|
||||
'cc-exp-month',
|
||||
'cc-exp-year',
|
||||
'cc-csc',
|
||||
'cc-type',
|
||||
'transaction-currency',
|
||||
'transaction-amount',
|
||||
'language',
|
||||
'bday',
|
||||
'bday-day',
|
||||
'bday-month',
|
||||
'bday-year',
|
||||
'sex',
|
||||
'url',
|
||||
'photo'
|
||||
];
|
||||
|
||||
export const contact_type_tokens = ['home', 'work', 'mobile', 'fax', 'pager'];
|
||||
|
||||
export const autofill_contact_field_name_tokens = [
|
||||
'tel',
|
||||
'tel-country-code',
|
||||
'tel-national',
|
||||
'tel-area-code',
|
||||
'tel-local',
|
||||
'tel-local-prefix',
|
||||
'tel-local-suffix',
|
||||
'tel-extension',
|
||||
'email',
|
||||
'impp'
|
||||
];
|
||||
|
||||
export const ElementInteractivity = /** @type {const} */ ({
|
||||
Interactive: 'interactive',
|
||||
NonInteractive: 'non-interactive',
|
||||
Static: 'static'
|
||||
});
|
||||
|
||||
export const invisible_elements = ['meta', 'html', 'script', 'style'];
|
||||
|
||||
export const aria_roles = roles_map.keys();
|
||||
|
||||
export const abstract_roles = aria_roles.filter((role) => roles_map.get(role)?.abstract);
|
||||
|
||||
const non_abstract_roles = aria_roles.filter((name) => !abstract_roles.includes(name));
|
||||
|
||||
export const non_interactive_roles = non_abstract_roles
|
||||
.filter((name) => {
|
||||
const role = roles_map.get(name);
|
||||
return (
|
||||
// 'toolbar' does not descend from widget, but it does support
|
||||
// aria-activedescendant, thus in practice we treat it as a widget.
|
||||
// focusable tabpanel elements are recommended if any panels in a set contain content where the first element in the panel is not focusable.
|
||||
// 'generic' is meant to have no semantic meaning.
|
||||
// 'cell' is treated as CellRole by the AXObject which is interactive, so we treat 'cell' it as interactive as well.
|
||||
!['toolbar', 'tabpanel', 'generic', 'cell'].includes(name) &&
|
||||
!role?.superClass.some((classes) => classes.includes('widget') || classes.includes('window'))
|
||||
);
|
||||
})
|
||||
.concat(
|
||||
// The `progressbar` is descended from `widget`, but in practice, its
|
||||
// value is always `readonly`, so we treat it as a non-interactive role.
|
||||
'progressbar'
|
||||
);
|
||||
|
||||
export const interactive_roles = non_abstract_roles.filter(
|
||||
(name) =>
|
||||
!non_interactive_roles.includes(name) &&
|
||||
// 'generic' is meant to have no semantic meaning.
|
||||
name !== 'generic'
|
||||
);
|
||||
|
||||
export const presentation_roles = ['presentation', 'none'];
|
||||
|
||||
/** @type {ARIARoleRelationConcept[]} */
|
||||
export const non_interactive_element_role_schemas = [];
|
||||
|
||||
/** @type {ARIARoleRelationConcept[]} */
|
||||
export const interactive_element_role_schemas = [];
|
||||
|
||||
for (const [schema, roles] of elementRoles.entries()) {
|
||||
if ([...roles].every((role) => role !== 'generic' && non_interactive_roles.includes(role))) {
|
||||
non_interactive_element_role_schemas.push(schema);
|
||||
}
|
||||
|
||||
if ([...roles].every((role) => interactive_roles.includes(role))) {
|
||||
interactive_element_role_schemas.push(schema);
|
||||
}
|
||||
}
|
||||
|
||||
const interactive_ax_objects = [...AXObjects.keys()].filter(
|
||||
(name) => AXObjects.get(name).type === 'widget'
|
||||
);
|
||||
|
||||
/** @type {ARIARoleRelationConcept[]} */
|
||||
export const interactive_element_ax_object_schemas = [];
|
||||
|
||||
/** @type {ARIARoleRelationConcept[]} */
|
||||
export const non_interactive_element_ax_object_schemas = [];
|
||||
|
||||
const non_interactive_ax_objects = [...AXObjects.keys()].filter((name) =>
|
||||
['windows', 'structure'].includes(AXObjects.get(name).type)
|
||||
);
|
||||
|
||||
for (const [schema, ax_object] of elementAXObjects.entries()) {
|
||||
if ([...ax_object].every((role) => interactive_ax_objects.includes(role))) {
|
||||
interactive_element_ax_object_schemas.push(schema);
|
||||
}
|
||||
|
||||
if ([...ax_object].every((role) => non_interactive_ax_objects.includes(role))) {
|
||||
non_interactive_element_ax_object_schemas.push(schema);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,126 @@
|
||||
/** @import { AwaitExpression, Expression, Property, SpreadElement } from 'estree' */
|
||||
/** @import { Context } from '../types' */
|
||||
import { dev, is_ignored } from '../../../../state.js';
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {AwaitExpression} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function AwaitExpression(node, context) {
|
||||
const argument = /** @type {Expression} */ (context.visit(node.argument));
|
||||
|
||||
const tla = context.state.is_instance && context.state.scope.function_depth === 1;
|
||||
|
||||
// preserve context for
|
||||
// a) top-level await and
|
||||
// b) awaits that precede other expressions in template or `$derived(...)`
|
||||
if (tla || (is_reactive_expression(context) && !is_last_evaluated_expression(context, node))) {
|
||||
return b.call(b.await(b.call('$.save', argument)));
|
||||
}
|
||||
|
||||
// in dev, note which values are read inside a reactive expression,
|
||||
// but don't track them
|
||||
else if (dev && !is_ignored(node, 'await_reactivity_loss')) {
|
||||
return b.call(b.await(b.call('$.track_reactivity_loss', argument)));
|
||||
}
|
||||
|
||||
return argument === node.argument ? node : { ...node, argument };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Context} context
|
||||
*/
|
||||
function is_reactive_expression(context) {
|
||||
if (context.state.in_derived) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let i = context.path.length;
|
||||
|
||||
while (i--) {
|
||||
const parent = context.path[i];
|
||||
|
||||
if (
|
||||
parent.type === 'ArrowFunctionExpression' ||
|
||||
parent.type === 'FunctionExpression' ||
|
||||
parent.type === 'FunctionDeclaration'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// @ts-expect-error we could probably use a neater/more robust mechanism
|
||||
if (parent.metadata) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Context} context
|
||||
* @param {Expression | SpreadElement | Property} node
|
||||
*/
|
||||
function is_last_evaluated_expression(context, node) {
|
||||
let i = context.path.length;
|
||||
|
||||
while (i--) {
|
||||
const parent = /** @type {Expression | Property | SpreadElement} */ (context.path[i]);
|
||||
|
||||
// @ts-expect-error we could probably use a neater/more robust mechanism
|
||||
if (parent.metadata) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (parent.type) {
|
||||
case 'ArrayExpression':
|
||||
if (node !== parent.elements.at(-1)) return false;
|
||||
break;
|
||||
|
||||
case 'AssignmentExpression':
|
||||
case 'BinaryExpression':
|
||||
case 'LogicalExpression':
|
||||
if (node === parent.left) return false;
|
||||
break;
|
||||
|
||||
case 'CallExpression':
|
||||
case 'NewExpression':
|
||||
if (node !== parent.arguments.at(-1)) return false;
|
||||
break;
|
||||
|
||||
case 'ConditionalExpression':
|
||||
if (node === parent.test) return false;
|
||||
break;
|
||||
|
||||
case 'MemberExpression':
|
||||
if (parent.computed && node === parent.object) return false;
|
||||
break;
|
||||
|
||||
case 'ObjectExpression':
|
||||
if (node !== parent.properties.at(-1)) return false;
|
||||
break;
|
||||
|
||||
case 'Property':
|
||||
if (node === parent.key) return false;
|
||||
break;
|
||||
|
||||
case 'SequenceExpression':
|
||||
if (node !== parent.expressions.at(-1)) return false;
|
||||
break;
|
||||
|
||||
case 'TaggedTemplateExpression':
|
||||
if (node !== parent.quasi.expressions.at(-1)) return false;
|
||||
break;
|
||||
|
||||
case 'TemplateLiteral':
|
||||
if (node !== parent.expressions.at(-1)) return false;
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
node = parent;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/** @import { AwaitExpression } from 'estree' */
|
||||
/** @import { Context } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {AwaitExpression} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function AwaitExpression(node, context) {
|
||||
// if `await` is inside a function, or inside `<script module>`,
|
||||
// allow it, otherwise error
|
||||
if (
|
||||
context.state.scope.function_depth === 0 ||
|
||||
context.path.some(
|
||||
(node) =>
|
||||
node.type === 'ArrowFunctionExpression' ||
|
||||
node.type === 'FunctionDeclaration' ||
|
||||
node.type === 'FunctionExpression'
|
||||
)
|
||||
) {
|
||||
return context.next();
|
||||
}
|
||||
|
||||
return b.call('$.await_outside_boundary');
|
||||
}
|
@ -1,15 +1,16 @@
|
||||
import { invalid_snippet_arguments } from '../../shared/errors.js';
|
||||
import * as e from '../errors.js';
|
||||
/**
|
||||
* @param {Node} anchor
|
||||
* @param {...(()=>any)[]} args
|
||||
*/
|
||||
export function validate_snippet_args(anchor, ...args) {
|
||||
if (typeof anchor !== 'object' || !(anchor instanceof Node)) {
|
||||
invalid_snippet_arguments();
|
||||
e.invalid_snippet_arguments();
|
||||
}
|
||||
|
||||
for (let arg of args) {
|
||||
if (typeof arg !== 'function') {
|
||||
invalid_snippet_arguments();
|
||||
e.invalid_snippet_arguments();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
/** @import { TemplateNode, Value } from '#client' */
|
||||
import { flatten } from '../../reactivity/async.js';
|
||||
import { get } from '../../runtime.js';
|
||||
import { get_pending_boundary } from './boundary.js';
|
||||
|
||||
/**
|
||||
* @param {TemplateNode} node
|
||||
* @param {Array<() => Promise<any>>} expressions
|
||||
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
|
||||
*/
|
||||
export function async(node, expressions, fn) {
|
||||
var boundary = get_pending_boundary();
|
||||
|
||||
boundary.update_pending_count(1);
|
||||
|
||||
flatten([], expressions, (values) => {
|
||||
try {
|
||||
// get values eagerly to avoid creating blocks if they reject
|
||||
for (const d of values) get(d);
|
||||
|
||||
fn(node, ...values);
|
||||
} finally {
|
||||
boundary.update_pending_count(-1);
|
||||
}
|
||||
});
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue