@frontiers-labs/argon 0.4.0 → 0.5.1

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.
@@ -3,6 +3,8 @@ export type Component = string & {
3
3
  };
4
4
  export declare function css(strings: TemplateStringsArray, ...values: any[]): string;
5
5
  export declare function state<T>(initial: T): T;
6
+ export declare function store<T>(initial: T): T;
7
+ export declare function derived<T>(compute: () => T): T;
6
8
  export declare function unsafeHtml(html: string): string;
7
9
  export interface Ref<T extends Element = Element> {
8
10
  readonly current: T | null;
@@ -10,6 +12,7 @@ export interface Ref<T extends Element = Element> {
10
12
  export declare function ref<T extends Element = Element>(): Ref<T>;
11
13
  export declare function onMount(callback: () => void | (() => void)): void;
12
14
  export declare function effect(callback: () => void | (() => void)): void;
15
+ export declare function emit(target: EventTarget, name: string, detail?: unknown): void;
13
16
  declare global {
14
17
  namespace JSX {
15
18
  type Element = string;
@@ -1 +1 @@
1
- {"version":3,"file":"annotations.d.ts","sourceRoot":"","sources":["../src/annotations.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,aAAa,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAE1D,wBAAgB,GAAG,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,CAE3E;AAED,wBAAgB,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAEtC;AAMD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAKD,MAAM,WAAW,GAAG,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO;IAC9C,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC;CAC5B;AAED,wBAAgB,GAAG,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,CAEzD;AAED,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAEjE;AAED,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAEhE;AAKD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,GAAG,CAAC;QACZ,KAAK,OAAO,GAAG,MAAM,CAAC;QACtB,UAAU,iBAAiB;YACzB,CAAC,OAAO,EAAE,MAAM,GAAG;gBAAE,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAA;aAAE,CAAC;SACrD;KACF;CACF"}
1
+ {"version":3,"file":"annotations.d.ts","sourceRoot":"","sources":["../src/annotations.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,aAAa,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAE1D,wBAAgB,GAAG,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,CAE3E;AAED,wBAAgB,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAEtC;AAED,wBAAgB,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAEtC;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CAE9C;AAMD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAKD,MAAM,WAAW,GAAG,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO;IAC9C,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC;CAC5B;AAED,wBAAgB,GAAG,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,CAEzD;AAED,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAEjE;AAED,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAEhE;AAED,wBAAgB,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAE9E;AAKD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,GAAG,CAAC;QACZ,KAAK,OAAO,GAAG,MAAM,CAAC;QACtB,UAAU,iBAAiB;YACzB,CAAC,OAAO,EAAE,MAAM,GAAG;gBAAE,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAA;aAAE,CAAC;SACrD;KACF;CACF"}
@@ -4,6 +4,12 @@ export function css(strings, ...values) {
4
4
  export function state(initial) {
5
5
  return initial;
6
6
  }
7
+ export function store(initial) {
8
+ return initial;
9
+ }
10
+ export function derived(compute) {
11
+ return compute();
12
+ }
7
13
  // Compile-time marker: interpolate the string as raw HTML instead of escaping
8
14
  // it. Identity at runtime — the name carries the audit burden (grep for it the
9
15
  // way you would `dangerouslySetInnerHTML`). Only feed it markup you trust or
@@ -20,4 +26,7 @@ export function onMount(callback) {
20
26
  export function effect(callback) {
21
27
  void callback;
22
28
  }
29
+ export function emit(target, name, detail) {
30
+ target.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail }));
31
+ }
23
32
  //# sourceMappingURL=annotations.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"annotations.js","sourceRoot":"","sources":["../src/annotations.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,GAAG,CAAC,OAA6B,EAAE,GAAG,MAAa;IACjE,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;AACxF,CAAC;AAED,MAAM,UAAU,KAAK,CAAI,OAAU;IACjC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,8EAA8E;AAC9E,+EAA+E;AAC/E,6EAA6E;AAC7E,gFAAgF;AAChF,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC;AACd,CAAC;AASD,MAAM,UAAU,GAAG;IACjB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,QAAmC;IACzD,KAAK,QAAQ,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,QAAmC;IACxD,KAAK,QAAQ,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"annotations.js","sourceRoot":"","sources":["../src/annotations.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,GAAG,CAAC,OAA6B,EAAE,GAAG,MAAa;IACjE,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;AACxF,CAAC;AAED,MAAM,UAAU,KAAK,CAAI,OAAU;IACjC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,KAAK,CAAI,OAAU;IACjC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,OAAO,CAAI,OAAgB;IACzC,OAAO,OAAO,EAAE,CAAC;AACnB,CAAC;AAED,8EAA8E;AAC9E,+EAA+E;AAC/E,6EAA6E;AAC7E,gFAAgF;AAChF,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC;AACd,CAAC;AASD,MAAM,UAAU,GAAG;IACjB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,QAAmC;IACzD,KAAK,QAAQ,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,QAAmC;IACxD,KAAK,QAAQ,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,MAAmB,EAAE,IAAY,EAAE,MAAgB;IACtE,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;AACzF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/codegen/client.ts"],"names":[],"mappings":"AAQA,OAAO,EAWL,KAAK,QAAQ,EAKd,MAAM,UAAU,CAAC;AAUlB,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEtD,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,QAAQ,CAAC;IAElB,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAQD,wBAAgB,aAAa,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,GAAE,aAAkB,GAAG,MAAM,CAEhF;AAGD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,GAAE,aAAkB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAqD5G"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/codegen/client.ts"],"names":[],"mappings":"AAQA,OAAO,EAWL,KAAK,QAAQ,EAMd,MAAM,UAAU,CAAC;AAUlB,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEtD,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,QAAQ,CAAC;IAElB,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAQD,wBAAgB,aAAa,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,GAAE,aAAkB,GAAG,MAAM,CAEhF;AAGD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,GAAE,aAAkB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CA2D5G"}
@@ -20,6 +20,9 @@ export function compileClient(module, opts = {}) {
20
20
  }
21
21
  // Full result including the source map (used by the CLI to write .js.map).
22
22
  export function compileClientCore(module, opts = {}) {
23
+ if (module.components.length === 0 && module.stores.length > 0) {
24
+ return { code: generateStoreModule(module.stores) };
25
+ }
23
26
  if (module.components.length === 0) {
24
27
  throw new ArgonCompileError([{
25
28
  code: "ARGON001",
@@ -44,7 +47,7 @@ export function compileClientCore(module, opts = {}) {
44
47
  }
45
48
  }
46
49
  for (const component of module.components) {
47
- edits.push({ start: component.span.start, end: component.span.end, text: generateClass(component, !!opts.compat03) });
50
+ edits.push({ start: component.span.start, end: component.span.end, text: generateClass(component, !!opts.compat03, storeAliases(module)) });
48
51
  }
49
52
  edits.sort((a, b) => b.start - a.start);
50
53
  let out = module.sourceText;
@@ -61,14 +64,95 @@ export function compileClientCore(module, opts = {}) {
61
64
  transformers: { before: [stripTags] },
62
65
  });
63
66
  const composedImports = module.composedImports.map(spec => `import ${JSON.stringify(spec)};\n`).join("");
67
+ const storeImports = [...storeAliases(module)].map(([spec, alias]) => `import { argonSubscribe as ${alias} } from ${JSON.stringify(spec)};\n`).join("");
64
68
  // Drop tsc's sourceMappingURL (it derives the wrong extension from the .tsx
65
69
  // input); the caller appends the correct `<name>.js.map` reference.
66
70
  const body = transpiled.outputText.replace(/\n?\/\/# sourceMappingURL=[^\n]*\n?$/, "");
67
71
  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
72
  composedImports +
73
+ storeImports +
69
74
  body.trimStart();
70
75
  return { code, map: transpiled.sourceMapText };
71
76
  }
77
+ function storeAliases(module) {
78
+ const specs = new Set();
79
+ const note = (expr) => {
80
+ visitExpr(expr, e => {
81
+ if (e.kind === "store")
82
+ specs.add(e.module);
83
+ });
84
+ };
85
+ for (const component of module.components) {
86
+ for (const stmt of component.setup) {
87
+ if (stmt.kind === "let")
88
+ note(stmt.init);
89
+ }
90
+ for (const expr of component.template.exprs)
91
+ note(expr);
92
+ }
93
+ return new Map([...specs].map((spec, i) => [spec, `__argonStoreSubscribe${i}`]));
94
+ }
95
+ function generateStoreModule(stores) {
96
+ const printInitial = (expr) => printExpr(expr, text => text);
97
+ const lines = [
98
+ "// Generated by Argon compiler — do not edit by hand.",
99
+ "// CSR store output: reactive shared state with no runtime dependency.",
100
+ "",
101
+ "const __argonSubs = new Map();",
102
+ "",
103
+ "function __argonNotify(key) {",
104
+ " for (const fn of __argonSubs.get(key) ?? []) fn();",
105
+ "}",
106
+ "",
107
+ "export function argonSubscribe(key, fn) {",
108
+ " let set = __argonSubs.get(key);",
109
+ " if (!set) __argonSubs.set(key, set = new Set());",
110
+ " set.add(fn);",
111
+ " return () => {",
112
+ " set.delete(fn);",
113
+ " if (set.size === 0) __argonSubs.delete(key);",
114
+ " };",
115
+ "}",
116
+ "",
117
+ ];
118
+ for (const store of stores) {
119
+ if (store.derived) {
120
+ const slot = `__argon_${store.name}`;
121
+ const deps = [...referencedNames(store.derived)]
122
+ .filter(name => name.startsWith("store::"))
123
+ .map(name => name.slice("store::".length));
124
+ lines.push(`function __argonCompute_${store.name}() {`, ` return ${printInitial(store.derived)};`, `}`, `let ${slot} = __argonCompute_${store.name}();`, `export const ${store.name} = {`, ` get value() { return ${slot}; },`, `};`, `function __argonUpdate_${store.name}() {`, ` const value = __argonCompute_${store.name}();`, ` if (Object.is(${slot}, value)) return;`, ` ${slot} = value;`, ` __argonNotify(${JSON.stringify(store.name)});`, `}`, ...deps.map(dep => `argonSubscribe(${JSON.stringify(dep)}, __argonUpdate_${store.name});`), "");
125
+ }
126
+ else if (store.fields) {
127
+ lines.push(`export const ${store.name} = {};`);
128
+ for (const field of store.fields) {
129
+ const slot = `__argon_${store.name}_${field.name}`;
130
+ const key = `${store.name}.${field.name}`;
131
+ lines.push(`let ${slot} = ${printInitial(field.initial)};`, `Object.defineProperty(${store.name}, ${JSON.stringify(field.name)}, {`, ` enumerable: true,`, ` get() { return ${slot}; },`, ` set(value) {`, ` if (Object.is(${slot}, value)) return;`, ` ${slot} = value;`, ` __argonNotify(${JSON.stringify(key)});`, ` },`, `});`, "");
132
+ }
133
+ }
134
+ else if (store.initial) {
135
+ const slot = `__argon_${store.name}`;
136
+ lines.push(`let ${slot} = ${printInitial(store.initial)};`, `export const ${store.name} = {`, ` get value() { return ${slot}; },`, ` set value(value) {`, ` if (Object.is(${slot}, value)) return;`, ` ${slot} = value;`, ` __argonNotify(${JSON.stringify(store.name)});`, ` },`, `};`, "");
137
+ }
138
+ }
139
+ lines.push("export function argonSnapshot() {", " return {", stores.map(store => store.fields
140
+ ? ` ${store.name}: { ${store.fields.map(f => `${f.name}: ${store.name}.${f.name}`).join(", ")} },`
141
+ : ` ${store.name}: ${store.name}.value,`).join("\n"), " };", "}", "", "export function argonHydrate(snapshot) {", " if (!snapshot) return;");
142
+ for (const store of stores) {
143
+ if (store.fields) {
144
+ lines.push(` if (snapshot.${store.name}) {`);
145
+ for (const field of store.fields)
146
+ lines.push(` if (${JSON.stringify(field.name)} in snapshot.${store.name}) ${store.name}.${field.name} = snapshot.${store.name}.${field.name};`);
147
+ lines.push(" }");
148
+ }
149
+ else if (!store.derived) {
150
+ lines.push(` if (${JSON.stringify(store.name)} in snapshot) ${store.name}.value = snapshot.${store.name};`);
151
+ }
152
+ }
153
+ lines.push("}", "", "const __argonPayload = globalThis.document?.querySelector?.('script[type=\"application/json\"][data-argon-stores]');", "if (__argonPayload?.textContent) argonHydrate(JSON.parse(__argonPayload.textContent));", "");
154
+ return lines.join("\n");
155
+ }
72
156
  // JSX compiles only in template value positions, where lowering turns it into
73
157
  // string templates. Opaque code containing JSX (event handlers, module
74
158
  // helpers, untyped consts) would pass through verbatim and break the emitted
@@ -88,6 +172,12 @@ function collectJsxOpaque(module) {
88
172
  visitExpr(expr, e => {
89
173
  if (e.kind === "opaque" && e.hasJsx)
90
174
  flag(e.diagnostic.span);
175
+ if (e.kind === "template") {
176
+ for (const event of e.events ?? []) {
177
+ if (event.handler.kind === "opaque" && event.handler.hasJsx)
178
+ flag(event.handler.diagnostic.span);
179
+ }
180
+ }
91
181
  });
92
182
  };
93
183
  for (const decl of module.decls) {
@@ -142,11 +232,25 @@ function kebab(name) {
142
232
  // untouched. `kind` controls printing: statements keep trailing semicolons,
143
233
  // expressions must not have one (they land inside `${...}` and call sites).
144
234
  function rewriteRefs(text, refs, states, kind) {
145
- if (refs.size === 0 && states.size === 0)
146
- return text.trim();
147
235
  const sf = ts.createSourceFile("_.ts", text, ts.ScriptTarget.Latest, true);
148
236
  const transformer = ctx => {
149
237
  const visit = (node) => {
238
+ if (ts.isCallExpression(node) &&
239
+ ts.isIdentifier(node.expression) &&
240
+ node.expression.text === "emit" &&
241
+ node.arguments.length >= 2) {
242
+ const [target, name, detail] = node.arguments;
243
+ return ctx.factory.createCallExpression(ctx.factory.createPropertyAccessExpression(ts.visitNode(target, visit), "dispatchEvent"), undefined, [
244
+ ctx.factory.createNewExpression(ctx.factory.createIdentifier("CustomEvent"), undefined, [
245
+ ts.visitNode(name, visit),
246
+ ctx.factory.createObjectLiteralExpression([
247
+ ctx.factory.createPropertyAssignment("bubbles", ctx.factory.createTrue()),
248
+ ctx.factory.createPropertyAssignment("composed", ctx.factory.createTrue()),
249
+ ctx.factory.createPropertyAssignment("detail", detail ? ts.visitNode(detail, visit) : ctx.factory.createVoidZero()),
250
+ ]),
251
+ ]),
252
+ ]);
253
+ }
150
254
  if (ts.isBinaryExpression(node) &&
151
255
  ts.isIdentifier(node.left) &&
152
256
  states.has(node.left.text)) {
@@ -203,6 +307,7 @@ function printExpr(e, opaque, attrEnc = "esc") {
203
307
  case "num": return e.text;
204
308
  case "bool": return String(e.value);
205
309
  case "ref": return e.space === "prop" || e.space === "state" ? `this.#${e.name}` : e.name;
310
+ case "store": return e.field ? `${e.local}.${e.field}` : `${e.local}.value`;
206
311
  case "member": return `${po(e.object)}.${e.name}`;
207
312
  case "length": return `${po(e.object)}.length`;
208
313
  case "index": return `${po(e.object)}[${p(e.index)}]`;
@@ -301,7 +406,7 @@ function coerce(type, value) {
301
406
  }
302
407
  }
303
408
  // ── Class generation ─────────────────────────────────────────────────────────
304
- function generateClass(component, compat03 = false) {
409
+ function generateClass(component, compat03 = false, storeAliases = new Map()) {
305
410
  const props = new Set(component.props.map(p => p.name));
306
411
  const states = new Set(component.states.map(s => s.name));
307
412
  const refs = new Set([...props, ...states, ...component.refs]);
@@ -309,6 +414,40 @@ function generateClass(component, compat03 = false) {
309
414
  const opaqueExpr = text => rewriteRefs(text, refs, states, "expr");
310
415
  const opaqueStmt = text => rewriteRefs(text, refs, states, "stmts");
311
416
  const print = (e) => printExpr(e, opaqueExpr);
417
+ const templateEventsIn = (expr) => {
418
+ const events = [];
419
+ visitExpr(expr, e => {
420
+ if (e.kind === "template") {
421
+ for (const event of e.events ?? [])
422
+ events.push(event);
423
+ }
424
+ });
425
+ return events;
426
+ };
427
+ const nestedEvents = [];
428
+ const noteNestedEvents = (expr) => {
429
+ nestedEvents.push(...templateEventsIn(expr));
430
+ };
431
+ for (const stmt of component.setup)
432
+ if (stmt.kind === "let")
433
+ noteNestedEvents(stmt.init);
434
+ for (const expr of template.exprs)
435
+ noteNestedEvents(expr);
436
+ const storeDeps = new Map();
437
+ const noteStoreDeps = (expr) => {
438
+ visitExpr(expr, e => {
439
+ if (e.kind !== "store")
440
+ return;
441
+ const deps = storeDeps.get(e.module) ?? new Set();
442
+ deps.add(e.key);
443
+ storeDeps.set(e.module, deps);
444
+ });
445
+ };
446
+ for (const stmt of component.setup)
447
+ if (stmt.kind === "let")
448
+ noteStoreDeps(stmt.init);
449
+ for (const expr of template.exprs)
450
+ noteStoreDeps(expr);
312
451
  // ── Escape-by-default text rendering (mirrors the Rust backend) ──
313
452
  //
314
453
  // Setup locals whose value is markup (a JSX string) read raw; every other
@@ -376,11 +515,30 @@ function generateClass(component, compat03 = false) {
376
515
  };
377
516
  // A map whose items are markup: render each item's result through textExpr so
378
517
  // interpolated scalar parts are escaped while the markup passes through.
379
- const printMapText = (m, result = textExpr) => {
518
+ const mapEventItem = (html, ctx, events) => {
519
+ if (events.length === 0)
520
+ return html;
521
+ const handlers = `{ ${events.map(event => `${JSON.stringify(event.id)}: ${print(event.handler)}`).join(", ")} }`;
522
+ const ids = `[${[...new Set(events.map(event => event.id))].map(id => JSON.stringify(String(id))).join(", ")}]`;
523
+ return `this.#mapEventItem(${html}, ${ctx}, ${handlers}, ${ids})`;
524
+ };
525
+ const printMapText = (m, result = textExpr, eventContext) => {
526
+ const events = templateEventsIn(m.body.result);
527
+ const needsIndex = events.length > 0 && !eventContext?.key && !m.indexVar;
380
528
  const item = typeof m.item === "string" ? m.item : `[${m.item.join(", ")}]`;
381
- const params = m.indexVar ? `(${item}, ${m.indexVar})` : `(${item})`;
529
+ const indexVar = m.indexVar ?? (needsIndex ? "__argonIndex" : undefined);
530
+ const params = indexVar ? `(${item}, ${indexVar})` : `(${item})`;
382
531
  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);
532
+ const rendered = (() => {
533
+ const html = result(m.body.result);
534
+ if (events.length === 0 || !eventContext || eventContext.key)
535
+ return html;
536
+ const ctx = eventContext.key
537
+ ? `${JSON.stringify(`${eventContext.bindingId}:`)} + ${print(eventContext.key)}`
538
+ : `${JSON.stringify(`${eventContext.bindingId}:`)} + ${indexVar}`;
539
+ return mapEventItem(html, ctx, events);
540
+ })();
541
+ const body = m.body.stmts.length > 0 ? `{ ${pre} return ${rendered}; }` : rendered;
384
542
  return `${po(m.array)}.map(${params} => ${body})`;
385
543
  };
386
544
  const attrExpr = (e) => {
@@ -417,7 +575,7 @@ function generateClass(component, compat03 = false) {
417
575
  .join("\n ");
418
576
  // Reactive dependencies per binding, flowing through setup locals. Refs are
419
577
  // rewritten like props/states but never change, so they are not tracked.
420
- const tracked = new Set([...props, ...states]);
578
+ const tracked = new Set([...props, ...states, ...[...storeDeps.entries()].flatMap(([module, keys]) => [...keys].map(key => `store:${module}:${key}`))]);
421
579
  const locals = localDependencies(component, tracked);
422
580
  const condition = (exprIndices) => {
423
581
  const deps = bindingDependencies(component, exprIndices, tracked, locals);
@@ -537,6 +695,10 @@ function generateClass(component, compat03 = false) {
537
695
  const info = listInfoOf(b);
538
696
  return info !== undefined && info.key === undefined;
539
697
  });
698
+ const hasMapEventBindings = template.textBindings.some(b => {
699
+ const info = listInfoOf(b);
700
+ return info !== undefined && templateEventsIn(info.map.body.result).length > 0;
701
+ });
540
702
  // `__argonNext` holds the freshly rendered string so unchanged bindings can
541
703
  // skip DOM work; the name is reserved and must not collide with user locals.
542
704
  const updateStatements = [
@@ -545,14 +707,19 @@ function generateClass(component, compat03 = false) {
545
707
  if (info && info.key) {
546
708
  // Each item renders to a [key, html] pair; the reconciler matches by
547
709
  // key so reordered items move their existing nodes.
548
- const pair = (r) => `[${print(info.key)}, ${textExpr(r)}]`;
710
+ const events = templateEventsIn(info.map.body.result);
711
+ const pair = (r) => {
712
+ const key = print(info.key);
713
+ const html = mapEventItem(textExpr(r), `${JSON.stringify(`${b.id}:`)} + ${key}`, events);
714
+ return `[${key}, ${html}]`;
715
+ };
549
716
  return ` if (${condition(b.exprIndices)}) {
550
- this.#reconcileKeyedList(${b.id}, this.#textStart${b.id}, this.#textEnd${b.id}, ${printMapText(info.map, pair)});
717
+ this.#reconcileKeyedList(${b.id}, this.#textStart${b.id}, this.#textEnd${b.id}, ${printMapText(info.map, pair, { bindingId: b.id, key: info.key })});
551
718
  }`;
552
719
  }
553
720
  if (info) {
554
721
  return ` if (${condition(b.exprIndices)}) {
555
- this.#reconcileList(${b.id}, this.#textStart${b.id}, this.#textEnd${b.id}, ${printMapText(info.map)});
722
+ this.#reconcileList(${b.id}, this.#textStart${b.id}, this.#textEnd${b.id}, ${printMapText(info.map, textExpr, { bindingId: b.id })});
556
723
  }`;
557
724
  }
558
725
  return ` if (${condition(b.exprIndices)}) {
@@ -633,18 +800,57 @@ ${textCases}
633
800
  const attrQueries = attrIds
634
801
  .map(id => ` this.#attr${id} = root.querySelector('[data-argon-bind="${id}"]');`)
635
802
  .join("\n");
636
- const wiring = template.events
803
+ const eventBindings = [
804
+ ...template.events.map(b => ({ id: b.id, event: b.event, handler: template.exprs[b.exprIndex] })),
805
+ ...nestedEvents,
806
+ ];
807
+ const wiring = eventBindings
637
808
  .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}));`;
809
+ const handler = print(b.handler);
810
+ return ` this.#eventTargets(scope, '${b.id}').forEach(el => this.#listen(el, '${b.id}', '${b.event}', ${handler}));`;
640
811
  })
641
812
  .join("\n");
813
+ const mapEventHelpers = hasMapEventBindings ? `
814
+ #mapEventItem(html, ctx, handlers, ids) {
815
+ const key = String(ctx);
816
+ this.#eventContexts[key] = handlers;
817
+ const attr = key.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
818
+ let out = String(html);
819
+ for (const id of ids) out = out.replaceAll(\`data-argon="\${id}"\`, \`data-argon="\${id}" data-argon-c="\${attr}"\`);
820
+ return out;
821
+ }
822
+
823
+ #eventHandler(el, id, fallback) {
824
+ const ctx = el.getAttribute('data-argon-c');
825
+ return ctx && this.#eventContexts[ctx]?.[id] ? this.#eventContexts[ctx][id] : fallback;
826
+ }
827
+
828
+ ` : "";
829
+ const eventHelpers = eventBindings.length === 0 ? "" : `
830
+ #eventTargets(scope, id) {
831
+ const selector = \`[data-argon="\${id}"]\`;
832
+ const targets = [...scope.querySelectorAll(selector)];
833
+ if (scope.nodeType === 1 && scope.matches(selector)) targets.unshift(scope);
834
+ return targets;
835
+ }
836
+
837
+ #listen(el, id, event, handler) {
838
+ const key = id + ':' + event;
839
+ if (!el.__argonListeners) Object.defineProperty(el, '__argonListeners', { value: {} });
840
+ const prev = el.__argonListeners[key];
841
+ if (prev) el.removeEventListener(event, prev);
842
+ const next = ${hasMapEventBindings ? "this.#eventHandler(el, id, handler)" : "handler"};
843
+ el.__argonListeners[key] = next;
844
+ el.addEventListener(event, next);
845
+ }
846
+ ${mapEventHelpers}`;
642
847
  const textFields = template.textBindings.map(b => ` #textStart${b.id};
643
848
  #textEnd${b.id};${listBindings.has(b.id) ? "" : `
644
849
  #textLast${b.id};`}`).join("\n");
645
850
  const attrFields = attrIds.map(id => ` #attr${id};`).join("\n");
646
851
  const attrMemoFields = template.attrBindings.map((_b, i) => ` #attrLast${i};`).join("\n");
647
852
  const listsField = listBindings.size > 0 ? " #lists = {};" : "";
853
+ const eventContextField = hasMapEventBindings ? " #eventContexts = {};" : "";
648
854
  // ── Refs and lifecycle hooks ──
649
855
  //
650
856
  // A ref resolves lazily against the live shadow DOM, so it stays correct
@@ -696,6 +902,7 @@ ${textCases}
696
902
  const mounts = hooksOf("mount");
697
903
  const effects = hooksOf("effect");
698
904
  const hasHooks = mounts.length > 0 || effects.length > 0;
905
+ const storeField = storeDeps.size > 0 ? " #storeCleanups = [];" : "";
699
906
  const hookFields = hasHooks
700
907
  ? ` #hooksLive = false;${mounts.length > 0 ? "\n #cleanups = [];" : ""}`
701
908
  : "";
@@ -733,13 +940,28 @@ ${[mountRuns, effectRuns].filter(Boolean).join("\n")}`
733
940
  const effectCleanups = effects
734
941
  .map((_e, i) => ` if (this.#effect${i}Cleanup) { this.#effect${i}Cleanup(); this.#effect${i}Cleanup = undefined; }`)
735
942
  .join("\n");
736
- const disconnected = !hasHooks ? "" : `
943
+ const storeConnect = storeDeps.size === 0 ? "" : `
944
+ #connectStores() {
945
+ if (this.#storeCleanups.length > 0) return;
946
+ ${[...storeDeps.entries()].flatMap(([module, keys]) => [...keys].map(key => ` this.#storeCleanups.push(${storeAliases.get(module)}(${JSON.stringify(key)}, () => this.#updateBindings(${JSON.stringify(`store:${module}:${key}`)})));`)).join("\n")}
947
+ }
948
+
949
+ #disconnectStores() {
950
+ for (const cleanup of this.#storeCleanups.splice(0)) cleanup();
951
+ }
952
+ `;
953
+ const disconnectBody = [
954
+ storeDeps.size > 0 ? " this.#disconnectStores();" : "",
955
+ hasHooks ? " this.#hooksLive = false;" : "",
956
+ hasHooks && mounts.length > 0 ? " for (const cleanup of this.#cleanups.splice(0)) cleanup();" : "",
957
+ effectCleanups,
958
+ ].filter(Boolean).join("\n");
959
+ const disconnectedCombined = storeDeps.size === 0 && !hasHooks ? "" : `
737
960
 
738
961
  disconnectedCallback() {
739
- this.#hooksLive = false;
740
- ${[mounts.length > 0 ? " for (const cleanup of this.#cleanups.splice(0)) cleanup();" : "", effectCleanups].filter(Boolean).join("\n")}
962
+ ${disconnectBody}
741
963
  }`;
742
- const bindingFields = [textFields, attrFields, attrMemoFields, listsField, refFields, hookFields].filter(Boolean).join("\n");
964
+ const bindingFields = [textFields, attrFields, attrMemoFields, listsField, eventContextField, refFields, storeField, hookFields].filter(Boolean).join("\n");
743
965
  // Keyed list patching: items are matched by key, so reordered or spliced
744
966
  // items move their existing DOM nodes; only items whose rendered HTML
745
967
  // changed (or that are genuinely new) are re-parsed.
@@ -782,25 +1004,38 @@ ${[mounts.length > 0 ? " for (const cleanup of this.#cleanups.splice(0)) clea
782
1004
  if (i === p) p++;
783
1005
  else anchor.before(...oldNodes[i]);
784
1006
  if (oldHtmls[i] !== html) {
785
- for (let k = 1; k < oldNodes[i].length; k++) oldNodes[i][k].remove();
786
1007
  const content = parse(html);
787
1008
  const fresh = [oldNodes[i][0], ...content.childNodes];
788
- oldNodes[i][0].after(content);
789
- nodes.push(fresh);
1009
+ if (this.#patchItem(oldNodes[i], fresh)) {
1010
+ nodes.push(oldNodes[i]);
1011
+ } else {
1012
+ for (let k = 1; k < oldNodes[i].length; k++) oldNodes[i][k].remove();
1013
+ oldNodes[i][0].after(content);
1014
+ nodes.push(fresh);
1015
+ }
790
1016
  } else {
791
- nodes.push(oldNodes[i]);
1017
+ ${hasMapEventBindings ? ` for (let k = 1; k < oldNodes[i].length; k++) this.#wireEvents(oldNodes[i][k]);
1018
+ ` : ""} nodes.push(oldNodes[i]);
792
1019
  }
793
1020
  }
794
1021
  keys.push(key);
795
1022
  htmls.push(html);
796
1023
  }
797
1024
  for (let i = 0; i < oldNodes.length; i++) {
798
- if (!used[i]) for (const n of oldNodes[i]) n.remove();
1025
+ if (!used[i]) {
1026
+ ${hasMapEventBindings ? ` delete this.#eventContexts[id + ':' + oldKeys[i]];
1027
+ ` : ""} for (const n of oldNodes[i]) n.remove();
1028
+ }
799
1029
  }
800
1030
  list.keys = keys;
801
1031
  list.htmls = htmls;
802
1032
  list.nodes = nodes;
803
1033
  }
1034
+
1035
+ #patchItem(current, fresh) {
1036
+ if (current.length !== 2 || fresh.length !== 2) return false;
1037
+ return this.#patchElement(current[1], fresh[1]);
1038
+ }
804
1039
  `;
805
1040
  // Per-item list patching: each item's rendered HTML is cached, and a
806
1041
  // comment marker delimits its DOM nodes. Only items whose HTML changed are
@@ -816,7 +1051,8 @@ ${[mounts.length > 0 ? " for (const cleanup of this.#cleanups.splice(0)) clea
816
1051
  }
817
1052
  const { htmls, marks } = list;
818
1053
  if (items.length < htmls.length) {
819
- let node = marks[items.length];
1054
+ ${hasMapEventBindings ? ` for (let i = items.length; i < htmls.length; i++) delete this.#eventContexts[id + ':' + i];
1055
+ ` : ""} let node = marks[items.length];
820
1056
  while (node && node !== end) { const next = node.nextSibling; node.remove(); node = next; }
821
1057
  htmls.length = items.length;
822
1058
  marks.length = items.length;
@@ -824,8 +1060,12 @@ ${[mounts.length > 0 ? " for (const cleanup of this.#cleanups.splice(0)) clea
824
1060
  for (let i = 0; i < items.length; i++) {
825
1061
  const html = String(items[i]);
826
1062
  if (i < htmls.length) {
827
- if (htmls[i] === html) continue;
828
1063
  const stop = i + 1 < marks.length ? marks[i + 1] : end;
1064
+ if (htmls[i] === html) {
1065
+ ${hasMapEventBindings ? ` let current = marks[i].nextSibling;
1066
+ while (current && current !== stop) { this.#wireEvents(current); current = current.nextSibling; }
1067
+ ` : ""} continue;
1068
+ }
829
1069
  let node = marks[i].nextSibling;
830
1070
  while (node && node !== stop) { const next = node.nextSibling; node.remove(); node = next; }
831
1071
  const template = document.createElement('template');
@@ -850,12 +1090,13 @@ ${[mounts.length > 0 ? " for (const cleanup of this.#cleanups.splice(0)) clea
850
1090
  const root = shadow ?? this.attachShadow({ mode: 'open' });
851
1091
  if (!shadow) root.innerHTML = this.#render();
852
1092
  this.#mount(root);`;
853
- const connected = hasHooks
1093
+ const connectStores = storeDeps.size > 0 ? "\n this.#connectStores();" : "";
1094
+ const connected = hasHooks || storeDeps.size > 0
854
1095
  ? ` connectedCallback() {
855
1096
  if (!this.#isCSR && !this.#bindingsMounted) {
856
1097
  ${hydrate}
857
- }${hookConnect}
858
- }${disconnected}`
1098
+ }${connectStores}${hookConnect}
1099
+ }${disconnectedCombined}`
859
1100
  : ` connectedCallback() {
860
1101
  if (this.#isCSR || this.#bindingsMounted) return;
861
1102
  ${hydrate}
@@ -908,21 +1149,39 @@ ${walker}${attrQueries}${attrQueries ? "\n" : ""} this.#bindingsMounted = tru
908
1149
  const html = String(value);
909
1150
  const sole = start.nextSibling;
910
1151
  if (sole && sole !== end && sole.nextSibling === end && sole.nodeType === 3 && !/[<&]/.test(html)) {
911
- sole.data = html;
1152
+ if (html.startsWith(sole.data)) sole.appendData(html.slice(sole.data.length));
1153
+ else sole.data = html;
912
1154
  return;
913
1155
  }
1156
+ const template = document.createElement('template');
1157
+ template.innerHTML = html;
1158
+ if (sole && sole !== end && sole.nextSibling === end && template.content.childNodes.length === 1 && this.#patchElement(sole, template.content.firstChild)) return;
914
1159
  let node = start.nextSibling;
915
1160
  while (node && node !== end) {
916
1161
  const next = node.nextSibling;
917
1162
  node.remove();
918
1163
  node = next;
919
1164
  }
920
- const template = document.createElement('template');
921
- template.innerHTML = html;
922
1165
  this.#wireEvents(template.content);
923
1166
  end.before(template.content);
924
1167
  }
925
- ${reconcileList}${reconcileKeyedList}${attrHelpers}${escHelpers ? escHelpers + "\n\n" : ""}${effectMethods ? effectMethods + "\n\n" : ""}
1168
+
1169
+ #patchElement(oldEl, newEl) {
1170
+ if (!oldEl || !newEl || oldEl.nodeType !== 1 || newEl.nodeType !== 1 || oldEl.localName !== newEl.localName) return false;
1171
+ for (const attr of [...oldEl.attributes]) {
1172
+ if (!newEl.hasAttribute(attr.name)) oldEl.removeAttribute(attr.name);
1173
+ }
1174
+ for (const attr of [...newEl.attributes]) {
1175
+ if (oldEl.getAttribute(attr.name) !== attr.value) oldEl.setAttribute(attr.name, attr.value);
1176
+ }
1177
+ if (oldEl.childNodes.length > 0 || newEl.childNodes.length > 0) {
1178
+ while (oldEl.firstChild) oldEl.firstChild.remove();
1179
+ oldEl.append(...newEl.childNodes);
1180
+ this.#wireEvents(oldEl);
1181
+ }
1182
+ return true;
1183
+ }
1184
+ ${reconcileList}${reconcileKeyedList}${attrHelpers}${escHelpers ? escHelpers + "\n\n" : ""}${effectMethods ? effectMethods + "\n\n" : ""}${storeConnect}${eventHelpers}
926
1185
  #updateBindings(prop) {
927
1186
  if (!this.#bindingsMounted) return;
928
1187
  ${updatePrelude}