Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
173 commits
Select commit Hold shift + click to select a range
a03b3f3
feat: custom renderers api
paoloricciuti Mar 17, 2026
f35e12c
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Mar 18, 2026
203d22b
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Mar 20, 2026
23e52db
chore: better types
paoloricciuti Mar 27, 2026
2145510
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Mar 27, 2026
101d1ce
chore: better types and more complete interface
paoloricciuti Mar 28, 2026
ac40179
chore: remove getter
paoloricciuti Mar 28, 2026
0a0212c
chore: new operations functions to replace direct DOM access
paoloricciuti Mar 28, 2026
4eb2c0b
fix: import correctly
paoloricciuti Mar 28, 2026
6d13e5a
fix: store current renderer in effect and push it before executing it
paoloricciuti Mar 28, 2026
a59efbf
fix: wrap component in boundary in `render`
paoloricciuti Mar 30, 2026
432c023
fix: push and pop renderer when needed
paoloricciuti Mar 30, 2026
9ae3faa
chore: centralize methods in `operations.js`
paoloricciuti Mar 30, 2026
c459824
fix: don't clone node if a renderer is available
paoloricciuti Mar 30, 2026
cf874c0
fix: generate types
paoloricciuti Mar 30, 2026
bd70ab6
fix: events dom access
paoloricciuti Mar 30, 2026
49cff54
chore: initial test suite
paoloricciuti Mar 30, 2026
ecffd81
fix: types
paoloricciuti Mar 30, 2026
e86da84
fix: handle compiled `nodeValue`/`textContent`
paoloricciuti Mar 30, 2026
f828ea3
chore: better test renderer
paoloricciuti Mar 30, 2026
0f44733
chore: boundary test
paoloricciuti Mar 30, 2026
beddf45
chore: render into a fragment in tests
paoloricciuti Mar 30, 2026
5456125
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Mar 30, 2026
7dca985
chore: add each reactive and key tests
paoloricciuti Mar 30, 2026
37bd61d
fix: attributes dom access
paoloricciuti Mar 30, 2026
6830052
fix: style and class special case
paoloricciuti Mar 30, 2026
a0d0f01
fix: un-special case customizable-select
paoloricciuti Mar 30, 2026
2dc8fd1
chore: add full object in output
paoloricciuti Mar 30, 2026
ff5e835
fix: handle autofocus and muted
paoloricciuti Mar 30, 2026
f91c2fd
fix: use operations functions everywhere
paoloricciuti Mar 30, 2026
b4cf3d0
chore: move option under experimental
paoloricciuti Mar 31, 2026
c189bdc
chore: make option accept a function
paoloricciuti Mar 31, 2026
100fbbf
fix: conditional invocation
paoloricciuti Mar 31, 2026
098cdeb
fix: disallow css injected with custom renderer
paoloricciuti Mar 31, 2026
df42e4b
fix: add validations for `svelte:dom` elements
paoloricciuti Mar 31, 2026
9167c8d
fix: disallow `bind` and `transitions`
paoloricciuti Mar 31, 2026
2242004
chore: cleanup
paoloricciuti Mar 31, 2026
38dc4ae
chore: cleanup
paoloricciuti Mar 31, 2026
1886dc9
chore: cleanup
paoloricciuti Mar 31, 2026
c322fdf
fix: `validate_snippet_arguments`
paoloricciuti Mar 31, 2026
13ae09b
fix: throw descriptive error if using `createRawSnippet` using custom…
paoloricciuti Mar 31, 2026
8270a2f
fix: make sure returned elements are not primitives
paoloricciuti Mar 31, 2026
a0b60cb
fix: handle context withing `render`
paoloricciuti Mar 31, 2026
61ea8e9
chore: cleanup
paoloricciuti Mar 31, 2026
206bdf4
fix: allow usage of `$props.id`
paoloricciuti Mar 31, 2026
1270d5d
chore: cleanup
paoloricciuti Mar 31, 2026
c573720
chore: better types
paoloricciuti Mar 31, 2026
700454d
fix: lint
paoloricciuti Mar 31, 2026
a0650d0
fix: events don't go through propagation
paoloricciuti Mar 31, 2026
e2cedcd
fix: don't lowercase attributes with custom renderers
paoloricciuti Mar 31, 2026
e0a5e4f
fix: pass all the args to custom renderer events
paoloricciuti Mar 31, 2026
d9a5f16
fix: import
paoloricciuti Apr 1, 2026
65b23d1
fix: options in migrate
paoloricciuti Apr 1, 2026
63d8744
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 1, 2026
02d23d1
fix: class toggle
paoloricciuti Apr 1, 2026
8c70351
chore: changeset
paoloricciuti Apr 1, 2026
cdc57e0
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 1, 2026
81bf921
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 2, 2026
5c0e54e
fix: skip microtask in custom renderer `create_event`
paoloricciuti Apr 2, 2026
9ccd0b4
fix: cleanup in `finally`
paoloricciuti Apr 2, 2026
020dc9e
fix: disable `@html` in custom renderer
paoloricciuti Apr 2, 2026
37bf445
fix: event listeners in spread
paoloricciuti Apr 2, 2026
8f8ac03
fix: special value handling
paoloricciuti Apr 2, 2026
84ee6a0
fix: don't emit `selectedcontent` in custom render mode
paoloricciuti Apr 2, 2026
184eaef
fix: allow for an offscreen_anchor per renderer
paoloricciuti Apr 2, 2026
49511b4
fix: `defaultValue`/`defaultChecked` in spread attributes
paoloricciuti Apr 2, 2026
62d1ce7
fix: default to empty props if props is undefined in render
paoloricciuti Apr 2, 2026
8a8b779
fix: remove anchor from target on `unmount`
paoloricciuti Apr 2, 2026
9c1d17e
fix: handle autofocus in `set_attributes`
paoloricciuti Apr 2, 2026
f68d6af
fix: handle template as a normal tag
paoloricciuti Apr 2, 2026
28e70f8
fix: allow one "window" for each renderer
paoloricciuti Apr 2, 2026
ce3b8ee
fix: explicit `is_html` false if render is not null
paoloricciuti Apr 2, 2026
8a82750
chore: left TODO for style manipulation
paoloricciuti Apr 2, 2026
ac62333
fix: return component exports from `render`
paoloricciuti Apr 2, 2026
11c8787
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 2, 2026
da9efc5
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 2, 2026
7f9021a
chore: move init operations into separate module
paoloricciuti Apr 2, 2026
ce4442d
fix: allow DOM components to be mounted into custom renderers (requir…
paoloricciuti Apr 2, 2026
7d37884
fix: throw error if snippet is rendered in renderer different from co…
paoloricciuti Apr 2, 2026
0031d8f
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 2, 2026
883611f
chore: remove duplicate error
paoloricciuti Apr 2, 2026
801c8ef
fix: don't emit HTML warnings in custom renderer compile
paoloricciuti Apr 3, 2026
4440cec
chore: better types
paoloricciuti Apr 3, 2026
0b3d179
chore: better types
paoloricciuti Apr 3, 2026
b391869
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 5, 2026
d7e01b7
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 7, 2026
9272440
fix: push renderer at the very top of component
paoloricciuti Apr 7, 2026
7c74482
fix: push same returned renderer in `renderer.render`
paoloricciuti Apr 7, 2026
b4431e9
fix: disallow on directive with custom renderers
paoloricciuti Apr 7, 2026
c82987e
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Apr 9, 2026
b50bdde
fix: `HMR_ANCHOR` use operations methods
paoloricciuti Apr 9, 2026
20bd0cf
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 10, 2026
039c028
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 11, 2026
5c60dff
chore: rename `custom_renderer_imports`
paoloricciuti Apr 11, 2026
7012a4d
chore: revert custom element `shadow` types
paoloricciuti Apr 11, 2026
2a4af72
chore: move Renderer type to `.d.ts`
paoloricciuti Apr 11, 2026
a2ccdc4
chore: adhere to naming conventions
paoloricciuti Apr 11, 2026
8e6d68c
chore: move `custom_renderer` to `state.js`
paoloricciuti Apr 11, 2026
ba3dd57
fix: import
paoloricciuti Apr 11, 2026
0a5e7f1
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 14, 2026
e567368
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 14, 2026
ab4142e
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 15, 2026
d6aa022
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 17, 2026
d333a7f
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 20, 2026
596716b
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 28, 2026
b7179ab
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Apr 30, 2026
4b4a134
chore: rename `captured_renderer`
paoloricciuti Apr 30, 2026
2af595b
chore: replace custom `window` management with `globalThis`
paoloricciuti Apr 30, 2026
1e62a1c
fix: disclose version for custom renderers too
paoloricciuti Apr 30, 2026
2ee47cf
chore: fix ts
paoloricciuti Apr 30, 2026
6436bb4
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 1, 2026
3e657cd
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 5, 2026
c92989a
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 6, 2026
c75ebe8
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 7, 2026
8a2e058
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 12, 2026
aff1ed8
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 13, 2026
42fcdb9
tweak
Rich-Harris May 13, 2026
2c5b556
consistency
Rich-Harris May 13, 2026
6e38e1d
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 14, 2026
e2c81d6
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 14, 2026
66875cc
fix: lint
paoloricciuti May 16, 2026
8d5f108
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 19, 2026
0baca1b
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 19, 2026
0ead405
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 20, 2026
078a77c
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 20, 2026
d8f9488
chore: merge
paoloricciuti May 22, 2026
a829635
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 27, 2026
203fe2e
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 27, 2026
91563f0
Merge branch 'main' into svelte-custom-renderer
paoloricciuti May 28, 2026
4fb6b2f
fix: special case `init_operations` check-treeshakeability
paoloricciuti May 28, 2026
516973c
chore: replace custom renderer type arguments with a single one (#18244)
paoloricciuti May 28, 2026
41764ad
merge main
Rich-Harris May 28, 2026
1711d7b
fix: set hydrating to false in `push_renderer`
paoloricciuti May 29, 2026
585e444
fix: treeshake hydration
paoloricciuti May 30, 2026
dfd18af
chore: add comment
paoloricciuti May 30, 2026
c58c556
fix: noop on the server for custom renderer components
paoloricciuti Jun 1, 2026
a72c365
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Jun 1, 2026
f781fe4
chore: fix type
paoloricciuti Jun 1, 2026
131fae3
fix: update snapshot
paoloricciuti Jun 1, 2026
e5beb72
fix: push renderer in effects even if it's `null`
paoloricciuti Jun 1, 2026
56856f0
fix: `customRenderer` as a global option and remove `without_renderer…
paoloricciuti Jun 3, 2026
114be1c
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Jun 3, 2026
0490721
fix: lint and check
paoloricciuti Jun 3, 2026
ee2f74e
fix: lint
paoloricciuti Jun 3, 2026
cd5e2ee
fix: wrap implicit children into `renderer_snippet`
paoloricciuti Jun 4, 2026
f915aa3
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Jun 4, 2026
2084125
Unified `mount` API for components with custom renderers (#18317)
Rich-Harris Jun 8, 2026
4116e9f
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Jun 8, 2026
4873b80
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Jun 16, 2026
d39049e
fix: lint
paoloricciuti Jun 16, 2026
7c5b066
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Jun 29, 2026
39887f5
fix: update snapshot
paoloricciuti Jun 29, 2026
33304b4
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Jul 2, 2026
0c226fc
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Jul 6, 2026
ce6e71c
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Jul 13, 2026
2311e17
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Jul 14, 2026
216f258
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Jul 21, 2026
c22f2fa
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Jul 30, 2026
dd148eb
Merge branch 'main' into svelte-custom-renderer
paoloricciuti Aug 14, 2026
54135ab
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Aug 24, 2026
eb7532d
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Aug 24, 2026
af6419b
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Aug 27, 2026
d2ec9db
chore: update upstream fixes to be custom renderer aware
paoloricciuti Aug 27, 2026
0c11669
chore: fix snapshot
paoloricciuti Aug 27, 2026
e20068f
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Aug 28, 2026
128023f
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Aug 31, 2026
17e37a5
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Sep 3, 2026
9b97b7f
fix: try finally pop render
paoloricciuti Sep 5, 2026
1e5dc32
fix: better `customRenderer` validation
paoloricciuti Sep 5, 2026
be0640b
fix: styles with semicolons
paoloricciuti Sep 5, 2026
a180976
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Sep 5, 2026
a1d9bb6
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Sep 11, 2026
b3f3a77
Merge remote-tracking branch 'origin/main' into svelte-custom-renderer
paoloricciuti Sep 15, 2026
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/salty-steaks-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: custom renderer api
12 changes: 12 additions & 0 deletions documentation/docs/98-reference/.generated/client-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ Failed to hydrate the application
Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}`
```

### invalid_snippet_in_custom_renderer

```
`createRawSnippet` cannot be used with a custom renderer
```

### lifecycle_legacy_only

```
Expand All @@ -225,6 +231,12 @@ Rest element properties of `$props()` such as `%property%` are readonly
The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
```

### snippet_renderer_mismatch

```
A snippet created in a component with a custom renderer cannot be rendered by a different renderer
```

### state_descriptors_fixed

```
Expand Down
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 @@ -617,6 +617,12 @@ Cannot use `await` in deriveds and template expressions, or at the top level of
Imports of `svelte/internal/*` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from `svelte/internal/*` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case
```

### incompatible_with_custom_renderer

```
%message% is not compatible with `customRenderer`
```

### inspect_trace_generator

```
Expand Down Expand Up @@ -1125,6 +1131,12 @@ A component can only have one `<%name%>` element
Valid `<svelte:...>` tag names are %list%
```

### svelte_options_customrenderer_disabled

```
`customRenderer` cannot be set in `<svelte:options>` unless the `experimental.customRenderer` compiler option is enabled
```

### svelte_options_deprecated_tag

```
Expand Down
8 changes: 8 additions & 0 deletions packages/svelte/messages/client-errors/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ This can happen if you render a hydratable on the client that was not rendered o

> Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}`

## invalid_snippet_in_custom_renderer

> `createRawSnippet` cannot be used with a custom renderer

## lifecycle_legacy_only

> `%name%(...)` cannot be used in runes mode
Expand All @@ -171,6 +175,10 @@ This can happen if you render a hydratable on the client that was not rendered o

> The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files

## snippet_renderer_mismatch

> A snippet created in a component with a custom renderer cannot be rendered by a different renderer

## state_descriptors_fixed

> Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`.
Expand Down
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 @@ -271,6 +271,10 @@ The same applies to components:

> `<%name%>` does not support non-event attributes or spread attributes

## incompatible_with_custom_renderer

> %message% is not compatible with `customRenderer`

## js_parse_error

> %message%
Expand Down Expand Up @@ -425,6 +429,10 @@ HTML restricts where certain elements can appear. In case of a violation the bro

> Valid `<svelte:...>` tag names are %list%

## svelte_options_customrenderer_disabled

> `customRenderer` cannot be set in `<svelte:options>` unless the `experimental.customRenderer` compiler option is enabled

## svelte_options_deprecated_tag

> "tag" option is deprecated — use "customElement" instead
Expand Down
7 changes: 7 additions & 0 deletions packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"./internal/disclose-version": {
"default": "./src/internal/disclose-version.js"
},
"./internal/init-operations": {
"default": "./src/internal/init-operations.js"
},
"./internal/flags/async": {
"default": "./src/internal/flags/async.js"
},
Expand Down Expand Up @@ -91,6 +94,10 @@
"types": "./types/index.d.ts",
"default": "./src/reactivity/window/index.js"
},
"./renderer": {
"types": "./types/index.d.ts",
"default": "./src/renderer/index.js"
},
"./server": {
"types": "./types/index.d.ts",
"default": "./src/server/index.js"
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/renderer.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './types/index.js';
1 change: 1 addition & 0 deletions packages/svelte/scripts/check-treeshakeability.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ for (const key in pkg.exports) {
if (key === './internal/disclose-version') continue;
if (key === './internal/flags/legacy') continue;
if (key === './internal/flags/tracing') continue;
if (key === './internal/init-operations') continue;

for (const type of ['browser', 'default']) {
if (!pkg.exports[key][type]) continue;
Expand Down
12 changes: 11 additions & 1 deletion packages/svelte/scripts/generate-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@ const pkg = JSON.parse(fs.readFileSync(`${dir}/package.json`, 'utf-8'));

// For people not using moduleResolution: 'bundler', we need to generate these files. Think about removing this in Svelte 6 or 7
// It may look weird, but the imports MUST be ending with index.js to be properly resolved in all TS modes
for (const name of ['action', 'animate', 'easing', 'motion', 'store', 'transition', 'legacy']) {
for (const name of [
'action',
'animate',
'easing',
'motion',
'store',
'transition',
'legacy',
'renderer'
]) {
fs.writeFileSync(`${dir}/${name}.d.ts`, "import './types/index.js';\n");
}

Expand Down Expand Up @@ -44,6 +53,7 @@ await createBundle({
[`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`,
[`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`,
[`${pkg.name}/reactivity/window`]: `${dir}/src/reactivity/window/index.js`,
[`${pkg.name}/renderer`]: `${dir}/src/internal/client/custom-renderer/index.js`,
[`${pkg.name}/server`]: `${dir}/src/server/index.d.ts`,
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,
Expand Down
19 changes: 19 additions & 0 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,16 @@ export function illegal_element_attribute(node, name) {
e(node, 'illegal_element_attribute', `\`<${name}>\` does not support non-event attributes or spread attributes\nhttps://svelte.dev/e/illegal_element_attribute`);
}

/**
* %message% is not compatible with `customRenderer`
* @param {null | number | NodeLike} node
* @param {string} message
* @returns {never}
*/
export function incompatible_with_custom_renderer(node, message) {
e(node, 'incompatible_with_custom_renderer', `${message} is not compatible with \`customRenderer\`\nhttps://svelte.dev/e/incompatible_with_custom_renderer`);
}

/**
* %message%
* @param {null | number | NodeLike} node
Expand Down Expand Up @@ -1539,6 +1549,15 @@ export function svelte_meta_invalid_tag(node, list) {
e(node, 'svelte_meta_invalid_tag', `Valid \`<svelte:...>\` tag names are ${list}\nhttps://svelte.dev/e/svelte_meta_invalid_tag`);
}

/**
* `customRenderer` cannot be set in `<svelte:options>` unless the `experimental.customRenderer` compiler option is enabled
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function svelte_options_customrenderer_disabled(node) {
e(node, 'svelte_options_customrenderer_disabled', `\`customRenderer\` cannot be set in \`<svelte:options>\` unless the \`experimental.customRenderer\` compiler option is enabled\nhttps://svelte.dev/e/svelte_options_customrenderer_disabled`);
}

/**
* "tag" option is deprecated — use "customElement" instead
* @param {null | number | NodeLike} node
Expand Down
39 changes: 37 additions & 2 deletions packages/svelte/src/compiler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { analyze_component, analyze_module } from './phases/2-analyze/index.js';
import { transform_component, transform_module } from './phases/3-transform/index.js';
import { validate_component_options, validate_module_options } from './validate-options.js';
import * as state from './state.js';
import * as e from './errors.js';
export { default as preprocess } from './preprocess/index.js';
export { print } from './print/index.js';

Expand All @@ -28,15 +29,49 @@ export function compile(source, options) {

let parsed = _parse(source);

const { customElement: customElementOptions, ...parsed_options } = parsed.options || {};
const {
customElement: customElementOptions,
customRenderer: custom_renderer,
...parsed_options
} = parsed.options || {};

// resolve the per-component custom renderer, taking `<svelte:options customRenderer={...} />`
// into account. The normalized option is always a function returning `string | null | undefined`
// (see `validate-options.js`). A string opts in to a specific renderer module, `null`
// opts out to plain DOM (while keeping the feature enabled) and an absent option inherits whatever
// the global option resolves to.
let custom_renderer_option = validated.experimental.customRenderer;

if (custom_renderer !== undefined) {
// the feature is a global compiler option — a component can only override the renderer it uses
// (or opt out) when `experimental.customRenderer` is enabled. Otherwise nothing pushes a
// renderer, so allowing `<svelte:options customRenderer>` would silently do the wrong thing.
if (!options.experimental?.customRenderer && parsed.options?.attributes) {
for (const attribute of parsed.options.attributes) {
if (attribute.name === 'customRenderer') {
e.svelte_options_customrenderer_disabled(attribute);
}
}
}

if (typeof custom_renderer === 'string') {
custom_renderer_option = () => custom_renderer;
} else if (custom_renderer === null) {
custom_renderer_option = () => null;
}
}

/** @type {ValidatedCompileOptions} */
const combined_options = {
...validated,
...parsed_options,
customElementOptions,
css: 'css' in parsed_options ? () => parsed_options.css ?? 'external' : validated.css,
runes: 'runes' in parsed_options ? () => parsed_options.runes : validated.runes
runes: 'runes' in parsed_options ? () => parsed_options.runes : validated.runes,
experimental: {
...validated.experimental,
customRenderer: custom_renderer_option
}
};

if (parsed.metadata.ts) {
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte/src/compiler/migrate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ export function migrate(source, { filename, use_ts } = {}) {
css: 'css' in parsed_options ? () => parsed_options.css ?? 'external' : () => 'external',
runes: 'runes' in parsed_options ? () => parsed_options.runes : () => undefined,
experimental: {
async: true
async: true,
customRenderer: () => undefined
}
};

Expand Down
31 changes: 31 additions & 0 deletions packages/svelte/src/compiler/phases/1-parse/read/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ export default function read_options(node) {
e.svelte_options_deprecated_tag(attribute);
break; // eslint doesn't know this is unnecessary
}
case 'customRenderer': {
const { value } = attribute;
const chunk = Array.isArray(value) ? value[0] : value;

if (chunk === true || !chunk || (Array.isArray(value) && value.length !== 1)) {
e.svelte_options_invalid_attribute_value(attribute, 'a string or null');
}

if (chunk.type === 'Text') {
component_options.customRenderer = chunk.data;
} else if (
chunk.expression?.type === 'Literal' &&
(typeof chunk.expression.value === 'string' || chunk.expression.value === null)
) {
component_options.customRenderer = chunk.expression.value;
} else {
e.svelte_options_invalid_attribute_value(attribute, 'a string or null');
}
break;
}
case 'customElement': {
/** @type {AST.SvelteOptions['customElement']} */
const ce = {};
Expand Down Expand Up @@ -193,6 +213,17 @@ export default function read_options(node) {
}
}

if (
component_options.css === 'injected' &&
typeof component_options.customRenderer === 'string'
) {
// Find the css attribute node for the error position
const css_attribute = node.attributes.find(
(/** @type {any} */ a) => a.type === 'Attribute' && a.name === 'css'
);
e.incompatible_with_custom_renderer(css_attribute ?? node, "`css: 'injected'`");
}

return component_options;
}

Expand Down
11 changes: 10 additions & 1 deletion packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,14 @@ export function analyze_component(root, source, options) {

const custom_element_from_option = options.customElement({ filename: options.filename });
const css = options.css({ filename: options.filename });
const custom_renderer = options.experimental.customRenderer?.({ filename: options.filename });

// only an actual renderer module (a string) is incompatible with injected css — a `null`
// renderer means the component renders to the DOM, which is fine
if (css === 'injected' && typeof custom_renderer === 'string') {
e.incompatible_with_custom_renderer(null, "`css: 'injected'`");
}

const custom_element = options.customElementOptions ?? custom_element_from_option;
const is_custom_element = !!options.customElementOptions || custom_element_from_option;

Expand All @@ -479,7 +487,8 @@ export function analyze_component(root, source, options) {
component_name: name,
dev: options.dev,
rootDir: options.rootDir,
runes
runes,
custom_renderer: custom_renderer
});

// TODO remove all the ?? stuff, we don't need it now that we're validating the config
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
/** @import { Context } from '../types' */
/** @import { AST } from '#compiler'; */
import * as e from '../../../errors.js';
import { custom_renderer } from '../../../state.js';

/**
* @param {AST.AnimateDirective} node
* @param {Context} context
*/
export function AnimateDirective(node, context) {
if (custom_renderer) {
e.incompatible_with_custom_renderer(node, '`animate:`');
}

context.next({ ...context.state, expression: node.metadata.expression });

if (node.metadata.expression.has_await) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/** @import { Context } from '../types' */
import { cannot_be_set_statically, can_delegate_event } from '../../../../utils.js';
import { get_attribute_chunks, is_event_attribute } from '../../../utils/ast.js';
import { custom_renderer } from '../../../state.js';
import { mark_subtree_dynamic } from './shared/fragment.js';

/**
Expand Down Expand Up @@ -59,8 +60,11 @@ export function Attribute(node, context) {
context.state.analysis.uses_event_attributes = true;
}

node.metadata.delegated =
parent?.type === 'RegularElement' && can_delegate_event(node.name.slice(2));
// we can't delegate event handlers in a non dom environment
if (!custom_renderer) {
node.metadata.delegated =
parent?.type === 'RegularElement' && can_delegate_event(node.name.slice(2));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { binding_properties } from '../../bindings.js';
import fuzzymatch from '../../../utils/fuzzymatch.js';
import { is_content_editable_binding, is_svg } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { custom_renderer } from '../../../state.js';

/**
* @param {AST.BindDirective} node
Expand All @@ -27,6 +28,9 @@ export function BindDirective(node, context) {
parent?.type === 'SvelteDocument' ||
parent?.type === 'SvelteBody'
) {
if (custom_renderer) {
e.incompatible_with_custom_renderer(node, '`bind:`');
}
if (node.name in binding_properties) {
const property = binding_properties[node.name];
if (property.valid_elements && !property.valid_elements.includes(parent.name)) {
Expand Down
Loading