@frontiers-labs/argon 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/dist/annotations.d.ts +23 -0
  4. package/dist/annotations.d.ts.map +1 -0
  5. package/dist/annotations.js +23 -0
  6. package/dist/annotations.js.map +1 -0
  7. package/dist/codegen/client.d.ts +14 -0
  8. package/dist/codegen/client.d.ts.map +1 -0
  9. package/dist/codegen/client.js +964 -0
  10. package/dist/codegen/client.js.map +1 -0
  11. package/dist/codegen/dts.d.ts +3 -0
  12. package/dist/codegen/dts.d.ts.map +1 -0
  13. package/dist/codegen/dts.js +82 -0
  14. package/dist/codegen/dts.js.map +1 -0
  15. package/dist/codegen/js.d.ts +3 -0
  16. package/dist/codegen/js.d.ts.map +1 -0
  17. package/dist/codegen/js.js +278 -0
  18. package/dist/codegen/js.js.map +1 -0
  19. package/dist/codegen/rust.d.ts +6 -0
  20. package/dist/codegen/rust.d.ts.map +1 -0
  21. package/dist/codegen/rust.js +1076 -0
  22. package/dist/codegen/rust.js.map +1 -0
  23. package/dist/component.d.ts +35 -0
  24. package/dist/component.d.ts.map +1 -0
  25. package/dist/component.js +120 -0
  26. package/dist/component.js.map +1 -0
  27. package/dist/diagnostics.d.ts +20 -0
  28. package/dist/diagnostics.d.ts.map +1 -0
  29. package/dist/diagnostics.js +40 -0
  30. package/dist/diagnostics.js.map +1 -0
  31. package/dist/docs.d.ts +3 -0
  32. package/dist/docs.d.ts.map +1 -0
  33. package/dist/docs.js +42 -0
  34. package/dist/docs.js.map +1 -0
  35. package/dist/error-catalog.d.ts +11 -0
  36. package/dist/error-catalog.d.ts.map +1 -0
  37. package/dist/error-catalog.js +92 -0
  38. package/dist/error-catalog.js.map +1 -0
  39. package/dist/html-attrs.d.ts +2 -0
  40. package/dist/html-attrs.d.ts.map +1 -0
  41. package/dist/html-attrs.js +37 -0
  42. package/dist/html-attrs.js.map +1 -0
  43. package/dist/html.d.ts +41 -0
  44. package/dist/html.d.ts.map +1 -0
  45. package/dist/html.js +188 -0
  46. package/dist/html.js.map +1 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +139 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/ir.d.ts +321 -0
  52. package/dist/ir.d.ts.map +1 -0
  53. package/dist/ir.js +243 -0
  54. package/dist/ir.js.map +1 -0
  55. package/dist/jsx.d.ts +31 -0
  56. package/dist/jsx.d.ts.map +1 -0
  57. package/dist/jsx.js +242 -0
  58. package/dist/jsx.js.map +1 -0
  59. package/dist/lower.d.ts +6 -0
  60. package/dist/lower.d.ts.map +1 -0
  61. package/dist/lower.js +1341 -0
  62. package/dist/lower.js.map +1 -0
  63. package/dist/parser.d.ts +3 -0
  64. package/dist/parser.d.ts.map +1 -0
  65. package/dist/parser.js +714 -0
  66. package/dist/parser.js.map +1 -0
  67. package/dist/ssr-subset.d.ts +29 -0
  68. package/dist/ssr-subset.d.ts.map +1 -0
  69. package/dist/ssr-subset.js +119 -0
  70. package/dist/ssr-subset.js.map +1 -0
  71. package/dist/template.d.ts +32 -0
  72. package/dist/template.d.ts.map +1 -0
  73. package/dist/template.js +90 -0
  74. package/dist/template.js.map +1 -0
  75. package/package.json +67 -0
