Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add error boundaries #14211

Merged
merged 46 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
007d5f3
feat: add error boundary support
trueadm Nov 5, 2024
f683ce2
tweak
trueadm Nov 7, 2024
51e49be
Update packages/svelte/elements.d.ts
trueadm Nov 7, 2024
11b2ecd
Update .changeset/polite-peaches-do.md
trueadm Nov 7, 2024
10897ac
fix issue with rethrowing
trueadm Nov 7, 2024
e6b6a68
handle fallback error
trueadm Nov 8, 2024
dc95bd3
handle fallback error
trueadm Nov 8, 2024
b9bc80d
add more test coverage
trueadm Nov 8, 2024
cf11f58
more tests
trueadm Nov 8, 2024
4d8ad24
more bug fixes
trueadm Nov 8, 2024
2c8dea6
guard against non-errors
trueadm Nov 9, 2024
5be2334
add component_stack to error
trueadm Nov 9, 2024
c468c6d
alternative approach
trueadm Nov 11, 2024
3ae0c80
Merge branch 'main' into error-boundaries
trueadm Nov 15, 2024
3441004
remove spread support
trueadm Nov 15, 2024
75a01a5
lint
trueadm Nov 15, 2024
371f118
add to legacy ast
dummdidumm Nov 20, 2024
36dcb7d
Merge branch 'main' into error-boundaries
dummdidumm Nov 20, 2024
3afd1bb
add to svelte-html
dummdidumm Nov 20, 2024
1564640
disallow anything but attributes on the boundary element
dummdidumm Nov 20, 2024
acb3cd0
Merge branch 'main' into error-boundaries
Rich-Harris Nov 26, 2024
bc72ed2
fix error
Rich-Harris Nov 26, 2024
a77bf50
more validation
Rich-Harris Nov 26, 2024
f82a59b
only create block when necessary
Rich-Harris Nov 26, 2024
6aa714e
swap argument order - results in nicer-looking code in many cases
Rich-Harris Nov 26, 2024
b53cfc8
Update .changeset/polite-peaches-do.md
Rich-Harris Nov 26, 2024
1ec18a3
simplify a bit
Rich-Harris Nov 26, 2024
a7ee520
simplify
Rich-Harris Nov 26, 2024
3070f67
move declaration closer to usage
Rich-Harris Nov 26, 2024
fbbb7d9
push once
Rich-Harris Nov 26, 2024
3322856
unused
Rich-Harris Nov 26, 2024
08b82f9
tweaks
Rich-Harris Nov 27, 2024
2d27f50
consistent naming
Rich-Harris Nov 27, 2024
702adf9
simplify
Rich-Harris Nov 27, 2024
69877ae
add a couple newlines
Rich-Harris Nov 27, 2024
8e74719
tweak comments
Rich-Harris Nov 27, 2024
b4a30a4
simplify
Rich-Harris Nov 27, 2024
a81dc34
newlines
Rich-Harris Nov 27, 2024
0fe5274
placeholder documentation
Rich-Harris Nov 27, 2024
62e1af8
add some docs
Rich-Harris Nov 27, 2024
dfdcf02
Update packages/svelte/src/internal/client/dom/blocks/boundary.js
trueadm Nov 27, 2024
b3d1d92
Update packages/svelte/src/internal/client/dom/blocks/boundary.js
trueadm Nov 27, 2024
dc36557
Update packages/svelte/src/internal/client/dom/blocks/boundary.js
trueadm Nov 27, 2024
2b0778e
fix type
trueadm Nov 27, 2024
93b16b1
fix link
Rich-Harris Dec 1, 2024
4509d3b
explain what happens if onerror throws
Rich-Harris Dec 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/polite-peaches-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: add error boundaries with `<svelte:boundary>`
12 changes: 12 additions & 0 deletions documentation/docs/98-reference/.generated/compile-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,18 @@ A component can have a single top-level `<style>` element
`<svelte:body>` does not support non-event attributes or spread attributes
```

### svelte_boundary_invalid_attribute

```
Valid attributes on `<svelte:boundary>` are `onerror` and `failed`
```

### svelte_boundary_invalid_attribute_value

```
Attribute value must be a non-string expression
```

### svelte_component_invalid_this

```
Expand Down
4 changes: 4 additions & 0 deletions packages/svelte/elements.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2044,6 +2044,10 @@ export interface SvelteHTMLElements {
[name: string]: any;
};
'svelte:head': { [name: string]: any };
'svelte:boundary': {
onerror?: (error: unknown, reset: () => void) => void;
failed?: import('svelte').Snippet<[error: unknown, reset: () => void]>;
};

