Skip to content

Commit 0fd2b32

Browse files
feat: compiler-based declarative shadow DOM support via ShadowRoot wire opcode
Co-authored-by: NullVoxPopuli <[email protected]> Agent-Logs-Url: https://github.com/emberjs/ember.js/sessions/c72c698a-f4d5-4118-b66f-d9641f62c001
1 parent de3e085 commit 0fd2b32

13 files changed

Lines changed: 154 additions & 106 deletions

File tree

packages/@glimmer/compiler/lib/passes/1-normalization/visitors/statements.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,32 @@ class NormalizationStatements {
8686
}
8787

8888
SimpleElement(element: ASTv2.SimpleElement, state: NormalizationState): Result<mir.Statement> {
89+
// Detect declarative shadow DOM: <template shadowrootmode="open|closed">
90+
// When Glimmer builds the DOM via createElement rather than parsing HTML, the browser's native
91+
// declarative shadow DOM processing never fires. We handle it explicitly with a dedicated opcode.
92+
if (element.tag.chars === 'template') {
93+
const shadowrootmodeAttr = element.attrs.find(
94+
(attr): attr is ASTv2.HtmlAttr =>
95+
attr.type === 'HtmlAttr' && attr.name.chars === 'shadowrootmode'
96+
);
97+
98+
if (shadowrootmodeAttr && ASTv2.isLiteral(shadowrootmodeAttr.value, 'string')) {
99+
const mode = shadowrootmodeAttr.value.value as string;
100+
101+
if (mode === 'open' || mode === 'closed') {
102+
return this.visitList(element.body, state).mapOk(
103+
(body) =>
104+
new mir.DeclarativeShadowRoot({
105+
loc: element.loc,
106+
guid: state.generateUniqueCursor(),
107+
mode,
108+
body: body.toArray(),
109+
})
110+
);
111+
}
112+
}
113+
}
114+
89115
return new ClassifiedElement(
90116
element,
91117
new ClassifiedSimpleElement(element.tag, element, hasDynamicFeatures(element)),

packages/@glimmer/compiler/lib/passes/1-normalization/visitors/strict-mode.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ export default class StrictModeValidationPass {
100100

101101
case 'InvokeComponent':
102102
return this.InvokeComponent(statement);
103+
104+
case 'DeclarativeShadowRoot':
105+
return this.DeclarativeShadowRoot(statement);
103106
}
104107
}
105108

@@ -268,6 +271,10 @@ export default class StrictModeValidationPass {
268271
return this.ElementParameters(statement.params).andThen(() => this.Statements(statement.body));
269272
}
270273

274+
DeclarativeShadowRoot(statement: mir.DeclarativeShadowRoot): Result<null> {
275+
return this.Statements(statement.body);
276+
}
277+
271278
InvokeBlock(statement: mir.InvokeBlock): Result<null> {
272279
return this.Expression(statement.head, statement.head, COMPONENT_RESOLUTION)
273280
.andThen(() => this.Args(statement.args))

packages/@glimmer/compiler/lib/passes/2-encoding/content.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export class ContentEncoder {
6969
return this.Component(stmt);
7070
case 'SimpleElement':
7171
return this.SimpleElement(stmt);
72+
case 'DeclarativeShadowRoot':
73+
return this.DeclarativeShadowRoot(stmt);
7274
case 'InElement':
7375
return this.InElement(stmt);
7476
case 'InvokeBlock':
@@ -137,6 +139,14 @@ export class ContentEncoder {
137139
]);
138140
}
139141

142+
DeclarativeShadowRoot({
143+
guid,
144+
mode,
145+
body,
146+
}: mir.DeclarativeShadowRoot): WireFormat.Statements.ShadowRoot {
147+
return [SexpOpcodes.ShadowRoot, [CONTENT.list(body), []], guid, mode];
148+
}
149+
140150
Component({ tag, params, args, blocks }: mir.Component): WireFormat.Statements.Component {
141151
let wireTag = EXPR.expr(tag);
142152
let wirePositional = CONTENT.ElementParameters(params);

packages/@glimmer/compiler/lib/passes/2-encoding/mir.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ export class SimpleElement extends node('SimpleElement').fields<{
120120
dynamicFeatures: boolean;
121121
}>() {}
122122

123+
export class DeclarativeShadowRoot extends node('DeclarativeShadowRoot').fields<{
124+
guid: string;
125+
mode: string;
126+
body: Statement[];
127+
}>() {}
128+
123129
export class ElementParameters extends node('ElementParameters').fields<{
124130
body: AnyOptionalList<ElementParameter>;
125131
}>() {}
@@ -214,6 +220,7 @@ export type Statement =
214220
| AppendTextNode
215221
| Component
216222
| SimpleElement
223+
| DeclarativeShadowRoot
217224
| InvokeBlock
218225
| AppendComment
219226
| If

packages/@glimmer/compiler/lib/wire-format-debug.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ export default class WireFormatDebugger {
5656
opcode[3] ? this.formatOpcode(opcode[3]) : undefined,
5757
];
5858

59+
case Op.ShadowRoot:
60+
return ['shadow-root', opcode[3], opcode[2]];
61+
5962
case Op.OpenElement:
6063
return ['open-element', inflateTagName(opcode[1])];
6164

packages/@glimmer/constants/lib/syscall-ops.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import type {
5858
VmNot,
5959
VmOp,
6060
VmOpenDynamicElement,
61+
VmAttachShadowRoot,
6162
VmOpenElement,
6263
VmPop,
6364
VmPopArgs,
@@ -187,7 +188,8 @@ export const VM_IF_INLINE_OP = 109 satisfies VmIfInline;
187188
export const VM_NOT_OP = 110 satisfies VmNot;
188189
export const VM_GET_DYNAMIC_VAR_OP = 111 satisfies VmGetDynamicVar;
189190
export const VM_LOG_OP = 112 satisfies VmLog;
190-
export const VM_SYSCALL_SIZE = 113 satisfies VmSize;
191+
export const VM_ATTACH_SHADOW_ROOT_OP = 113 satisfies VmAttachShadowRoot;
192+
export const VM_SYSCALL_SIZE = 114 satisfies VmSize;
191193

192194
export function isOp(value: number): value is VmOp {
193195
return value >= 16;

packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import type {
4747
UndefinedOpcode,
4848
WithDynamicVarsOpcode,
4949
YieldOpcode,
50+
ShadowRootOpcode,
5051
} from './opcodes.js';
5152

5253
export type * from './opcodes.js';
@@ -300,6 +301,13 @@ export namespace Statements {
300301
blocks: Blocks | null,
301302
];
302303

304+
export type ShadowRoot = [
305+
op: ShadowRootOpcode,
306+
block: SerializedInlineBlock,
307+
guid: string,
308+
mode: string,
309+
];
310+
303311
/**
304312
* A Handlebars statement
305313
*/
@@ -325,7 +333,8 @@ export namespace Statements {
325333
| Each
326334
| Let
327335
| WithDynamicVars
328-
| InvokeComponent;
336+
| InvokeComponent
337+
| ShadowRoot;
329338

330339
export type Attribute =
331340
| StaticAttr

packages/@glimmer/interfaces/lib/compile/wire-format/opcodes.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export type NotOpcode = 51;
6464
export type IfInlineOpcode = 52;
6565
export type GetDynamicVarOpcode = 53;
6666
export type LogOpcode = 54;
67+
export type ShadowRootOpcode = 55;
6768

6869
export type GetStartOpcode = GetSymbolOpcode;
6970
export type GetEndOpcode = GetFreeAsComponentHeadOpcode;

packages/@glimmer/interfaces/lib/vm-opcodes.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ export type VmIfInline = 109;
111111
export type VmNot = 110;
112112
export type VmGetDynamicVar = 111;
113113
export type VmLog = 112;
114-
export type VmSize = 113;
114+
export type VmAttachShadowRoot = 113;
115+
export type VmSize = 114;
115116

116117
export type VmOp =
117118
| VmHelper
@@ -206,6 +207,7 @@ export type VmOp =
206207
| VmIfInline
207208
| VmNot
208209
| VmGetDynamicVar
209-
| VmLog;
210+
| VmLog
211+
| VmAttachShadowRoot;
210212

211213
export type SomeVmOp = VmOp | VmMachineOp;

packages/@glimmer/opcode-compiler/lib/syntax/statements.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
WireFormat,
77
} from '@glimmer/interfaces';
88
import {
9+
VM_ATTACH_SHADOW_ROOT_OP,
910
VM_CLOSE_ELEMENT_OP,
1011
VM_COMMENT_OP,
1112
VM_COMPONENT_ATTR_OP,
@@ -291,6 +292,33 @@ STATEMENTS.add(SexpOpcodes.InElement, (op, [, block, guid, destination, insertBe
291292
);
292293
});
293294

295+
STATEMENTS.add(SexpOpcodes.ShadowRoot, (op, [, block, guid, mode]) => {
296+
// Push guid and insertBefore (always undefined for shadow roots) onto the stack,
297+
// then call VM_ATTACH_SHADOW_ROOT_OP which attaches a shadow root to the current
298+
// parent element and pushes a reference to it. Use ReplayableIf so that the whole
299+
// sequence is re-evaluated on update and so that SSR (where attachShadow is unavailable)
300+
// gracefully falls through without rendering into a shadow root.
301+
ReplayableIf(
302+
op,
303+
304+
() => {
305+
expr(op, guid);
306+
PushPrimitiveReference(op, undefined); // insertBefore
307+
PushPrimitiveReference(op, mode); // 'open' | 'closed'
308+
op(VM_ATTACH_SHADOW_ROOT_OP);
309+
op(VM_DUP_OP, $sp, 0);
310+
311+
return 4;
312+
},
313+
314+
() => {
315+
op(VM_PUSH_REMOTE_ELEMENT_OP);
316+
InvokeStaticBlock(op, block);
317+
op(VM_POP_REMOTE_ELEMENT_OP);
318+
}
319+
);
320+
});
321+
294322
STATEMENTS.add(SexpOpcodes.If, (op, [, condition, block, inverse]) =>
295323
ReplayableIf(
296324
op,

0 commit comments

Comments
 (0)