@@ -0,0 +1,964 @@
1
+ // CSR backend: ModuleIR → a self-contained ES module defining custom elements.
2
+ //
3
+ // Components compile to classes; everything else in the source module is kept
4
+ // verbatim so helper functions and constants survive untouched. Binding
5
+ // expressions are printed from the IR; opaque code (event handlers, CSR-only
6
+ // values) passes through a reference-rewriting transform instead.
7
+ import * as ts from "typescript";
8
+ import { attrPositions, bindingDependencies, isMarkup, localDependencies, markupValue, referencedNames, visitExpr, } from "../ir.js";
9
+ import { ArgonCompileError } from "../diagnostics.js";
10
+ import { MARKER_RE } from "../template.js";
11
+ import { serialize } from "../html.js";
12
+ import { isBooleanAttr } from "../html-attrs.js";
13
+ const TARGETS = {
14
+ es2020: ts.ScriptTarget.ES2020,
15
+ es2022: ts.ScriptTarget.ES2022,
16
+ esnext: ts.ScriptTarget.ESNext,
17
+ };
18
+ export function compileClient(module, opts = {}) {
19
+ return compileClientCore(module, opts).code;
20
+ }
21
+ // Full result including the source map (used by the CLI to write .js.map).
22
+ export function compileClientCore(module, opts = {}) {
23
+ if (module.components.length === 0) {
24
+ throw new ArgonCompileError([{
25
+ code: "ARGON001",
26
+ target: "all",
27
+ message: "No Argon component found.",
28
+ span: { start: 0, end: 0 },
29
+ hint: "Export a capitalized multi-word function returning Component with a TSX template.",
30
+ }], module.source);
31
+ }
32
+ const jsxDiagnostics = collectJsxOpaque(module);
33
+ if (jsxDiagnostics.length > 0)
34
+ throw new ArgonCompileError(jsxDiagnostics, module.source);
35
+ const edits = [];
36
+ for (const im of module.imports) {
37
+ if (im.isArgon)
38
+ edits.push({ start: im.span.start, end: im.span.end, text: "" });
39
+ }
40
+ for (const decl of module.decls) {
41
+ // Helpers containing JSX can't pass through verbatim; re-emit from IR.
42
+ if (decl.kind === "fn" && decl.hasJsx) {
43
+ edits.push({ start: decl.span.start, end: decl.span.end, text: printFnDecl(decl) });
44
+ }
45
+ }
46
+ for (const component of module.components) {
47
+ edits.push({ start: component.span.start, end: component.span.end, text: generateClass(component, !!opts.compat03) });
48
+ }
49
+ edits.sort((a, b) => b.start - a.start);
50
+ let out = module.sourceText;
51
+ for (const e of edits)
52
+ out = out.slice(0, e.start) + e.text + out.slice(e.end);
53
+ const transpiled = ts.transpileModule(out, {
54
+ fileName: opts.fileName,
55
+ compilerOptions: {
56
+ target: TARGETS[opts.target ?? "es2022"],
57
+ module: ts.ModuleKind.ESNext,
58
+ sourceMap: !!opts.sourceMap,
59
+ inlineSources: !!opts.sourceMap,
60
+ },
61
+ transformers: { before: [stripTags] },
62
+ });
63
+ const composedImports = module.composedImports.map(spec => `import ${JSON.stringify(spec)};\n`).join("");
64
+ // Drop tsc's sourceMappingURL (it derives the wrong extension from the .tsx
65
+ // input); the caller appends the correct `<name>.js.map` reference.
66
+ const body = transpiled.outputText.replace(/\n?\/\/# sourceMappingURL=[^\n]*\n?$/, "");
67
+ const code = "// Generated by Argon compiler — do not edit by hand.\n// CSR output: browser-only hydration and reactive updates. SSR data evaluation stays out of this file.\n\n" +
68
+ composedImports +
69
+ body.trimStart();
70
+ return { code, map: transpiled.sourceMapText };
71
+ }
72
+ // JSX compiles only in template value positions, where lowering turns it into
73
+ // string templates. Opaque code containing JSX (event handlers, module
74
+ // helpers, untyped consts) would pass through verbatim and break the emitted
75
+ // module, so it is rejected up front with a usable location.
76
+ function collectJsxOpaque(module) {
77
+ const diagnostics = [];
78
+ const flag = (span) => {
79
+ diagnostics.push({
80
+ code: "ARGON121",
81
+ target: "csr",
82
+ message: "JSX is only supported in template value positions; this code is emitted verbatim and cannot contain JSX.",
83
+ span,
84
+ hint: "Build the markup as a template-literal string here, or move it into the component template.",
85
+ });
86
+ };
87
+ const checkExpr = (expr) => {
88
+ visitExpr(expr, e => {
89
+ if (e.kind === "opaque" && e.hasJsx)
90
+ flag(e.diagnostic.span);
91
+ });
92
+ };
93
+ for (const decl of module.decls) {
94
+ if (decl.kind === "opaque" && decl.hasJsx)
95
+ flag(decl.diagnostic.span);
96
+ if (decl.kind === "table" && decl.hasJsx)
97
+ flag(decl.span);
98
+ }
99
+ for (const component of module.components) {
100
+ for (const prop of component.props) {
101
+ if (prop.default)
102
+ checkExpr(prop.default);
103
+ }
104
+ for (const stmt of component.setup) {
105
+ if (stmt.kind === "opaque" && stmt.hasJsx)
106
+ flag(stmt.diagnostic.span);
107
+ else if (stmt.kind === "hook" && stmt.hasJsx)
108
+ flag(stmt.span);
109
+ else if (stmt.kind === "let")
110
+ checkExpr(stmt.init);
111
+ }
112
+ for (const expr of component.template.exprs)
113
+ checkExpr(expr);
114
+ }
115
+ return diagnostics;
116
+ }
117
+ // The `css` tag exists in the source only for typing; an untagged template
118
+ // literal yields the same string. Dropping it keeps each output a clean ES
119
+ // module with no injected module-scope globals (bundler/minifier safe).
120
+ const stripTags = ctx => {
121
+ const visit = (node) => {
122
+ if (ts.isTaggedTemplateExpression(node) &&
123
+ ts.isIdentifier(node.tag) &&
124
+ node.tag.text === "css") {
125
+ return ts.visitNode(node.template, visit);
126
+ }
127
+ return ts.visitEachChild(node, visit, ctx);
128
+ };
129
+ return sf => ts.visitNode(sf, visit);
130
+ };
131
+ // ── Naming ───────────────────────────────────────────────────────────────────
132
+ function cap(name) {
133
+ return name.charAt(0).toUpperCase() + name.slice(1);
134
+ }
135
+ function kebab(name) {
136
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
137
+ }
138
+ // ── Opaque code: rewrite prop/state references ───────────────────────────────
139
+ //
140
+ // Replace bare prop/state references with private fields and state mutations
141
+ // with setter calls, leaving property names and unrelated identifiers
142
+ // untouched. `kind` controls printing: statements keep trailing semicolons,
143
+ // expressions must not have one (they land inside `${...}` and call sites).
144
+ function rewriteRefs(text, refs, states, kind) {
145
+ if (refs.size === 0 && states.size === 0)
146
+ return text.trim();
147
+ const sf = ts.createSourceFile("_.ts", text, ts.ScriptTarget.Latest, true);
148
+ const transformer = ctx => {
149
+ const visit = (node) => {
150
+ if (ts.isBinaryExpression(node) &&
151
+ ts.isIdentifier(node.left) &&
152
+ states.has(node.left.text)) {
153
+ const op = node.operatorToken.kind;
154
+ const setter = ctx.factory.createPropertyAccessExpression(ctx.factory.createThis(), ctx.factory.createPrivateIdentifier("#set" + cap(node.left.text)));
155
+ if (op === ts.SyntaxKind.EqualsToken) {
156
+ return ctx.factory.createCallExpression(setter, undefined, [
157
+ ts.visitNode(node.right, visit),
158
+ ]);
159
+ }
160
+ const binaryOps = {
161
+ [ts.SyntaxKind.PlusEqualsToken]: ts.SyntaxKind.PlusToken,
162
+ [ts.SyntaxKind.MinusEqualsToken]: ts.SyntaxKind.MinusToken,
163
+ [ts.SyntaxKind.AsteriskEqualsToken]: ts.SyntaxKind.AsteriskToken,
164
+ [ts.SyntaxKind.SlashEqualsToken]: ts.SyntaxKind.SlashToken,
165
+ };
166
+ const binaryOp = binaryOps[op];
167
+ if (binaryOp) {
168
+ return ctx.factory.createCallExpression(setter, undefined, [
169
+ ctx.factory.createBinaryExpression(ctx.factory.createPropertyAccessExpression(ctx.factory.createThis(), ctx.factory.createPrivateIdentifier("#" + node.left.text)), binaryOp, ts.visitNode(node.right, visit)),
170
+ ]);
171
+ }
172
+ }
173
+ if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
174
+ ts.isIdentifier(node.operand) &&
175
+ states.has(node.operand.text) &&
176
+ (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken)) {
177
+ return ctx.factory.createCallExpression(ctx.factory.createPropertyAccessExpression(ctx.factory.createThis(), ctx.factory.createPrivateIdentifier("#set" + cap(node.operand.text))), undefined, [ctx.factory.createBinaryExpression(ctx.factory.createPropertyAccessExpression(ctx.factory.createThis(), ctx.factory.createPrivateIdentifier("#" + node.operand.text)), node.operator === ts.SyntaxKind.PlusPlusToken ? ts.SyntaxKind.PlusToken : ts.SyntaxKind.MinusToken, ctx.factory.createNumericLiteral(1))]);
178
+ }
179
+ if (ts.isIdentifier(node) &&
180
+ refs.has(node.text) &&
181
+ !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) &&
182
+ !(ts.isPropertyAssignment(node.parent) && node.parent.name === node) &&
183
+ !(ts.isBindingElement(node.parent) && node.parent.name === node)) {
184
+ return ctx.factory.createPropertyAccessExpression(ctx.factory.createThis(), ctx.factory.createPrivateIdentifier("#" + node.text));
185
+ }
186
+ return ts.visitEachChild(node, visit, ctx);
187
+ };
188
+ return sf => ts.visitNode(sf, visit);
189
+ };
190
+ const out = ts.transform(sf, [transformer]).transformed[0];
191
+ const printer = ts.createPrinter({ removeComments: false });
192
+ if (kind === "stmts")
193
+ return printer.printFile(out).trim();
194
+ const stmt = out.statements[0];
195
+ const expr = ts.isExpressionStatement(stmt) ? stmt.expression : stmt;
196
+ return printer.printNode(ts.EmitHint.Unspecified, expr, out);
197
+ }
198
+ function printExpr(e, opaque, attrEnc = "esc") {
199
+ const p = (x) => printExpr(x, opaque, attrEnc);
200
+ const po = (x) => x.kind === "binary" || x.kind === "cond" || x.kind === "unary" || x.kind === "exists" ? `(${p(x)})` : p(x);
201
+ switch (e.kind) {
202
+ case "str": return JSON.stringify(e.value);
203
+ case "num": return e.text;
204
+ case "bool": return String(e.value);
205
+ case "ref": return e.space === "prop" || e.space === "state" ? `this.#${e.name}` : e.name;
206
+ case "member": return `${po(e.object)}.${e.name}`;
207
+ case "length": return `${po(e.object)}.length`;
208
+ case "index": return `${po(e.object)}[${p(e.index)}]`;
209
+ case "lookup": return `${e.table}[${p(e.key)}]`;
210
+ // Loose null check: absent props are undefined, hydrated optional struct
211
+ // fields arrive as JSON null.
212
+ case "exists": return `${po(e.object)} ${e.present ? "!=" : "=="} null`;
213
+ // The guard guarantees presence; read the raw value.
214
+ case "unwrap": return p(e.object);
215
+ case "unary": return `${e.op}${po(e.operand)}`;
216
+ case "binary": return `${po(e.left)} ${e.op === "==" ? "===" : e.op === "!=" ? "!==" : e.op} ${po(e.right)}`;
217
+ case "cond": return `${po(e.cond)} ? ${po(e.whenTrue)} : ${po(e.whenFalse)}`;
218
+ case "template": {
219
+ let out = "`" + escapeTemplate(e.quasis[0]);
220
+ e.parts.forEach((part, i) => {
221
+ out += "${" + p(part) + "}" + escapeTemplate(e.quasis[i + 1]);
222
+ });
223
+ return out + "`";
224
+ }
225
+ case "call": return `${e.fn}(${e.args.map(p).join(", ")})`;
226
+ case "map": {
227
+ const item = typeof e.item === "string" ? e.item : `[${e.item.join(", ")}]`;
228
+ const params = e.indexVar ? `(${item}, ${e.indexVar})` : `(${item})`;
229
+ const body = e.body.stmts.length > 0
230
+ ? `{ ${e.body.stmts.map(s => printStmt(s, opaque, undefined, attrEnc)).join(" ")} return ${p(e.body.result)}; }`
231
+ : po(e.body.result);
232
+ return `${po(e.array)}.map(${params} => ${body})`;
233
+ }
234
+ case "join": return `${po(e.array)}.join(${JSON.stringify(e.sep)})`;
235
+ case "minmax": return `Math.${e.op}(...${po(e.array)})`;
236
+ case "math": return `Math.${e.fn}(${e.args.map(p).join(", ")})`;
237
+ // Every whitelisted string method is a native JS method of the same name.
238
+ case "strmethod": return `${po(e.object)}.${e.method}(${e.args.map(p).join(", ")})`;
239
+ // Array methods are native too; predicate methods take the arrow back.
240
+ case "arrmethod":
241
+ return e.param && e.body
242
+ ? `${po(e.object)}.${e.method}(${e.param} => ${p(e.body)})`
243
+ : `${po(e.object)}.${e.method}(${e.args.map(p).join(", ")})`;
244
+ case "toFixed": return `${po(e.value)}.toFixed(${e.digits})`;
245
+ case "arr": return `[${e.elems.map(p).join(", ")}]`;
246
+ case "obj": return `{ ${e.fields.map(f => `${f.name}: ${p(f.value)}`).join(", ")} }`;
247
+ // The child element hydrates itself from the data-* attributes around
248
+ // this marker; only the server inlines shadow DOM here.
249
+ case "construct": return `""`;
250
+ case "propattr": return `this.${attrEnc === "raw" ? "#attrRaw" : "#attrEsc"}(${p(e.value)})`;
251
+ // Raw markup (unsafeHtml): the inner value is interpolated as-is. Text
252
+ // escaping is applied at the text-render site, which skips markup values.
253
+ case "raw": return p(e.inner);
254
+ case "opaque": return opaque(e.text);
255
+ }
256
+ }
257
+ // Module helpers containing JSX are re-emitted from the IR (their original
258
+ // text would leave raw JSX in the output module).
259
+ function printFnDecl(decl) {
260
+ const verbatim = text => text;
261
+ const body = decl.body.map(s => " " + printStmt(s, verbatim)).join("\n");
262
+ return `function ${decl.name}(${decl.params.map(p => p.name).join(", ")}) {\n${body}\n}`;
263
+ }
264
+ function printStmt(stmt, opaque, opaqueStmt, attrEnc = "esc") {
265
+ if (stmt.kind === "let") {
266
+ const target = stmt.tuple ? `[${stmt.names.join(", ")}]` : stmt.names[0];
267
+ return `const ${target} = ${printExpr(stmt.init, opaque, attrEnc)};`;
268
+ }
269
+ if (stmt.kind === "return")
270
+ return `return ${printExpr(stmt.value, opaque, attrEnc)};`;
271
+ if (stmt.kind === "if") {
272
+ const body = (stmts) => stmts.map(s => printStmt(s, opaque, opaqueStmt, attrEnc)).join(" ");
273
+ const els = stmt.else ? ` else { ${body(stmt.else)} }` : "";
274
+ return `if (${printExpr(stmt.cond, opaque, attrEnc)}) { ${body(stmt.then)} }${els}`;
275
+ }
276
+ if (stmt.kind === "opaque")
277
+ return (opaqueStmt ?? opaque)(stmt.text);
278
+ // Hooks and ref declarations are emitted by the class generator, not here.
279
+ return "";
280
+ }
281
+ function escapeTemplate(text) {
282
+ return text.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
283
+ }
284
+ // ── Typed attribute coercion ─────────────────────────────────────────────────
285
+ //
286
+ // Attribute and dataset values arrive as strings; constructor props arrive as
287
+ // real values. The setter normalizes both based on the declared prop type.
288
+ function coerce(type, value) {
289
+ // Absence (null/undefined) is handled by the setter before coercion runs.
290
+ if (type.kind === "option")
291
+ type = type.inner;
292
+ switch (type.kind) {
293
+ case "number": return `Number(${value})`;
294
+ case "boolean": return `typeof ${value} === 'string' ? ${value} !== 'false' : Boolean(${value})`;
295
+ case "array":
296
+ case "fixed":
297
+ case "tuple":
298
+ case "struct":
299
+ return `typeof ${value} === 'string' ? JSON.parse(${value}) : ${value}`;
300
+ default: return value;
301
+ }
302
+ }
303
+ // ── Class generation ─────────────────────────────────────────────────────────
304
+ function generateClass(component, compat03 = false) {
305
+ const props = new Set(component.props.map(p => p.name));
306
+ const states = new Set(component.states.map(s => s.name));
307
+ const refs = new Set([...props, ...states, ...component.refs]);
308
+ const template = component.template;
309
+ const opaqueExpr = text => rewriteRefs(text, refs, states, "expr");
310
+ const opaqueStmt = text => rewriteRefs(text, refs, states, "stmts");
311
+ const print = (e) => printExpr(e, opaqueExpr);
312
+ // ── Escape-by-default text rendering (mirrors the Rust backend) ──
313
+ //
314
+ // Setup locals whose value is markup (a JSX string) read raw; every other
315
+ // string interpolated into text position is HTML-escaped so it can't inject
316
+ // markup. `unsafeHtml(...)` opts a trusted string out via a `raw` node.
317
+ const markupLocals = new Set();
318
+ for (const stmt of component.setup) {
319
+ if (stmt.kind === "let" && !stmt.tuple && markupValue(stmt.init, markupLocals))
320
+ markupLocals.add(stmt.names[0]);
321
+ }
322
+ // Classify each template marker by position: bound boolean attribute
323
+ // (present-or-absent), other attribute value (attribute-escaped), or text.
324
+ const attrOf = new Map();
325
+ const boolAttr = new Set();
326
+ // 0.3 compat renders markers verbatim: no attribute/text split, no boolean
327
+ // collapse, no escaping.
328
+ if (!compat03) {
329
+ for (const b of template.attrBindings) {
330
+ for (const idx of b.exprIndices)
331
+ attrOf.set(idx, b.attr);
332
+ if (isBooleanAttr(b.attr) && b.exprIndices.length === 1 && b.value.replace(MARKER_RE, "") === "") {
333
+ boolAttr.add(b.exprIndices[0]);
334
+ }
335
+ }
336
+ }
337
+ let usesEscText = false;
338
+ let usesEscAttr = false;
339
+ const po = (e) => e.kind === "binary" || e.kind === "cond" || e.kind === "unary" || e.kind === "exists" ? `(${print(e)})` : print(e);
340
+ const textExpr = (e) => {
341
+ if (compat03)
342
+ return print(e); // 0.3: verbatim text, no escaping
343
+ if (markupValue(e, markupLocals))
344
+ return markupExpr(e);
345
+ if (e.type.kind === "string") {
346
+ usesEscText = true;
347
+ return `this.#escText(${print(e)})`;
348
+ }
349
+ return print(e);
350
+ };
351
+ const markupExpr = (e) => {
352
+ switch (e.kind) {
353
+ case "str": return JSON.stringify(e.value);
354
+ case "raw": return print(e.inner);
355
+ case "construct": return `""`;
356
+ case "template": {
357
+ const inAttr = attrPositions(e.quasis);
358
+ let out = "`" + escapeTemplate(e.quasis[0]);
359
+ e.parts.forEach((part, i) => {
360
+ // Attribute-position parts attribute-escape; text-position parts
361
+ // text-escape; a composed child's prop value self-encodes.
362
+ const rendered = part.kind === "propattr" ? print(part) : inAttr[i] ? attrExpr(part) : textExpr(part);
363
+ out += "${" + rendered + "}" + escapeTemplate(e.quasis[i + 1]);
364
+ });
365
+ return out + "`";
366
+ }
367
+ // Each branch renders by its own kind: markup raw, a plain string escaped.
368
+ case "cond": {
369
+ const branch = (b) => (isMarkup(b) ? markupExpr(b) : textExpr(b));
370
+ return `${po(e.cond)} ? ${branch(e.whenTrue)} : ${branch(e.whenFalse)}`;
371
+ }
372
+ case "join":
373
+ return e.array.kind === "map" ? `${printMapText(e.array)}.join(${JSON.stringify(e.sep)})` : print(e);
374
+ default: return print(e);
375
+ }
376
+ };
377
+ // A map whose items are markup: render each item's result through textExpr so
378
+ // interpolated scalar parts are escaped while the markup passes through.
379
+ const printMapText = (m, result = textExpr) => {
380
+ const item = typeof m.item === "string" ? m.item : `[${m.item.join(", ")}]`;
381
+ const params = m.indexVar ? `(${item}, ${m.indexVar})` : `(${item})`;
382
+ const pre = m.body.stmts.map(s => printStmt(s, opaqueExpr, undefined, "esc")).join(" ");
383
+ const body = m.body.stmts.length > 0 ? `{ ${pre} return ${result(m.body.result)}; }` : result(m.body.result);
384
+ return `${po(m.array)}.map(${params} => ${body})`;
385
+ };
386
+ const attrExpr = (e) => {
387
+ // A composed child's prop value already encodes and escapes itself.
388
+ if (e.kind === "propattr")
389
+ return print(e);
390
+ if (e.type.kind === "number" || e.type.kind === "boolean")
391
+ return print(e);
392
+ if (e.type.kind === "string") {
393
+ usesEscAttr = true;
394
+ return `this.#escAttr(${print(e)})`;
395
+ }
396
+ return print(e);
397
+ };
398
+ // Render one template marker in its classified position.
399
+ const renderMarker = (idx) => {
400
+ const e = template.exprs[idx];
401
+ if (compat03)
402
+ return print(e);
403
+ if (boolAttr.has(idx))
404
+ return `${po(e)} ? ' ${attrOf.get(idx)}' : ''`;
405
+ if (attrOf.has(idx))
406
+ return attrExpr(e);
407
+ return textExpr(e);
408
+ };
409
+ const setupStmts = component.setup.filter(s => (s.kind === "let" && !s.isState) || s.kind === "opaque");
410
+ // Markup stored in a local escapes its interpolated parts, so reading it raw
411
+ // later cannot inject unescaped values.
412
+ const printSetupStmt = (s) => s.kind === "let" && !s.tuple && isMarkup(s.init)
413
+ ? `const ${s.names[0]} = ${markupExpr(s.init)};`
414
+ : printStmt(s, opaqueExpr, opaqueStmt);
415
+ const prelude = setupStmts
416
+ .map(printSetupStmt)
417
+ .join("\n ");
418
+ // Reactive dependencies per binding, flowing through setup locals. Refs are
419
+ // rewritten like props/states but never change, so they are not tracked.
420
+ const tracked = new Set([...props, ...states]);
421
+ const locals = localDependencies(component, tracked);
422
+ const condition = (exprIndices) => {
423
+ const deps = bindingDependencies(component, exprIndices, tracked, locals);
424
+ return ["prop === undefined", ...[...deps].map(d => `prop === '${d}'`)].join(" || ");
425
+ };
426
+ // ── Setup gating for #updateBindings ──
427
+ //
428
+ // A setup local only needs recomputing when a binding that reads it
429
+ // (directly or through other locals) is about to update. usedBy maps each
430
+ // local to the union of those bindings' dependency sets; locals reached
431
+ // from opaque setup statements always run, since opaque code is
432
+ // re-executed unconditionally.
433
+ const localNames = new Set();
434
+ for (const s of setupStmts)
435
+ if (s.kind === "let")
436
+ for (const n of s.names)
437
+ localNames.add(n);
438
+ const directLocalRefs = new Map();
439
+ for (const s of setupStmts) {
440
+ if (s.kind !== "let")
441
+ continue;
442
+ const refsOfInit = new Set([...referencedNames(s.init)].filter(n => localNames.has(n)));
443
+ for (const n of s.names)
444
+ directLocalRefs.set(n, refsOfInit);
445
+ }
446
+ const localsOf = (names) => {
447
+ const out = new Set();
448
+ const stack = [...names].filter(n => localNames.has(n));
449
+ while (stack.length > 0) {
450
+ const name = stack.pop();
451
+ if (out.has(name))
452
+ continue;
453
+ out.add(name);
454
+ for (const r of directLocalRefs.get(name) ?? [])
455
+ if (!out.has(r))
456
+ stack.push(r);
457
+ }
458
+ return out;
459
+ };
460
+ const usedBy = new Map();
461
+ const noteBinding = (exprIndices) => {
462
+ const deps = bindingDependencies(component, exprIndices, tracked, locals);
463
+ const names = new Set();
464
+ for (const i of exprIndices)
465
+ for (const n of referencedNames(template.exprs[i]))
466
+ names.add(n);
467
+ for (const local of localsOf(names)) {
468
+ const set = usedBy.get(local) ?? new Set();
469
+ for (const d of deps)
470
+ set.add(d);
471
+ usedBy.set(local, set);
472
+ }
473
+ };
474
+ for (const b of template.textBindings)
475
+ noteBinding(b.exprIndices);
476
+ for (const b of template.attrBindings)
477
+ noteBinding(b.exprIndices);
478
+ const alwaysNeeded = localsOf(setupStmts.flatMap(s => (s.kind === "opaque" ? s.freeIds : [])));
479
+ const updatePrelude = setupStmts
480
+ .map(stmt => {
481
+ if (stmt.kind !== "let")
482
+ return printSetupStmt(stmt);
483
+ if (stmt.names.some(n => alwaysNeeded.has(n)))
484
+ return printSetupStmt(stmt);
485
+ // Markup locals recompute unconditionally with escaped parts.
486
+ if (!stmt.tuple && isMarkup(stmt.init))
487
+ return printSetupStmt(stmt);
488
+ const deps = new Set();
489
+ for (const n of stmt.names)
490
+ for (const d of usedBy.get(n) ?? [])
491
+ deps.add(d);
492
+ if ([...tracked].every(t => deps.has(t)))
493
+ return printSetupStmt(stmt);
494
+ const cond = ["prop === undefined", ...[...deps].map(d => `prop === '${d}'`)].join(" || ");
495
+ const target = stmt.tuple ? `[${stmt.names.join(", ")}]` : stmt.names[0];
496
+ return `const ${target} = (${cond}) ? (${printExpr(stmt.init, opaqueExpr)}) : ${stmt.tuple ? "[]" : "undefined"};`;
497
+ })
498
+ .join("\n ");
499
+ // Text-binding updates re-render into innerHTML, so scalar values escape like
500
+ // the initial render; attribute-binding updates go through setAttribute (mode
501
+ // "raw"), which needs no HTML escaping.
502
+ const markers = (value, mode = "text") => {
503
+ const body = escapeTemplate(value);
504
+ return "`" + body.replace(MARKER_RE, (_m, i) => {
505
+ const e = template.exprs[Number(i)];
506
+ // Attribute updates interpolate verbatim (setAttribute); text updates
507
+ // escape (textExpr, which is itself verbatim under 0.3 compat).
508
+ const code = mode === "raw" ? printExpr(e, opaqueExpr, "raw") : textExpr(e);
509
+ return "${" + code + "}";
510
+ }) + "`";
511
+ };
512
+ const NUL = String.fromCharCode(0);
513
+ const renderBody = (() => {
514
+ // Collapse a bound boolean attribute to a whole-chunk marker so it renders
515
+ // as the bare attribute or nothing.
516
+ let html = serialize(template.nodes).replace(new RegExp(`\\s([a-zA-Z][a-zA-Z0-9-]*)="${NUL}(\\d+)${NUL}"`, "g"), (m, _n, idx) => (boolAttr.has(Number(idx)) ? `${NUL}${idx}${NUL}` : m));
517
+ return escapeTemplate(html).replace(MARKER_RE, (_m, i) => "${" + renderMarker(Number(i)) + "}");
518
+ })();
519
+ // A text binding that is exactly one `arr.map(...).join('')` expression
520
+ // renders a homogeneous item list; those reconcile per item instead of
521
+ // re-parsing the whole fragment. Items carrying `key={...}` reconcile by
522
+ // identity, so reorders and splices move nodes instead of re-rendering.
523
+ const listInfoOf = (b) => {
524
+ if (b.exprIndices.length !== 1)
525
+ return undefined;
526
+ if (b.value.replace(MARKER_RE, "") !== "")
527
+ return undefined;
528
+ const e = template.exprs[b.exprIndices[0]];
529
+ if (e.kind !== "join" || e.sep !== "" || e.array.kind !== "map")
530
+ return undefined;
531
+ const result = e.array.body.result;
532
+ return { map: e.array, key: result.kind === "template" ? result.key : undefined };
533
+ };
534
+ const listBindings = new Set(template.textBindings.filter(b => listInfoOf(b)).map(b => b.id));
535
+ const hasKeyedList = template.textBindings.some(b => listInfoOf(b)?.key);
536
+ const hasPlainList = template.textBindings.some(b => {
537
+ const info = listInfoOf(b);
538
+ return info !== undefined && info.key === undefined;
539
+ });
540
+ // `__argonNext` holds the freshly rendered string so unchanged bindings can
541
+ // skip DOM work; the name is reserved and must not collide with user locals.
542
+ const updateStatements = [
543
+ ...template.textBindings.map(b => {
544
+ const info = listInfoOf(b);
545
+ if (info && info.key) {
546
+ // Each item renders to a [key, html] pair; the reconciler matches by
547
+ // key so reordered items move their existing nodes.
548
+ const pair = (r) => `[${print(info.key)}, ${textExpr(r)}]`;
549
+ return ` if (${condition(b.exprIndices)}) {
550
+ this.#reconcileKeyedList(${b.id}, this.#textStart${b.id}, this.#textEnd${b.id}, ${printMapText(info.map, pair)});
551
+ }`;
552
+ }
553
+ if (info) {
554
+ return ` if (${condition(b.exprIndices)}) {
555
+ this.#reconcileList(${b.id}, this.#textStart${b.id}, this.#textEnd${b.id}, ${printMapText(info.map)});
556
+ }`;
557
+ }
558
+ return ` if (${condition(b.exprIndices)}) {
559
+ const __argonNext = ${markers(b.value)};
560
+ if (__argonNext !== this.#textLast${b.id}) {
561
+ this.#textLast${b.id} = __argonNext;
562
+ this.#replaceRange(this.#textStart${b.id}, this.#textEnd${b.id}, __argonNext);
563
+ }
564
+ }`;
565
+ }),
566
+ ...template.attrBindings.map((b, i) => {
567
+ // A bound boolean attribute toggles present/absent instead of taking a
568
+ // literal "true"/"false" value.
569
+ if (b.exprIndices.length === 1 && boolAttr.has(b.exprIndices[0])) {
570
+ return ` if ((${condition(b.exprIndices)}) && this.#attr${b.id}) {
571
+ this.#attr${b.id}.toggleAttribute('${b.attr}', !!(${print(template.exprs[b.exprIndices[0]])}));
572
+ }`;
573
+ }
574
+ return ` if ((${condition(b.exprIndices)}) && this.#attr${b.id}) {
575
+ const __argonNext = ${markers(b.value, "raw")};
576
+ if (__argonNext !== this.#attrLast${i}) {
577
+ this.#attrLast${i} = __argonNext;
578
+ this.#attr${b.id}.setAttribute('${b.attr}', __argonNext);
579
+ }
580
+ }`;
581
+ }),
582
+ ].join("\n");
583
+ const propField = (p) => p.default !== undefined ? `#${p.name} = ${print(p.default)};` : `#${p.name};`;
584
+ // States initialize in #initState — after props are populated — so
585
+ // `state(somePropValue)` captures the real initial prop.
586
+ const stateField = (s) => `#${s.name};`;
587
+ const stateInits = component.states
588
+ .map(s => ` this.#${s.name} = ${print(s.initial)};`)
589
+ .join("\n");
590
+ const accessors = [
591
+ ...component.props.map(p => propAccessors(p, print)),
592
+ ...component.states.map(stateSetter),
593
+ ].join("\n\n");
594
+ const ctorAssigns = component.props
595
+ .map(p => ` this.${p.name} = props.${p.name};`)
596
+ .join("\n");
597
+ const ssrReads = component.props
598
+ .map(p => ` if (!this.#${p.name}Set) this.${p.name} = this.dataset.${p.name};`)
599
+ .join("\n");
600
+ const observed = component.props.flatMap(p => [p.name.toLowerCase(), `data-${kebab(p.name)}`]);
601
+ const attrCases = component.props
602
+ .map(p => ` case '${p.name.toLowerCase()}':
603
+ case 'data-${kebab(p.name)}':
604
+ this.${p.name} = newValue;
605
+ break;`)
606
+ .join("\n");
607
+ const attrChanged = component.props.length === 0 ? "" : `
608
+ attributeChangedCallback(name, oldValue, newValue) {
609
+ if (oldValue === newValue) return;
610
+ switch (name) {
611
+ ${attrCases}
612
+ }
613
+ }`;
614
+ const textCases = template.textBindings
615
+ .map(b => ` case 'argon-text:${b.id}':
616
+ this.#textStart${b.id} = marker;
617
+ break;
618
+ case '/argon-text:${b.id}':
619
+ this.#textEnd${b.id} = marker;
620
+ break;`)
621
+ .join("\n");
622
+ const walker = template.textBindings.length > 0
623
+ ? ` const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
624
+ let marker;
625
+ while ((marker = walker.nextNode())) {
626
+ switch (marker.nodeValue) {
627
+ ${textCases}
628
+ }
629
+ }
630
+ `
631
+ : "";
632
+ const attrIds = [...new Set(template.attrBindings.map(b => b.id))];
633
+ const attrQueries = attrIds
634
+ .map(id => ` this.#attr${id} = root.querySelector('[data-argon-bind="${id}"]');`)
635
+ .join("\n");
636
+ const wiring = template.events
637
+ .map(b => {
638
+ const handler = print(template.exprs[b.exprIndex]);
639
+ return ` scope.querySelectorAll('[data-argon="${b.id}"]').forEach(el => el.addEventListener('${b.event}', ${handler}));`;
640
+ })
641
+ .join("\n");
642
+ const textFields = template.textBindings.map(b => ` #textStart${b.id};
643
+ #textEnd${b.id};${listBindings.has(b.id) ? "" : `
644
+ #textLast${b.id};`}`).join("\n");
645
+ const attrFields = attrIds.map(id => ` #attr${id};`).join("\n");
646
+ const attrMemoFields = template.attrBindings.map((_b, i) => ` #attrLast${i};`).join("\n");
647
+ const listsField = listBindings.size > 0 ? " #lists = {};" : "";
648
+ // ── Refs and lifecycle hooks ──
649
+ //
650
+ // A ref resolves lazily against the live shadow DOM, so it stays correct
651
+ // even when its element sits inside a re-rendered fragment. Mount callbacks
652
+ // run on connect; their returned cleanups (and effect cleanups) run on
653
+ // disconnect, and everything re-arms if the element is re-inserted.
654
+ const refFields = component.refs
655
+ .map(name => ` #${name} = (() => { const self = this; return { get current() { return self.#root ? self.#root.querySelector('[data-argon-ref="${name}"]') : null; } }; })();`)
656
+ .join("\n");
657
+ // Composed-component prop encoding: JSON for structs/arrays, String for
658
+ // scalars; #attrEsc additionally escapes for HTML-parsed contexts.
659
+ let usesPropAttr = false;
660
+ const notePropAttr = (e) => visitExpr(e, x => {
661
+ if (x.kind === "propattr")
662
+ usesPropAttr = true;
663
+ });
664
+ for (const e of template.exprs)
665
+ notePropAttr(e);
666
+ for (const s of setupStmts) {
667
+ if (s.kind === "let")
668
+ notePropAttr(s.init);
669
+ }
670
+ const attrHelpers = !usesPropAttr
671
+ ? ""
672
+ : ` #attrRaw(v) {
673
+ return typeof v === 'object' ? JSON.stringify(v) : String(v);
674
+ }
675
+
676
+ #attrEsc(v) {
677
+ return this.#attrRaw(v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
678
+ }
679
+
680
+ `;
681
+ // Text/attribute escaping for interpolated values, matching the Rust
682
+ // backend's argon_escape_text / argon_escape_attr so SSR and CSR agree.
683
+ const escHelpers = [
684
+ usesEscText
685
+ ? ` #escText(v) {
686
+ return String(v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
687
+ }`
688
+ : "",
689
+ usesEscAttr
690
+ ? ` #escAttr(v) {
691
+ return String(v).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
692
+ }`
693
+ : "",
694
+ ].filter(Boolean).join("\n\n");
695
+ const hooksOf = (hook) => component.setup.filter((s) => s.kind === "hook" && s.hook === hook);
696
+ const mounts = hooksOf("mount");
697
+ const effects = hooksOf("effect");
698
+ const hasHooks = mounts.length > 0 || effects.length > 0;
699
+ const hookFields = hasHooks
700
+ ? ` #hooksLive = false;${mounts.length > 0 ? "\n #cleanups = [];" : ""}`
701
+ : "";
702
+ const effectMethods = effects
703
+ .map((e, i) => ` #effect${i}Cleanup;
704
+ #effect${i}() {
705
+ if (this.#effect${i}Cleanup) this.#effect${i}Cleanup();
706
+ const cleanup = (${opaqueExpr(e.text)})();
707
+ this.#effect${i}Cleanup = typeof cleanup === 'function' ? cleanup : undefined;
708
+ }`)
709
+ .join("\n\n");
710
+ // Effects re-run after the DOM update for any tracked name they read.
711
+ const effectTriggers = effects
712
+ .map((e, i) => {
713
+ const deps = [...new Set(e.freeIds.filter(n => tracked.has(n)))];
714
+ if (deps.length === 0)
715
+ return "";
716
+ return ` if (this.#hooksLive && (${deps.map(d => `prop === '${d}'`).join(" || ")})) this.#effect${i}();`;
717
+ })
718
+ .filter(Boolean)
719
+ .join("\n");
720
+ const mountRuns = mounts
721
+ .map(m => ` {
722
+ const cleanup = (${opaqueExpr(m.text)})();
723
+ if (typeof cleanup === 'function') this.#cleanups.push(cleanup);
724
+ }`)
725
+ .join("\n");
726
+ const effectRuns = effects.map((_e, i) => ` this.#effect${i}();`).join("\n");
727
+ const hookConnect = hasHooks
728
+ ? `
729
+ if (this.#hooksLive) return;
730
+ this.#hooksLive = true;
731
+ ${[mountRuns, effectRuns].filter(Boolean).join("\n")}`
732
+ : "";
733
+ const effectCleanups = effects
734
+ .map((_e, i) => ` if (this.#effect${i}Cleanup) { this.#effect${i}Cleanup(); this.#effect${i}Cleanup = undefined; }`)
735
+ .join("\n");
736
+ const disconnected = !hasHooks ? "" : `
737
+
738
+ disconnectedCallback() {
739
+ this.#hooksLive = false;
740
+ ${[mounts.length > 0 ? " for (const cleanup of this.#cleanups.splice(0)) cleanup();" : "", effectCleanups].filter(Boolean).join("\n")}
741
+ }`;
742
+ const bindingFields = [textFields, attrFields, attrMemoFields, listsField, refFields, hookFields].filter(Boolean).join("\n");
743
+ // Keyed list patching: items are matched by key, so reordered or spliced
744
+ // items move their existing DOM nodes; only items whose rendered HTML
745
+ // changed (or that are genuinely new) are re-parsed.
746
+ const reconcileKeyedList = !hasKeyedList ? "" : `
747
+ #reconcileKeyedList(id, start, end, items) {
748
+ if (!start || !end) return;
749
+ let list = this.#lists[id];
750
+ if (!list) {
751
+ let node = start.nextSibling;
752
+ while (node && node !== end) { const next = node.nextSibling; node.remove(); node = next; }
753
+ list = this.#lists[id] = { keys: [], htmls: [], nodes: [] };
754
+ }
755
+ // nodes[i] = [marker comment, ...content nodes]; owned by the reconciler,
756
+ // so item DOM never needs re-walking between updates.
757
+ const oldKeys = list.keys, oldHtmls = list.htmls, oldNodes = list.nodes;
758
+ const oldIndex = new Map();
759
+ for (let i = 0; i < oldKeys.length; i++) oldIndex.set(oldKeys[i], i);
760
+ const used = new Array(oldKeys.length).fill(false);
761
+ const parse = (html) => {
762
+ const template = document.createElement('template');
763
+ template.innerHTML = html;
764
+ this.#wireEvents(template.content);
765
+ return template.content;
766
+ };
767
+ const keys = [], htmls = [], nodes = [];
768
+ let p = 0;
769
+ for (const [key, value] of items) {
770
+ const html = String(value);
771
+ while (p < oldKeys.length && used[p]) p++;
772
+ const anchor = p < oldKeys.length ? oldNodes[p][0] : end;
773
+ const i = oldIndex.get(key);
774
+ if (i === undefined || used[i]) {
775
+ const mark = document.createComment('argon-item');
776
+ const content = parse(html);
777
+ const fresh = [mark, ...content.childNodes];
778
+ anchor.before(mark, content);
779
+ nodes.push(fresh);
780
+ } else {
781
+ used[i] = true;
782
+ if (i === p) p++;
783
+ else anchor.before(...oldNodes[i]);
784
+ if (oldHtmls[i] !== html) {
785
+ for (let k = 1; k < oldNodes[i].length; k++) oldNodes[i][k].remove();
786
+ const content = parse(html);
787
+ const fresh = [oldNodes[i][0], ...content.childNodes];
788
+ oldNodes[i][0].after(content);
789
+ nodes.push(fresh);
790
+ } else {
791
+ nodes.push(oldNodes[i]);
792
+ }
793
+ }
794
+ keys.push(key);
795
+ htmls.push(html);
796
+ }
797
+ for (let i = 0; i < oldNodes.length; i++) {
798
+ if (!used[i]) for (const n of oldNodes[i]) n.remove();
799
+ }
800
+ list.keys = keys;
801
+ list.htmls = htmls;
802
+ list.nodes = nodes;
803
+ }
804
+ `;
805
+ // Per-item list patching: each item's rendered HTML is cached, and a
806
+ // comment marker delimits its DOM nodes. Only items whose HTML changed are
807
+ // re-parsed and swapped in; the rest keep their nodes (and listeners).
808
+ const reconcileList = !hasPlainList ? "" : `
809
+ #reconcileList(id, start, end, items) {
810
+ if (!start || !end) return;
811
+ let list = this.#lists[id];
812
+ if (!list) {
813
+ let node = start.nextSibling;
814
+ while (node && node !== end) { const next = node.nextSibling; node.remove(); node = next; }
815
+ list = this.#lists[id] = { htmls: [], marks: [] };
816
+ }
817
+ const { htmls, marks } = list;
818
+ if (items.length < htmls.length) {
819
+ let node = marks[items.length];
820
+ while (node && node !== end) { const next = node.nextSibling; node.remove(); node = next; }
821
+ htmls.length = items.length;
822
+ marks.length = items.length;
823
+ }
824
+ for (let i = 0; i < items.length; i++) {
825
+ const html = String(items[i]);
826
+ if (i < htmls.length) {
827
+ if (htmls[i] === html) continue;
828
+ const stop = i + 1 < marks.length ? marks[i + 1] : end;
829
+ let node = marks[i].nextSibling;
830
+ while (node && node !== stop) { const next = node.nextSibling; node.remove(); node = next; }
831
+ const template = document.createElement('template');
832
+ template.innerHTML = html;
833
+ this.#wireEvents(template.content);
834
+ stop.before(template.content);
835
+ htmls[i] = html;
836
+ } else {
837
+ const mark = document.createComment('argon-item');
838
+ const template = document.createElement('template');
839
+ template.innerHTML = html;
840
+ this.#wireEvents(template.content);
841
+ end.before(mark, template.content);
842
+ marks.push(mark);
843
+ htmls.push(html);
844
+ }
845
+ }
846
+ }
847
+ `;
848
+ const hydrate = `${ssrReads}${ssrReads ? "\n" : ""} this.#initState();
849
+ const shadow = this.shadowRoot;
850
+ const root = shadow ?? this.attachShadow({ mode: 'open' });
851
+ if (!shadow) root.innerHTML = this.#render();
852
+ this.#mount(root);`;
853
+ const connected = hasHooks
854
+ ? ` connectedCallback() {
855
+ if (!this.#isCSR && !this.#bindingsMounted) {
856
+ ${hydrate}
857
+ }${hookConnect}
858
+ }${disconnected}`
859
+ : ` connectedCallback() {
860
+ if (this.#isCSR || this.#bindingsMounted) return;
861
+ ${hydrate}
862
+ }`;
863
+ return `export class ${component.className} extends HTMLElement {
864
+ static observedAttributes = ${JSON.stringify(observed)};
865
+
866
+ ${[...component.props.map(propField), ...component.states.map(stateField)].join("\n ")}
867
+ ${component.props.map(p => `#${p.name}Set = false;`).join("\n ")}
868
+ ${bindingFields ? bindingFields + "\n" : ""} #root;
869
+ #bindingsMounted = false;
870
+ #isCSR = false;
871
+
872
+ ${accessors}
873
+
874
+ constructor(props) {
875
+ super();
876
+ if (props !== undefined) {
877
+ this.#isCSR = true;
878
+ ${ctorAssigns}
879
+ this.#initState();
880
+ const shadow = this.attachShadow({ mode: 'open' });
881
+ shadow.innerHTML = this.#render();
882
+ this.#mount(shadow);
883
+ }
884
+ }
885
+
886
+ ${connected}
887
+ ${attrChanged}
888
+
889
+ #initState() {
890
+ ${stateInits}
891
+ }
892
+
893
+ #render() {
894
+ ${prelude}
895
+ return \`${renderBody}\`;
896
+ }
897
+
898
+ #mount(root) {
899
+ this.#root = root;
900
+ ${walker}${attrQueries}${attrQueries ? "\n" : ""} this.#bindingsMounted = true;
901
+ this.#updateBindings();
902
+ this.#wireEvents(root);
903
+ this.setAttribute('hydrated', '');
904
+ }
905
+
906
+ #replaceRange(start, end, value) {
907
+ if (!start || !end) return;
908
+ const html = String(value);
909
+ const sole = start.nextSibling;
910
+ if (sole && sole !== end && sole.nextSibling === end && sole.nodeType === 3 && !/[<&]/.test(html)) {
911
+ sole.data = html;
912
+ return;
913
+ }
914
+ let node = start.nextSibling;
915
+ while (node && node !== end) {
916
+ const next = node.nextSibling;
917
+ node.remove();
918
+ node = next;
919
+ }
920
+ const template = document.createElement('template');
921
+ template.innerHTML = html;
922
+ this.#wireEvents(template.content);
923
+ end.before(template.content);
924
+ }
925
+ ${reconcileList}${reconcileKeyedList}${attrHelpers}${escHelpers ? escHelpers + "\n\n" : ""}${effectMethods ? effectMethods + "\n\n" : ""}
926
+ #updateBindings(prop) {
927
+ if (!this.#bindingsMounted) return;
928
+ ${updatePrelude}
929
+ ${updateStatements}${effectTriggers ? "\n" + effectTriggers : ""}
930
+ }
931
+
932
+ #wireEvents(scope) {
933
+ const root = this.#root;
934
+ ${wiring}
935
+ }
936
+ }
937
+
938
+ customElements.define('${component.tagName}', ${component.className});`;
939
+ }
940
+ function propAccessors(prop, print) {
941
+ const fallback = prop.default !== undefined ? `value ?? ${print(prop.default)}` : "value";
942
+ const next = coerce(prop.type, "raw");
943
+ return ` get ${prop.name}() {
944
+ return this.#${prop.name};
945
+ }
946
+
947
+ set ${prop.name}(value) {
948
+ const raw = ${fallback};
949
+ const next = raw == null ? raw : ${next};
950
+ if (Object.is(this.#${prop.name}, next)) return;
951
+ this.#${prop.name} = next;
952
+ this.#${prop.name}Set = true;
953
+ this.#updateBindings('${prop.name}');
954
+ }`;
955
+ }
956
+ function stateSetter(state) {
957
+ const setter = "#set" + cap(state.name);
958
+ return ` ${setter}(value) {
959
+ if (Object.is(this.#${state.name}, value)) return;
960
+ this.#${state.name} = value;
961
+ this.#updateBindings('${state.name}');
962
+ }`;
963
+ }
964
+ //# sourceMappingURL=client.js.map