[name: string]: { [name: string]: any };
}
8 changes: 8 additions & 0 deletions packages/svelte/messages/compile-errors/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,14 @@ HTML restricts where certain elements can appear. In case of a violation the bro

> `<svelte:body>` does not support non-event attributes or spread attributes

## svelte_boundary_invalid_attribute

> Valid attributes on `<svelte:boundary>` are `onerror` and `failed`

## svelte_boundary_invalid_attribute_value

> Attribute value must be a non-string expression

## svelte_component_invalid_this

> Invalid component definition — must be an `{expression}`
Expand Down
18 changes: 18 additions & 0 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,24 @@ export function svelte_body_illegal_attribute(node) {
e(node, "svelte_body_illegal_attribute", "`<svelte:body>` does not support non-event attributes or spread attributes");
}

/**
* Valid attributes on `<svelte:boundary>` are `onerror` and `failed`
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function svelte_boundary_invalid_attribute(node) {
e(node, "svelte_boundary_invalid_attribute", "Valid attributes on `<svelte:boundary>` are `onerror` and `failed`");
}

/**
* Attribute value must be a non-string expression
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function svelte_boundary_invalid_attribute_value(node) {
e(node, "svelte_boundary_invalid_attribute_value", "Attribute value must be a non-string expression");
}

/**
* Invalid component definition — must be an `{expression}`
* @param {null | number | NodeLike} node
Expand Down
14 changes: 14 additions & 0 deletions packages/svelte/src/compiler/legacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,20 @@ export function convert(source, ast) {
children: node.body.nodes.map((child) => visit(child))
};
},
// @ts-expect-error
SvelteBoundary(node, { visit }) {
remove_surrounding_whitespace_nodes(node.fragment.nodes);
return {
type: 'SvelteBoundary',
name: 'svelte:boundary',
start: node.start,
end: node.end,
attributes: node.attributes.map(
(child) => /** @type {Legacy.LegacyAttributeLike} */ (visit(child))
),
children: node.fragment.nodes.map((child) => visit(child))
};
},
RegularElement(node, { visit }) {
return {
type: 'Element',
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte/src/compiler/phases/1-parse/state/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ const meta_tags = new Map([
['svelte:element', 'SvelteElement'],
['svelte:component', 'SvelteComponent'],
['svelte:self', 'SvelteSelf'],
['svelte:fragment', 'SvelteFragment']
['svelte:fragment', 'SvelteFragment'],
['svelte:boundary', 'SvelteBoundary']
]);

/** @param {Parser} parser */
Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js';
import { Text } from './visitors/Text.js';
import { TitleElement } from './visitors/TitleElement.js';
Expand Down Expand Up @@ -171,6 +172,7 @@ const visitors = {
SvelteHead,
SvelteSelf,
SvelteWindow,
SvelteBoundary,
TaggedTemplateExpression,
Text,
TransitionDirective,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';

const valid = ['onerror', 'failed'];

/**
* @param {AST.SvelteBoundary} node
* @param {Context} context
*/
export function SvelteBoundary(node, context) {
for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute' || !valid.includes(attribute.name)) {
e.svelte_boundary_invalid_attribute(attribute);
}

if (
attribute.value === true ||
(Array.isArray(attribute.value) &&
(attribute.value.length !== 1 || attribute.value[0].type !== 'ExpressionTag'))
) {
e.svelte_boundary_invalid_attribute_value(attribute);
}
}

context.next();
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { SvelteComponent } from './visitors/SvelteComponent.js';
import { SvelteDocument } from './visitors/SvelteDocument.js';
import { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
Expand Down Expand Up @@ -122,6 +123,7 @@ const visitors = {
SvelteDocument,
SvelteElement,
SvelteFragment,
SvelteBoundary,
SvelteHead,
SvelteSelf,
SvelteWindow,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/** @import { BlockStatement, Statement, Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';

/**
* @param {AST.SvelteBoundary} node
* @param {ComponentContext} context
*/
export function SvelteBoundary(node, context) {
const props = b.object([]);

for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute' || attribute.value === true) {
// these can't exist, because they would have caused validation
// to fail, but typescript doesn't know that
continue;
}

const chunk = Array.isArray(attribute.value)
? /** @type {AST.ExpressionTag} */ (attribute.value[0])
: attribute.value;

const expression = /** @type {Expression} */ (context.visit(chunk.expression, context.state));

if (attribute.metadata.expression.has_state) {
props.properties.push(b.get(attribute.name, [b.return(expression)]));
} else {
props.properties.push(b.init(attribute.name, expression));
}
}

const nodes = [];

/** @type {Statement[]} */
const snippet_statements = [];

// Capture the `failed` implicit snippet prop
for (const child of node.fragment.nodes) {
if (child.type === 'SnippetBlock' && child.expression.name === 'failed') {
/** @type {Statement[]} */
const init = [];
context.visit(child, { ...context.state, init });
props.properties.push(b.prop('init', child.expression, child.expression));
snippet_statements.push(...init);
} else {
nodes.push(child);
}
}

const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));

const boundary = b.stmt(
b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block))
);

