Skip to content

Commit ee2edae

Browse files
fix(ssr): correctly render foreign namespace element closing tags (#4992)
* fix: correctly render foreign element closing tags * fix: properly handle scoped class for void and foreign elements * fix: address review comments * fix: remove math example for now * fix: ignore UNKNOWN_HTML_TAG_IN_TEMPLATE warning to allow for MathML test * fix: update tag warning suppression to reference issue * fix: simpler MathML example
1 parent b8362b5 commit ee2edae

File tree

9 files changed

+90
-10
lines changed

9 files changed

+90
-10
lines changed

packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ async function compileFixture({
8080
// IGNORED_SLOT_ATTRIBUTE_IN_CHILD is fine; it is used in some of these tests
8181
message.includes('LWC1201') ||
8282
message.includes('-h-t-m-l') ||
83-
code === 'CIRCULAR_DEPENDENCY';
83+
code === 'CIRCULAR_DEPENDENCY' ||
84+
// TODO [#5010]: template-compiler -> index -> validateElement generates UNKNOWN_HTML_TAG_IN_TEMPLATE for MathML elements
85+
message.includes('LWC1123');
8486
if (!shouldIgnoreWarning) {
8587
throw new Error(message);
8688
}

packages/@lwc/engine-server/src/__tests__/fixtures/render-correct-tags/error.txt

Whitespace-only changes.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<x-elements class="lwc-4q9u0sqsfc0-host" data-lwc-host-scope-token="lwc-4q9u0sqsfc0-host">
2+
<template shadowrootmode="open">
3+
<style class="lwc-4q9u0sqsfc0" type="text/css">
4+
:host {background: blue;}
5+
</style>
6+
<a class="lwc-4q9u0sqsfc0" href="https://www.salesforce.com/">
7+
</a>
8+
<label class="lwc-4q9u0sqsfc0" for="textInput">
9+
Some input
10+
</label>
11+
<input class="lwc-4q9u0sqsfc0" id="textInput" type="text">
12+
<svg class="lwc-4q9u0sqsfc0">
13+
<pattern class="lwc-4q9u0sqsfc0">
14+
<image class="lwc-4q9u0sqsfc0" xlink:href="https://www.salesforce.com/"/>
15+
</pattern>
16+
<image class="lwc-4q9u0sqsfc0" xlink:title="title"/>
17+
</svg>
18+
<math class="lwc-4q9u0sqsfc0">
19+
<mrow class="lwc-4q9u0sqsfc0">
20+
<mi class="lwc-4q9u0sqsfc0">
21+
a
22+
</mi>
23+
<mo class="lwc-4q9u0sqsfc0">
24+
+
25+
</mo>
26+
<mi class="lwc-4q9u0sqsfc0">
27+
b
28+
</mi>
29+
</mrow>
30+
</math>
31+
<svg class="lwc-4q9u0sqsfc0">
32+
<pattern class="lwc-4q9u0sqsfc0">
33+
<image xlink:href="https://www.salesforce.com/">
34+
</image>
35+
</pattern>
36+
<image class="lwc-4q9u0sqsfc0" xlink:title="title"/>
37+
</svg>
38+
</template>
39+
</x-elements>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const tagName = 'x-elements';
2+
export { default } from 'x/elements';
3+
export * from 'x/elements';
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<template>
2+
<!-- Empty non-void HTML namespace element -->
3+
<a href="https://www.salesforce.com/"></a>
4+
<!-- Non-empty non-void HTML namespace element -->
5+
<label for="textInput">Some input</label>
6+
<!-- Void HTML namespace element -->
7+
<input id="textInput" type="text">
8+
<!-- Foreign namespace elements -->
9+
<svg>
10+
<!-- Non-empty foreign element -->
11+
<pattern>
12+
<image xlink:href="https://www.salesforce.com/"></image>
13+
</pattern>
14+
<!-- Empty foreign element -->
15+
<image xlink:title="title"></image>
16+
</svg>
17+
<math>
18+
<!-- TODO [#5015]: Add void example -->
19+
<mrow>
20+
<mi>a</mi>
21+
<mo>+</mo>
22+
<mi>b</mi>
23+
</mrow>
24+
</math>
25+
<!-- Rendered with inner-html -->
26+
<svg>
27+
<pattern lwc:inner-html={foreignNamespaceInnerHtml}></pattern>
28+
<image xlink:title="title"></image>
29+
</svg>
30+
</template>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { LightningElement, api } from 'lwc';
2+
3+
export default class extends LightningElement {
4+
@api foreignNamespaceInnerHtml = '<image xlink:href="https://www.salesforce.com/"></image>';
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:host {
2+
background: blue;
3+
}

packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export const expectedFailures = new Set([
1414
'attribute-component-global-html/index.js',
1515
'attribute-global-html/as-component-prop/undeclared/index.js',
1616
'attribute-global-html/as-component-prop/without-@api/index.js',
17-
'attribute-namespace/index.js',
1817
'attribute-style/basic/index.js',
1918
'attribute-style/dynamic/index.js',
2019
'exports/component-as-default/index.js',
@@ -32,7 +31,6 @@ export const expectedFailures = new Set([
3231
'superclass/render-in-superclass/no-template-in-subclass/index.js',
3332
'superclass/render-in-superclass/unused-default-in-subclass/index.js',
3433
'superclass/render-in-superclass/unused-default-in-superclass/index.js',
35-
'svgs/index.js',
3634
'wire/errors/throws-on-computed-key/index.js',
3735
'wire/errors/throws-when-colliding-prop-then-method/index.js',
3836
'wire/errors/throws-when-computed-prop-is-expression/index.js',

packages/@lwc/ssr-compiler/src/compile-template/transformers/element.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,6 @@ export const Element: Transformer<IrElement | IrExternalComponent | IrSlot> = fu
227227
return result;
228228
});
229229

230-
if (isVoidElement(node.name, HTML_NAMESPACE)) {
231-
return [bYield(b.literal(`<${node.name}`)), ...yieldAttrsAndProps, bYield(b.literal(`>`))];
232-
}
233-
234230
let childContent: EsStatement[];
235231
// An element can have children or lwc:inner-html, but not both
236232
// If it has both, the template compiler will throw an error before reaching here
@@ -246,13 +242,17 @@ export const Element: Transformer<IrElement | IrExternalComponent | IrSlot> = fu
246242
childContent = [];
247243
}
248244

245+
const isForeignSelfClosingElement =
246+
node.namespace !== HTML_NAMESPACE && childContent.length === 0;
247+
const isSelfClosingElement =
248+
isVoidElement(node.name, HTML_NAMESPACE) || isForeignSelfClosingElement;
249+
249250
return [
250251
bYield(b.literal(`<${node.name}`)),
251252
// If we haven't already prefixed the scope token to an existing class, add an explicit class here
252253
...(hasClassAttribute ? [] : [bConditionallyYieldScopeTokenClass()]),
253254
...yieldAttrsAndProps,
254-
bYield(b.literal(`>`)),
255-
...childContent,
256-
bYield(b.literal(`</${node.name}>`)),
255+
bYield(b.literal(isForeignSelfClosingElement ? `/>` : `>`)),
256+
...(isSelfClosingElement ? [] : [...childContent, bYield(b.literal(`</${node.name}>`))]),
257257
].filter(Boolean);
258258
};

0 commit comments

Comments
 (0)