@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.
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/annotations.d.ts +23 -0
- package/dist/annotations.d.ts.map +1 -0
- package/dist/annotations.js +23 -0
- package/dist/annotations.js.map +1 -0
- package/dist/codegen/client.d.ts +14 -0
- package/dist/codegen/client.d.ts.map +1 -0
- package/dist/codegen/client.js +964 -0
- package/dist/codegen/client.js.map +1 -0
- package/dist/codegen/dts.d.ts +3 -0
- package/dist/codegen/dts.d.ts.map +1 -0
- package/dist/codegen/dts.js +82 -0
- package/dist/codegen/dts.js.map +1 -0
- package/dist/codegen/js.d.ts +3 -0
- package/dist/codegen/js.d.ts.map +1 -0
- package/dist/codegen/js.js +278 -0
- package/dist/codegen/js.js.map +1 -0
- package/dist/codegen/rust.d.ts +6 -0
- package/dist/codegen/rust.d.ts.map +1 -0
- package/dist/codegen/rust.js +1076 -0
- package/dist/codegen/rust.js.map +1 -0
- package/dist/component.d.ts +35 -0
- package/dist/component.d.ts.map +1 -0
- package/dist/component.js +120 -0
- package/dist/component.js.map +1 -0
- package/dist/diagnostics.d.ts +20 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +40 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/docs.d.ts +3 -0
- package/dist/docs.d.ts.map +1 -0
- package/dist/docs.js +42 -0
- package/dist/docs.js.map +1 -0
- package/dist/error-catalog.d.ts +11 -0
- package/dist/error-catalog.d.ts.map +1 -0
- package/dist/error-catalog.js +92 -0
- package/dist/error-catalog.js.map +1 -0
- package/dist/html-attrs.d.ts +2 -0
- package/dist/html-attrs.d.ts.map +1 -0
- package/dist/html-attrs.js +37 -0
- package/dist/html-attrs.js.map +1 -0
- package/dist/html.d.ts +41 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +188 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +139 -0
- package/dist/index.js.map +1 -0
- package/dist/ir.d.ts +321 -0
- package/dist/ir.d.ts.map +1 -0
- package/dist/ir.js +243 -0
- package/dist/ir.js.map +1 -0
- package/dist/jsx.d.ts +31 -0
- package/dist/jsx.d.ts.map +1 -0
- package/dist/jsx.js +242 -0
- package/dist/jsx.js.map +1 -0
- package/dist/lower.d.ts +6 -0
- package/dist/lower.d.ts.map +1 -0
- package/dist/lower.js +1341 -0
- package/dist/lower.js.map +1 -0
- package/dist/parser.d.ts +3 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +714 -0
- package/dist/parser.js.map +1 -0
- package/dist/ssr-subset.d.ts +29 -0
- package/dist/ssr-subset.d.ts.map +1 -0
- package/dist/ssr-subset.js +119 -0
- package/dist/ssr-subset.js.map +1 -0
- package/dist/template.d.ts +32 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +90 -0
- package/dist/template.js.map +1 -0
- 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, '&').replace(/</g, '<').replace(/"/g, '"');
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
687
|
+
}`
|
|
688
|
+
: "",
|
|
689
|
+
usesEscAttr
|
|
690
|
+
? ` #escAttr(v) {
|
|
691
|
+
return String(v).replace(/&/g, '&').replace(/"/g, '"');
|
|
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
|