context.state.template.push('<!>');
context.state.init.push(
snippet_statements.length > 0 ? b.block([...snippet_statements, boundary]) : boundary
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { SvelteSelf } from './visitors/SvelteSelf.js';
import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';

/** @type {Visitors} */
const global_visitors = {
Expand Down Expand Up @@ -75,7 +76,8 @@ const template_visitors = {
SvelteFragment,
SvelteHead,
SvelteSelf,
TitleElement
TitleElement,
SvelteBoundary
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../../internal/server/hydration.js';
import * as b from '../../../../utils/builders.js';

/**
* @param {AST.SvelteBoundary} node
* @param {ComponentContext} context
*/
export function SvelteBoundary(node, context) {
context.state.template.push(
b.literal(BLOCK_OPEN),
/** @type {BlockStatement} */ (context.visit(node.fragment)),
b.literal(BLOCK_CLOSE)
);
}
1 change: 1 addition & 0 deletions packages/svelte/src/compiler/phases/3-transform/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ export function clean_nodes(
parent.type === 'SnippetBlock' ||
parent.type === 'EachBlock' ||
parent.type === 'SvelteComponent' ||
parent.type === 'SvelteBoundary' ||
parent.type === 'Component' ||
parent.type === 'SvelteSelf') &&
first &&
Expand Down
8 changes: 7 additions & 1 deletion packages/svelte/src/compiler/types/template.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,11 @@ export namespace AST {
name: 'svelte:fragment';
}

export interface SvelteBoundary extends BaseElement {
type: 'SvelteBoundary';
name: 'svelte:boundary';
}

export interface SvelteHead extends BaseElement {
type: 'SvelteHead';
name: 'svelte:head';
Expand Down Expand Up @@ -499,7 +504,8 @@ export type ElementLike =
| AST.SvelteHead
| AST.SvelteOptionsRaw
| AST.SvelteSelf
| AST.SvelteWindow;
| AST.SvelteWindow
| AST.SvelteBoundary;

export type TemplateNode =
| AST.Root
Expand Down
27 changes: 14 additions & 13 deletions packages/svelte/src/internal/client/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@ export const RENDER_EFFECT = 1 << 3;
export const BLOCK_EFFECT = 1 << 4;
export const BRANCH_EFFECT = 1 << 5;
export const ROOT_EFFECT = 1 << 6;
export const UNOWNED = 1 << 7;
export const DISCONNECTED = 1 << 8;
export const CLEAN = 1 << 9;
export const DIRTY = 1 << 10;
export const MAYBE_DIRTY = 1 << 11;
export const INERT = 1 << 12;
export const DESTROYED = 1 << 13;
export const EFFECT_RAN = 1 << 14;
export const BOUNDARY_EFFECT = 1 << 7;
export const UNOWNED = 1 << 8;
export const DISCONNECTED = 1 << 9;
export const CLEAN = 1 << 10;
export const DIRTY = 1 << 11;
export const MAYBE_DIRTY = 1 << 12;
export const INERT = 1 << 13;
export const DESTROYED = 1 << 14;
export const EFFECT_RAN = 1 << 15;
/** 'Transparent' effects do not create a transition boundary */
export const EFFECT_TRANSPARENT = 1 << 15;
export const EFFECT_TRANSPARENT = 1 << 16;
/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */
export const LEGACY_DERIVED_PROP = 1 << 16;
export const INSPECT_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18;
export const EFFECT_HAS_DERIVED = 1 << 19;
export const LEGACY_DERIVED_PROP = 1 << 17;
export const INSPECT_EFFECT = 1 << 18;
export const HEAD_EFFECT = 1 << 19;
export const EFFECT_HAS_DERIVED = 1 << 20;

export const STATE_SYMBOL = Symbol('$state');
export const STATE_SYMBOL_METADATA = Symbol('$state metadata');
Expand Down
Loading