Skip to content

Commit 27eb91b

Browse files
authored
fix: dynamic namespace fixes and enhancements (#11219)
* fix: fall back to component namespace when not statically determinable In #10006 we added more elaborate mechanisms to determine which namespace a given element is in. For `<svelte:element>` we added a "can't know at compile time" case and introduced a limited heuristic into the runtime. This doesn't work for a few reasons: - we're checking the parent's namespace to determine the current namespace, but the element itself could be the one that _changes_ the namespace - as mentioned in the previous comment already, on the first render we can't do any parent analysis - it does not take into account the static component namespace The last point is the crucial one: In Svelte 4, we're falling back to the component namespace if we can't know statically - e.g. if someone added `<svelte:options namespace="svg">` then `<svelte:element>` should fall back to that namespace instead. We were not doing that up until now, which introduced a regression. Fixing this also means getting rid of the (flawed) "can't know statically" heuristic. Fixes #10858, though for a complete solution we likely need some way to tell `<svelte:element>` the namespace at runtime through a special attribute. Maybe we can use `xmlns` for that like we do in the static case * support dynamic svelte:element namespace through xmlns attribute * fix
1 parent 43d13e9 commit 27eb91b

File tree

12 files changed

+85
-38
lines changed

12 files changed

+85
-38
lines changed

.changeset/hip-pumpkins-boil.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: fall back to component namespace when not statically determinable, add way to tell `<svelte:element>` the namespace at runtime

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1341,7 +1341,8 @@ const common_visitors = {
13411341
ancestor.type === 'SvelteFragment' ||
13421342
ancestor.type === 'SnippetBlock'
13431343
) {
1344-
// Inside a slot or a snippet -> this resets the namespace, so we can't determine it
1344+
// Inside a slot or a snippet -> this resets the namespace, so assume the component namespace
1345+
node.metadata.svg = context.state.options.namespace === 'svg';
13451346
return;
13461347
}
13471348
if (ancestor.type === 'SvelteElement' || ancestor.type === 'RegularElement') {

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { global_visitors } from './visitors/global.js';
77
import { javascript_visitors } from './visitors/javascript.js';
88
import { javascript_visitors_runes } from './visitors/javascript-runes.js';
99
import { javascript_visitors_legacy } from './visitors/javascript-legacy.js';
10-
import { is_state_source, serialize_get_binding } from './utils.js';
10+
import { serialize_get_binding } from './utils.js';
1111
import { render_stylesheet } from '../css/index.js';
1212

1313
/**

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2008,6 +2008,9 @@ export const template_visitors = {
20082008
/** @type {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} */
20092009
const attributes = [];
20102010

2011+
/** @type {import('#compiler').Attribute['value'] | undefined} */
2012+
let dynamic_namespace = undefined;
2013+
20112014
/** @type {import('#compiler').ClassDirective[]} */
20122015
const class_directives = [];
20132016

@@ -2036,6 +2039,9 @@ export const template_visitors = {
20362039

20372040
for (const attribute of node.attributes) {
20382041
if (attribute.type === 'Attribute') {
2042+
if (attribute.name === 'xmlns' && !is_text_attribute(attribute)) {
2043+
dynamic_namespace = attribute.value;
2044+
}
20392045
attributes.push(attribute);
20402046
} else if (attribute.type === 'SpreadAttribute') {
20412047
attributes.push(attribute);
@@ -2090,23 +2096,16 @@ export const template_visitors = {
20902096
}
20912097
})
20922098
);
2093-
context.state.init.push(
2094-
b.stmt(
2095-
b.call(
2096-
'$.element',
2097-
context.state.node,
2098-
get_tag,
2099-
node.metadata.svg === true
2100-
? b.true
2101-
: node.metadata.svg === false
2102-
? b.false
2103-
: b.literal(null),
2104-
inner.length === 0
2105-
? /** @type {any} */ (undefined)
2106-
: b.arrow([element_id, b.id('$$anchor')], b.block(inner))
2107-
)
2108-
)
2109-
);
2099+
2100+
const args = [context.state.node, get_tag, node.metadata.svg ? b.true : b.false];
2101+
if (inner.length > 0) {
2102+
args.push(b.arrow([element_id, b.id('$$anchor')], b.block(inner)));
2103+
}
2104+
if (dynamic_namespace) {
2105+
if (inner.length === 0) args.push(b.id('undefined'));
2106+
args.push(b.thunk(serialize_attribute_value(dynamic_namespace, context)[1]));
2107+
}
2108+
context.state.init.push(b.stmt(b.call('$.element', ...args)));
21102109
},
21112110
EachBlock(node, context) {
21122111
const each_node_meta = node.metadata;

packages/svelte/src/compiler/types/template.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,10 @@ export interface SvelteElement extends BaseElement {
316316
tag: Expression;
317317
metadata: {
318318
/**
319-
* `true`/`false` if this is definitely (not) an svg element.
320-
* `null` means we can't know statically.
319+
* `true` if this is an svg element. The boolean may not be accurate because
320+
* the tag is dynamic, but we do our best to infer it from the template.
321321
*/
322-
svg: boolean | null;
322+
svg: boolean;
323323
scoped: boolean;
324324
};
325325
}

packages/svelte/src/internal/client/dom/blocks/svelte-element.js

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@ function swap_block_dom(effect, from, to) {
3939
/**
4040
* @param {Comment} anchor
4141
* @param {() => string} get_tag
42-
* @param {boolean | null} is_svg `null` == not statically known
43-
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn
42+
* @param {boolean} is_svg
43+
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn,
44+
* @param {undefined | (() => string)} get_namespace
4445
* @returns {void}
4546
*/
46-
export function element(anchor, get_tag, is_svg, render_fn) {
47+
export function element(anchor, get_tag, is_svg, render_fn, get_namespace) {
4748
const parent_effect = /** @type {import('#client').Effect} */ (current_effect);
4849

4950
render_effect(() => {
@@ -68,22 +69,18 @@ export function element(anchor, get_tag, is_svg, render_fn) {
6869

6970
block(() => {
7071
const next_tag = get_tag() || null;
72+
const ns = get_namespace
73+
? get_namespace()
74+
: is_svg || next_tag === 'svg'
75+
? namespace_svg
76+
: null;
77+
// Assumption: Noone changes the namespace but not the tag (what would that even mean?)
7178
if (next_tag === tag) return;
7279

7380
// See explanation of `each_item_block` above
7481
var previous_each_item = current_each_item;
7582
set_current_each_item(each_item_block);
7683

77-
// We try our best infering the namespace in case it's not possible to determine statically,
78-
// but on the first render on the client (without hydration) the parent will be undefined,
79-
// since the anchor is not attached to its parent / the dom yet.
80-
const ns =
81-
is_svg || next_tag === 'svg'
82-
? namespace_svg
83-
: is_svg === false || anchor.parentElement?.tagName === 'foreignObject'
84-
? null
85-
: anchor.parentElement?.namespaceURI ?? null;
86-
8784
if (effect) {
8885
if (next_tag === null) {
8986
// start outro
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
test({ assert, target }) {
5+
const path = target.querySelector('path');
6+
7+
assert.equal(path?.namespaceURI, 'http://www.w3.org/2000/svg');
8+
}
9+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<svelte:options namespace="svg" />
2+
3+
<script>
4+
import Svg from "./svg.svelte";
5+
6+
let tag = "path";
7+
</script>
8+
9+
<Svg>
10+
<svelte:element this="{tag}" d="M21 12a9 9 0 1 1-6.219-8.56"/>
11+
</Svg>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<svg><slot></slot></svg>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
async test({ assert, target }) {
5+
assert.equal(target.querySelector('path')?.namespaceURI, 'http://www.w3.org/2000/svg');
6+
7+
await target.querySelector('button')?.click();
8+
assert.equal(target.querySelector('div')?.namespaceURI, 'http://www.w3.org/1999/xhtml');
9+
}
10+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script>
2+
let tag = $state('path');
3+
let xmlns = $state('http://www.w3.org/2000/svg');
4+
</script>
5+
6+
<button onclick={() => {
7+
tag = 'div';
8+
xmlns = null;
9+
}}>change</button>
10+
11+
<!-- wrapper necessary or else jsdom says this is always an xhtml namespace -->
12+
<svg>
13+
<svelte:element this={tag} xmlns={xmlns} />
14+
</svg>

packages/svelte/types/index.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,10 +1539,10 @@ declare module 'svelte/compiler' {
15391539
tag: Expression;
15401540
metadata: {
15411541
/**
1542-
* `true`/`false` if this is definitely (not) an svg element.
1543-
* `null` means we can't know statically.
1542+
* `true` if this is an svg element. The boolean may not be accurate because
1543+
* the tag is dynamic, but we do our best to infer it from the template.
15441544
*/
1545-
svg: boolean | null;
1545+
svg: boolean;
15461546
scoped: boolean;
15471547
};
15481548
}

0 commit comments

Comments
 (0)