@kuratchi/js 0.0.14 → 0.0.16

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 (65) hide show
  1. package/README.md +135 -68
  2. package/dist/cli.js +80 -47
  3. package/dist/compiler/api-route-pipeline.d.ts +8 -0
  4. package/dist/compiler/api-route-pipeline.js +23 -0
  5. package/dist/compiler/asset-pipeline.d.ts +7 -0
  6. package/dist/compiler/asset-pipeline.js +33 -0
  7. package/dist/compiler/client-module-pipeline.d.ts +25 -0
  8. package/dist/compiler/client-module-pipeline.js +257 -0
  9. package/dist/compiler/compiler-shared.d.ts +55 -0
  10. package/dist/compiler/compiler-shared.js +4 -0
  11. package/dist/compiler/component-pipeline.d.ts +15 -0
  12. package/dist/compiler/component-pipeline.js +163 -0
  13. package/dist/compiler/config-reading.d.ts +11 -0
  14. package/dist/compiler/config-reading.js +323 -0
  15. package/dist/compiler/convention-discovery.d.ts +9 -0
  16. package/dist/compiler/convention-discovery.js +83 -0
  17. package/dist/compiler/durable-object-pipeline.d.ts +9 -0
  18. package/dist/compiler/durable-object-pipeline.js +255 -0
  19. package/dist/compiler/error-page-pipeline.d.ts +1 -0
  20. package/dist/compiler/error-page-pipeline.js +16 -0
  21. package/dist/compiler/import-linking.d.ts +36 -0
  22. package/dist/compiler/import-linking.js +139 -0
  23. package/dist/compiler/index.d.ts +3 -3
  24. package/dist/compiler/index.js +137 -3265
  25. package/dist/compiler/layout-pipeline.d.ts +31 -0
  26. package/dist/compiler/layout-pipeline.js +155 -0
  27. package/dist/compiler/page-route-pipeline.d.ts +16 -0
  28. package/dist/compiler/page-route-pipeline.js +62 -0
  29. package/dist/compiler/parser.d.ts +4 -0
  30. package/dist/compiler/parser.js +433 -51
  31. package/dist/compiler/root-layout-pipeline.d.ts +10 -0
  32. package/dist/compiler/root-layout-pipeline.js +517 -0
  33. package/dist/compiler/route-discovery.d.ts +7 -0
  34. package/dist/compiler/route-discovery.js +87 -0
  35. package/dist/compiler/route-pipeline.d.ts +57 -0
  36. package/dist/compiler/route-pipeline.js +296 -0
  37. package/dist/compiler/route-state-pipeline.d.ts +25 -0
  38. package/dist/compiler/route-state-pipeline.js +139 -0
  39. package/dist/compiler/routes-module-feature-blocks.d.ts +2 -0
  40. package/dist/compiler/routes-module-feature-blocks.js +330 -0
  41. package/dist/compiler/routes-module-pipeline.d.ts +2 -0
  42. package/dist/compiler/routes-module-pipeline.js +6 -0
  43. package/dist/compiler/routes-module-runtime-shell.d.ts +2 -0
  44. package/dist/compiler/routes-module-runtime-shell.js +81 -0
  45. package/dist/compiler/routes-module-types.d.ts +44 -0
  46. package/dist/compiler/routes-module-types.js +1 -0
  47. package/dist/compiler/script-transform.d.ts +16 -0
  48. package/dist/compiler/script-transform.js +218 -0
  49. package/dist/compiler/server-module-pipeline.d.ts +13 -0
  50. package/dist/compiler/server-module-pipeline.js +124 -0
  51. package/dist/compiler/template.d.ts +13 -1
  52. package/dist/compiler/template.js +323 -60
  53. package/dist/compiler/worker-output-pipeline.d.ts +13 -0
  54. package/dist/compiler/worker-output-pipeline.js +37 -0
  55. package/dist/compiler/wrangler-sync.d.ts +14 -0
  56. package/dist/compiler/wrangler-sync.js +185 -0
  57. package/dist/runtime/app.js +15 -3
  58. package/dist/runtime/generated-worker.d.ts +33 -0
  59. package/dist/runtime/generated-worker.js +412 -0
  60. package/dist/runtime/index.d.ts +2 -1
  61. package/dist/runtime/index.js +1 -0
  62. package/dist/runtime/router.d.ts +2 -1
  63. package/dist/runtime/router.js +12 -3
  64. package/dist/runtime/types.d.ts +8 -2
  65. package/package.json +5 -1
@@ -37,13 +37,244 @@ const JS_CONTROL_PATTERNS = [
37
37
  function isJsControlLine(line) {
38
38
  return JS_CONTROL_PATTERNS.some(p => p.test(line));
39
39
  }
40
+ const FRAGMENT_OPEN_MARKER = '<!--__KURATCHI_FRAGMENT_OPEN:';
41
+ const FRAGMENT_CLOSE_MARKER = '<!--__KURATCHI_FRAGMENT_CLOSE-->';
42
+ export function splitTemplateRenderSections(template) {
43
+ const bodyLines = [];
44
+ const headLines = [];
45
+ const lines = template.split('\n');
46
+ let inHead = false;
47
+ let inStyle = false;
48
+ let inScript = false;
49
+ for (const line of lines) {
50
+ const trimmed = line.trim();
51
+ const opensStyle = /<style[\s>]/i.test(trimmed);
52
+ const closesStyle = /<\/style>/i.test(trimmed);
53
+ const opensScript = /<script[\s>]/i.test(trimmed);
54
+ const closesScript = /<\/script>/i.test(trimmed);
55
+ const inRawBlock = inStyle || inScript || opensStyle || opensScript;
56
+ if (inRawBlock) {
57
+ let remaining = line;
58
+ let bodyLine = '';
59
+ let headLine = '';
60
+ while (remaining.length > 0) {
61
+ if (!inHead) {
62
+ const openMatch = remaining.match(/<head(?:\s[^>]*)?>/i);
63
+ if (!openMatch || openMatch.index === undefined) {
64
+ bodyLine += remaining;
65
+ break;
66
+ }
67
+ bodyLine += remaining.slice(0, openMatch.index);
68
+ remaining = remaining.slice(openMatch.index + openMatch[0].length);
69
+ const closeMatch = remaining.match(/<\/head>/i);
70
+ if (!closeMatch || closeMatch.index === undefined) {
71
+ headLine += remaining;
72
+ remaining = '';
73
+ inHead = true;
74
+ break;
75
+ }
76
+ headLine += remaining.slice(0, closeMatch.index);
77
+ remaining = remaining.slice(closeMatch.index + closeMatch[0].length);
78
+ }
79
+ else {
80
+ const closeMatch = remaining.match(/<\/head>/i);
81
+ if (!closeMatch || closeMatch.index === undefined) {
82
+ headLine += remaining;
83
+ remaining = '';
84
+ break;
85
+ }
86
+ headLine += remaining.slice(0, closeMatch.index);
87
+ remaining = remaining.slice(closeMatch.index + closeMatch[0].length);
88
+ inHead = false;
89
+ }
90
+ }
91
+ bodyLines.push(bodyLine);
92
+ headLines.push(headLine);
93
+ if (opensStyle && !closesStyle)
94
+ inStyle = true;
95
+ if (closesStyle)
96
+ inStyle = false;
97
+ if (opensScript && !closesScript)
98
+ inScript = true;
99
+ if (closesScript)
100
+ inScript = false;
101
+ continue;
102
+ }
103
+ if (isJsControlLine(trimmed) && !/<\/?head(?:\s|>)/i.test(line)) {
104
+ bodyLines.push(line);
105
+ headLines.push(line);
106
+ continue;
107
+ }
108
+ let remaining = line;
109
+ let bodyLine = '';
110
+ let headLine = '';
111
+ while (remaining.length > 0) {
112
+ if (!inHead) {
113
+ const openMatch = remaining.match(/<head(?:\s[^>]*)?>/i);
114
+ if (!openMatch || openMatch.index === undefined) {
115
+ bodyLine += remaining;
116
+ break;
117
+ }
118
+ bodyLine += remaining.slice(0, openMatch.index);
119
+ remaining = remaining.slice(openMatch.index + openMatch[0].length);
120
+ const closeMatch = remaining.match(/<\/head>/i);
121
+ if (!closeMatch || closeMatch.index === undefined) {
122
+ headLine += remaining;
123
+ remaining = '';
124
+ inHead = true;
125
+ break;
126
+ }
127
+ headLine += remaining.slice(0, closeMatch.index);
128
+ remaining = remaining.slice(closeMatch.index + closeMatch[0].length);
129
+ }
130
+ else {
131
+ const closeMatch = remaining.match(/<\/head>/i);
132
+ if (!closeMatch || closeMatch.index === undefined) {
133
+ headLine += remaining;
134
+ remaining = '';
135
+ break;
136
+ }
137
+ headLine += remaining.slice(0, closeMatch.index);
138
+ remaining = remaining.slice(closeMatch.index + closeMatch[0].length);
139
+ inHead = false;
140
+ }
141
+ }
142
+ bodyLines.push(bodyLine);
143
+ headLines.push(headLine);
144
+ }
145
+ return {
146
+ bodyTemplate: bodyLines.join('\n'),
147
+ headTemplate: headLines.join('\n'),
148
+ };
149
+ }
150
+ function buildAppendStatement(expression, emitCall) {
151
+ return emitCall ? `${emitCall}(${expression});` : `__html += ${expression};`;
152
+ }
153
+ function extractPollFragmentExpr(tagText, rpcNameMap) {
154
+ const pollMatch = tagText.match(/\bdata-poll=\{([\s\S]*?)\}/);
155
+ if (!pollMatch)
156
+ return null;
157
+ const inner = pollMatch[1].trim();
158
+ const callMatch = inner.match(/^([A-Za-z_$][\w$]*)\(([\s\S]*)\)$/);
159
+ if (!callMatch)
160
+ return null;
161
+ const fnName = callMatch[1];
162
+ const rpcName = rpcNameMap?.get(fnName) || fnName;
163
+ const argsExpr = (callMatch[2] || '').trim();
164
+ if (!argsExpr)
165
+ return JSON.stringify(`__poll_${rpcName}`);
166
+ return `'__poll_' + String(${argsExpr}).replace(/[^a-zA-Z0-9]/g, '_')`;
167
+ }
168
+ function findTagEnd(template, start) {
169
+ let quote = null;
170
+ let escaped = false;
171
+ let braceDepth = 0;
172
+ for (let i = start; i < template.length; i++) {
173
+ const ch = template[i];
174
+ if (quote) {
175
+ if (escaped) {
176
+ escaped = false;
177
+ continue;
178
+ }
179
+ if (ch === '\\') {
180
+ escaped = true;
181
+ continue;
182
+ }
183
+ if (ch === quote)
184
+ quote = null;
185
+ continue;
186
+ }
187
+ if (ch === '"' || ch === "'" || ch === '`') {
188
+ quote = ch;
189
+ continue;
190
+ }
191
+ if (ch === '{') {
192
+ braceDepth++;
193
+ continue;
194
+ }
195
+ if (ch === '}') {
196
+ braceDepth = Math.max(0, braceDepth - 1);
197
+ continue;
198
+ }
199
+ if (ch === '>' && braceDepth === 0) {
200
+ return i;
201
+ }
202
+ }
203
+ return -1;
204
+ }
205
+ function instrumentPollFragments(template, rpcNameMap) {
206
+ const out = [];
207
+ const stack = [];
208
+ let cursor = 0;
209
+ while (cursor < template.length) {
210
+ const lt = template.indexOf('<', cursor);
211
+ if (lt === -1) {
212
+ out.push(template.slice(cursor));
213
+ break;
214
+ }
215
+ out.push(template.slice(cursor, lt));
216
+ if (template.startsWith('<!--', lt)) {
217
+ const commentEnd = template.indexOf('-->', lt + 4);
218
+ if (commentEnd === -1) {
219
+ out.push(template.slice(lt));
220
+ break;
221
+ }
222
+ out.push(template.slice(lt, commentEnd + 3));
223
+ cursor = commentEnd + 3;
224
+ continue;
225
+ }
226
+ const tagEnd = findTagEnd(template, lt + 1);
227
+ if (tagEnd === -1) {
228
+ out.push(template.slice(lt));
229
+ break;
230
+ }
231
+ const tagText = template.slice(lt, tagEnd + 1);
232
+ const closingMatch = tagText.match(/^<\s*\/\s*([A-Za-z][\w:-]*)\s*>$/);
233
+ if (closingMatch) {
234
+ const closingTag = closingMatch[1].toLowerCase();
235
+ const last = stack[stack.length - 1];
236
+ if (last && last.tagName === closingTag) {
237
+ if (last.fragmentExpr)
238
+ out.push(FRAGMENT_CLOSE_MARKER);
239
+ stack.pop();
240
+ }
241
+ out.push(tagText);
242
+ cursor = tagEnd + 1;
243
+ continue;
244
+ }
245
+ const openMatch = tagText.match(/^<\s*([A-Za-z][\w:-]*)\b/);
246
+ out.push(tagText);
247
+ if (!openMatch) {
248
+ cursor = tagEnd + 1;
249
+ continue;
250
+ }
251
+ const tagName = openMatch[1].toLowerCase();
252
+ const isVoidLike = /\/\s*>$/.test(tagText) || /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i.test(tagName);
253
+ const fragmentExpr = extractPollFragmentExpr(tagText, rpcNameMap);
254
+ if (fragmentExpr) {
255
+ out.push(`${FRAGMENT_OPEN_MARKER}${encodeURIComponent(fragmentExpr)}-->`);
256
+ if (isVoidLike) {
257
+ out.push(FRAGMENT_CLOSE_MARKER);
258
+ }
259
+ }
260
+ if (!isVoidLike) {
261
+ stack.push({ tagName, fragmentExpr });
262
+ }
263
+ cursor = tagEnd + 1;
264
+ }
265
+ return out.join('');
266
+ }
40
267
  /**
41
268
  * Compile a template string into a JS render function body.
42
269
  *
43
270
  * The generated code expects `data` in scope (destructured load return)
44
271
  * and an `__esc` helper for HTML-escaping.
45
272
  */
46
- export function compileTemplate(template, componentNames, actionNames, rpcNameMap) {
273
+ export function compileTemplate(template, componentNames, actionNames, rpcNameMap, options = {}) {
274
+ const emitCall = options.emitCall;
275
+ if (options.enableFragmentManifest) {
276
+ template = instrumentPollFragments(template, rpcNameMap);
277
+ }
47
278
  const out = ['let __html = "";'];
48
279
  const lines = template.split('\n');
49
280
  let inStyle = false;
@@ -58,7 +289,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
58
289
  if (trimmed.match(/<style[\s>]/i))
59
290
  inStyle = true;
60
291
  if (inStyle) {
61
- out.push(`__html += \`${escapeLiteral(line)}\\n\`;`);
292
+ out.push(buildAppendStatement(`\`${escapeLiteral(line)}\\n\``, emitCall));
62
293
  if (trimmed.match(/<\/style>/i))
63
294
  inStyle = false;
64
295
  continue;
@@ -70,7 +301,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
70
301
  if (trimmed.match(/<\/script>/i)) {
71
302
  const transformed = transformClientScriptBlock(scriptBuffer.join('\n'));
72
303
  for (const scriptLine of transformed.split('\n')) {
73
- out.push(`__html += \`${escapeLiteral(scriptLine)}\\n\`;`);
304
+ out.push(buildAppendStatement(`\`${escapeLiteral(scriptLine)}\\n\``, emitCall));
74
305
  }
75
306
  scriptBuffer = [];
76
307
  inScript = false;
@@ -82,7 +313,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
82
313
  if (trimmed.match(/<\/script>/i)) {
83
314
  const transformed = transformClientScriptBlock(scriptBuffer.join('\n'));
84
315
  for (const scriptLine of transformed.split('\n')) {
85
- out.push(`__html += \`${escapeLiteral(scriptLine)}\\n\`;`);
316
+ out.push(buildAppendStatement(`\`${escapeLiteral(scriptLine)}\\n\``, emitCall));
86
317
  }
87
318
  scriptBuffer = [];
88
319
  inScript = false;
@@ -92,7 +323,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
92
323
  const startedInsideQuotedAttr = !!htmlAttrQuote;
93
324
  const nextHtmlState = advanceHtmlTagState(line, inHtmlTag, htmlAttrQuote);
94
325
  if (startedInsideQuotedAttr) {
95
- out.push(`__html += \`${escapeLiteral(line)}\n\`;`);
326
+ out.push(buildAppendStatement(`\`${escapeLiteral(line)}\n\``, emitCall));
96
327
  inHtmlTag = nextHtmlState.inTag;
97
328
  htmlAttrQuote = nextHtmlState.quote;
98
329
  continue;
@@ -101,13 +332,13 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
101
332
  htmlAttrQuote = nextHtmlState.quote;
102
333
  // Skip empty lines
103
334
  if (!trimmed) {
104
- out.push('__html += "\\n";');
335
+ out.push(buildAppendStatement(`"\\n"`, emitCall));
105
336
  continue;
106
337
  }
107
338
  // One-line inline if/else with branch content:
108
339
  // if (cond) { text/html } else { text/html }
109
340
  // Compile branch content as template output instead of raw JS.
110
- const inlineIfElse = tryCompileInlineIfElseLine(trimmed, actionNames, rpcNameMap);
341
+ const inlineIfElse = tryCompileInlineIfElseLine(trimmed, actionNames, rpcNameMap, options);
111
342
  if (inlineIfElse) {
112
343
  out.push(...inlineIfElse);
113
344
  continue;
@@ -137,7 +368,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
137
368
  }
138
369
  }
139
370
  // Self-closing: <Card attr="x" />
140
- const componentLine = tryCompileComponentTag(joinedTrimmed, componentNames, actionNames, rpcNameMap);
371
+ const componentLine = tryCompileComponentTag(joinedTrimmed, componentNames, actionNames, rpcNameMap, options);
141
372
  if (componentLine) {
142
373
  i += joinedExtra;
143
374
  out.push(componentLine);
@@ -167,10 +398,12 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
167
398
  }
168
399
  // Compile children into a sub-render block
169
400
  const childTemplate = childLines.join('\n');
170
- const childBody = compileTemplate(childTemplate, componentNames, actionNames, rpcNameMap);
401
+ const childBody = compileTemplate(childTemplate, componentNames, actionNames, rpcNameMap, {
402
+ clientRouteRegistry: options.clientRouteRegistry,
403
+ });
171
404
  // Wrap in an IIFE that returns the children HTML
172
405
  const childrenExpr = `(function() { ${childBody}; return __html; })()`;
173
- out.push(`__html += ${openResult.funcName}({ ${openResult.propsStr}${openResult.propsStr ? ', ' : ''}children: ${childrenExpr} }, __esc);`);
406
+ out.push(buildAppendStatement(`${openResult.funcName}({ ${openResult.propsStr}${openResult.propsStr ? ', ' : ''}children: ${childrenExpr} }, __esc)`, emitCall));
174
407
  continue;
175
408
  }
176
409
  }
@@ -187,7 +420,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
187
420
  }
188
421
  i += extraLines;
189
422
  }
190
- out.push(compileHtmlLine(htmlLine, actionNames, rpcNameMap));
423
+ out.push(...compileHtmlLineStatements(htmlLine, actionNames, rpcNameMap, options));
191
424
  }
192
425
  return out.join('\n');
193
426
  }
@@ -441,14 +674,13 @@ function findMatching(src, openPos, openChar, closeChar) {
441
674
  }
442
675
  return -1;
443
676
  }
444
- function compileInlineBranchContent(content, actionNames, rpcNameMap) {
677
+ function compileInlineBranchContent(content, actionNames, rpcNameMap, options = {}) {
445
678
  const c = content.trim();
446
679
  if (!c)
447
680
  return [];
448
- const compiled = compileHtmlLine(c, actionNames, rpcNameMap);
449
- return [compiled.replace(/\\n`;/, '`;')];
681
+ return compileHtmlLineStatements(c, actionNames, rpcNameMap, { ...options, appendNewline: false });
450
682
  }
451
- function tryCompileInlineIfElseLine(line, actionNames, rpcNameMap) {
683
+ function tryCompileInlineIfElseLine(line, actionNames, rpcNameMap, options = {}) {
452
684
  if (!line.startsWith('if'))
453
685
  return null;
454
686
  const ifMatch = line.match(/^if\s*\(/);
@@ -489,9 +721,9 @@ function tryCompileInlineIfElseLine(line, actionNames, rpcNameMap) {
489
721
  const elseContent = line.slice(elseOpen + 1, elseClose);
490
722
  const out = [];
491
723
  out.push(`if (${condition}) {`);
492
- out.push(...compileInlineBranchContent(thenContent, actionNames, rpcNameMap));
724
+ out.push(...compileInlineBranchContent(thenContent, actionNames, rpcNameMap, options));
493
725
  out.push(`} else {`);
494
- out.push(...compileInlineBranchContent(elseContent, actionNames, rpcNameMap));
726
+ out.push(...compileInlineBranchContent(elseContent, actionNames, rpcNameMap, options));
495
727
  out.push(`}`);
496
728
  return out;
497
729
  }
@@ -575,7 +807,7 @@ function parseComponentAttrs(attrsStr, actionNames) {
575
807
  * Matches: <StatCard attr="literal" attr={expr} />
576
808
  * Generates: __html += __c_stat_card({ attr: "literal", attr: (expr) }, __esc);
577
809
  */
578
- function tryCompileComponentTag(line, componentNames, actionNames, rpcNameMap) {
810
+ function tryCompileComponentTag(line, componentNames, actionNames, rpcNameMap, options = {}) {
579
811
  // Match self-closing tags: <TagName ...attrs... />
580
812
  const selfCloseMatch = line.match(/^<([A-Z]\w*)\s*(.*?)\s*\/>\s*$/);
581
813
  // Match open+close on same line with no content: <TagName ...attrs...></TagName>
@@ -590,7 +822,7 @@ function tryCompileComponentTag(line, componentNames, actionNames, rpcNameMap) {
590
822
  return null;
591
823
  const funcName = componentFuncName(fileName);
592
824
  const propsStr = parseComponentAttrs(match[2].trim(), actionNames);
593
- return `__html += ${funcName}({ ${propsStr} }, __esc);`;
825
+ return buildAppendStatement(`${funcName}({ ${propsStr} }, __esc)`, options.emitCall);
594
826
  }
595
827
  if (inlinePairMatch) {
596
828
  const tagName = inlinePairMatch[1];
@@ -601,9 +833,11 @@ function tryCompileComponentTag(line, componentNames, actionNames, rpcNameMap) {
601
833
  const propsStr = parseComponentAttrs(inlinePairMatch[2].trim(), actionNames);
602
834
  const innerContent = inlinePairMatch[3];
603
835
  // Compile the inline content as a mini-template to handle {expr} interpolation
604
- const childBody = compileTemplate(innerContent, componentNames, actionNames, rpcNameMap);
836
+ const childBody = compileTemplate(innerContent, componentNames, actionNames, rpcNameMap, {
837
+ clientRouteRegistry: options.clientRouteRegistry,
838
+ });
605
839
  const childrenExpr = `(function() { ${childBody}; return __html; })()`;
606
- return `__html += ${funcName}({ ${propsStr}${propsStr ? ', ' : ''}children: ${childrenExpr} }, __esc);`;
840
+ return buildAppendStatement(`${funcName}({ ${propsStr}${propsStr ? ', ' : ''}children: ${childrenExpr} }, __esc)`, options.emitCall);
607
841
  }
608
842
  return null;
609
843
  }
@@ -642,17 +876,47 @@ function expandShorthands(line) {
642
876
  });
643
877
  return line;
644
878
  }
879
+ function compileHtmlLineStatements(line, actionNames, rpcNameMap, options = {}) {
880
+ const markerRegex = /<!--__KURATCHI_FRAGMENT_(OPEN:([\s\S]*?)|CLOSE)-->/g;
881
+ const statements = [];
882
+ let cursor = 0;
883
+ let sawLiteral = false;
884
+ let match;
885
+ while ((match = markerRegex.exec(line)) !== null) {
886
+ const literal = line.slice(cursor, match.index);
887
+ if (literal) {
888
+ statements.push(compileHtmlSegment(literal, actionNames, rpcNameMap, { ...options, appendNewline: false }));
889
+ sawLiteral = true;
890
+ }
891
+ if (match[1].startsWith('OPEN:')) {
892
+ const encodedExpr = match[2] || '';
893
+ statements.push(`__pushFragment(${decodeURIComponent(encodedExpr)});`);
894
+ }
895
+ else {
896
+ statements.push(`__popFragment();`);
897
+ }
898
+ cursor = match.index + match[0].length;
899
+ }
900
+ const tail = line.slice(cursor);
901
+ if (tail || sawLiteral || options.appendNewline !== false) {
902
+ const appendNewline = options.appendNewline !== false;
903
+ if (tail || appendNewline) {
904
+ statements.push(compileHtmlSegment(tail, actionNames, rpcNameMap, { ...options, appendNewline }));
905
+ }
906
+ }
907
+ return statements.filter(Boolean);
908
+ }
645
909
  /**
646
- * Compile a single HTML line, replacing {expr} with escaped output,
910
+ * Compile a single HTML segment, replacing {expr} with escaped output,
647
911
  * {@html expr} with sanitized HTML, and {@raw expr} with raw output.
648
912
  * Handles attribute values like value={x}.
649
913
  */
650
- function compileHtmlLine(line, actionNames, rpcNameMap) {
914
+ function compileHtmlSegment(line, actionNames, rpcNameMap, options = {}) {
651
915
  // Expand shorthand syntax before main compilation
652
916
  line = expandShorthands(line);
653
917
  let result = '';
654
918
  let pos = 0;
655
- let hasExpr = false;
919
+ let pendingActionHiddenInput = null;
656
920
  while (pos < line.length) {
657
921
  const braceIdx = findNextTemplateBrace(line, pos);
658
922
  if (braceIdx === -1) {
@@ -671,19 +935,16 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
671
935
  if (inner.startsWith('@html ')) {
672
936
  const expr = inner.slice(6).trim();
673
937
  result += `\${__sanitizeHtml(${expr})}`;
674
- hasExpr = true;
675
938
  }
676
939
  else if (inner.startsWith('@raw ')) {
677
940
  // Unsafe raw HTML: {@raw expr}
678
941
  const expr = inner.slice(5).trim();
679
942
  result += `\${__rawHtml(${expr})}`;
680
- hasExpr = true;
681
943
  }
682
944
  else if (inner.startsWith('=html ')) {
683
945
  // Legacy alias for raw HTML: {=html expr}
684
946
  const expr = inner.slice(6).trim();
685
947
  result += `\${__rawHtml(${expr})}`;
686
- hasExpr = true;
687
948
  }
688
949
  else {
689
950
  // Check if this is an attribute value: attr={expr}
@@ -697,7 +958,6 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
697
958
  if (attrName === 'data-dialog-data') {
698
959
  // data-dialog-data={expr} → data-dialog-data="JSON.stringify(expr)" (HTML-escaped)
699
960
  result += `"\${__esc(JSON.stringify(${inner}))}"`;
700
- hasExpr = true;
701
961
  pos = closeIdx + 1;
702
962
  continue;
703
963
  }
@@ -719,7 +979,6 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
719
979
  else {
720
980
  result += ` data-refresh="\${__esc(${inner})}"`;
721
981
  }
722
- hasExpr = true;
723
982
  pos = closeIdx + 1;
724
983
  continue;
725
984
  }
@@ -737,7 +996,6 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
737
996
  else {
738
997
  result += ` ${attrName}="${escapeLiteral(inner)}"`;
739
998
  }
740
- hasExpr = true;
741
999
  pos = closeIdx + 1;
742
1000
  continue;
743
1001
  }
@@ -759,12 +1017,11 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
759
1017
  // Non-call expression mode (e.g., data-get={someUrl})
760
1018
  result += ` ${attrName}="\${__esc(${inner})}"`;
761
1019
  }
762
- hasExpr = true;
763
1020
  pos = closeIdx + 1;
764
1021
  continue;
765
1022
  }
766
1023
  else if (attrName === 'data-poll') {
767
- // data-poll={fn(args)} → data-poll="fnName" data-poll-args="[serialized]"
1024
+ // data-poll={fn(args)} data-poll="fnName" data-poll-args="[serialized]" data-poll-id="stable-id"
768
1025
  const pollCallMatch = inner.match(/^(\w+)\((.*)\)$/);
769
1026
  if (pollCallMatch) {
770
1027
  const fnName = pollCallMatch[1];
@@ -772,13 +1029,18 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
772
1029
  const argsExpr = pollCallMatch[2].trim();
773
1030
  // Remove the trailing "data-poll=" we already appended
774
1031
  result = result.replace(/\s*data-poll=$/, '');
775
- // Emit data-poll and data-poll-args attributes
1032
+ // Emit data-poll, data-poll-args, and stable data-poll-id (based on fn + args expression)
776
1033
  result += ` data-poll="${rpcName}"`;
777
1034
  if (argsExpr) {
778
1035
  result += ` data-poll-args="\${__esc(JSON.stringify([${argsExpr}]))}"`;
1036
+ // Stable ID based on args so same data produces same ID across renders
1037
+ result += ` data-poll-id="\${__esc('__poll_' + String(${argsExpr}).replace(/[^a-zA-Z0-9]/g, '_'))}"`;
1038
+ }
1039
+ else {
1040
+ // No args - use function name as ID
1041
+ result += ` data-poll-id="__poll_${rpcName}"`;
779
1042
  }
780
1043
  }
781
- hasExpr = true;
782
1044
  pos = closeIdx + 1;
783
1045
  continue;
784
1046
  }
@@ -794,23 +1056,14 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
794
1056
  }
795
1057
  // Remove trailing `action=` from output and inject _action hidden field.
796
1058
  result = result.replace(/\s*action=$/, '');
1059
+ const actionValue = actionNames === undefined
1060
+ ? `\${__esc(${inner})}`
1061
+ : inner;
1062
+ pendingActionHiddenInput = `\\n<input type="hidden" name="_action" value="${actionValue}">`;
797
1063
  pos = closeIdx + 1;
798
- const tagEnd = line.indexOf('>', pos);
799
- if (tagEnd !== -1) {
800
- result += escapeLiteral(line.slice(pos, tagEnd + 1));
801
- // In a route context, inner is the literal action function name (a string key).
802
- // In a component context (actionNames === undefined), inner is a prop name whose
803
- // value at runtime is the action name string — emit it as a dynamic expression.
804
- const actionValue = actionNames === undefined
805
- ? `\${__esc(${inner})}`
806
- : inner;
807
- result += `\\n<input type="hidden" name="_action" value="${actionValue}">`;
808
- pos = tagEnd + 1;
809
- }
810
- hasExpr = true;
811
1064
  continue;
812
1065
  }
813
- else if (/^on[A-Za-z]+$/.test(attrName)) {
1066
+ else if (/^on[A-Za-z]+$/i.test(attrName)) {
814
1067
  // onClick/onChange/...={expr} — check if it's a server action or plain client JS
815
1068
  const eventName = attrName.slice(2).toLowerCase();
816
1069
  const actionCallMatch = inner.match(/^(\w+)\((.*)\)$/);
@@ -825,12 +1078,25 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
825
1078
  result += ` data-action="${fnName}" data-args="\${__esc(JSON.stringify([${argsExpr}]))}" data-action-event="${eventName}"`;
826
1079
  }
827
1080
  else {
828
- // Plain client-side event handler: onClick={myFn()}
829
- // Emit as native inline handler (lowercased event attribute).
830
- result = result.replace(new RegExp(`\\s*${attrName}=$`), ` on${eventName}=`);
831
- result += `"${escapeLiteral(inner)}"`;
1081
+ const clientRegistration = options.clientRouteRegistry?.registerEventHandler(eventName, inner) ?? null;
1082
+ if (clientRegistration) {
1083
+ result = result.replace(new RegExp(`\\s*${attrName}=$`), '');
1084
+ result += ` data-client-route="${clientRegistration.routeId}" data-client-handler="${clientRegistration.handlerId}" data-client-event="${eventName}"`;
1085
+ if (clientRegistration.argsExpr) {
1086
+ result += ` data-client-args="\${__esc(JSON.stringify([${clientRegistration.argsExpr}]))}"`;
1087
+ }
1088
+ }
1089
+ else if (options.clientRouteRegistry?.hasBindingReference(inner)) {
1090
+ throw new Error(`Unsupported client handler expression: "${inner}". ` +
1091
+ `Top-level $client/$shared event handlers must be a direct function reference or call, like onClick={openDialog()} or onClick={helpers.openDialog()}.`);
1092
+ }
1093
+ else {
1094
+ // Plain client-side event handler: onClick={myFn()}
1095
+ // Emit as native inline handler (lowercased event attribute).
1096
+ result = result.replace(new RegExp(`\\s*${attrName}=$`), ` on${eventName}=`);
1097
+ result += `"${escapeLiteral(inner)}"`;
1098
+ }
832
1099
  }
833
- hasExpr = true;
834
1100
  pos = closeIdx + 1;
835
1101
  continue;
836
1102
  }
@@ -846,17 +1112,14 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
846
1112
  else {
847
1113
  result += `\${__esc(${inner})}`;
848
1114
  }
849
- hasExpr = true;
850
1115
  }
851
1116
  pos = closeIdx + 1;
852
1117
  }
853
- // If the line had expressions, use template literal; otherwise plain string
854
- if (hasExpr) {
855
- return `__html += \`${result}\\n\`;`;
856
- }
857
- else {
858
- return `__html += \`${result}\\n\`;`;
1118
+ if (pendingActionHiddenInput && result.includes('>')) {
1119
+ const gtIndex = result.indexOf('>');
1120
+ result = result.slice(0, gtIndex + 1) + pendingActionHiddenInput + result.slice(gtIndex + 1);
859
1121
  }
1122
+ return buildAppendStatement(`\`${result}${options.appendNewline === false ? '' : '\\n'}\``, options.emitCall);
860
1123
  }
861
1124
  /** Escape characters that would break a JS template literal. */
862
1125
  function escapeLiteral(text) {
@@ -0,0 +1,13 @@
1
+ export interface WorkerClassExportEntry {
2
+ className: string;
3
+ exportKind: 'named' | 'default';
4
+ file: string;
5
+ }
6
+ export declare function resolveRuntimeImportPath(projectDir: string): string | null;
7
+ export declare function toWorkerImportPath(projectDir: string, outDir: string, filePath: string): string;
8
+ export declare function buildWorkerEntrypointSource(opts: {
9
+ projectDir: string;
10
+ outDir: string;
11
+ doClassNames: string[];
12
+ workerClassEntries: WorkerClassExportEntry[];
13
+ }): string;
@@ -0,0 +1,37 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ export function resolveRuntimeImportPath(projectDir) {
4
+ const candidates = [
5
+ { file: 'src/server/runtime.hook.ts', importPath: '../src/server/runtime.hook' },
6
+ ];
7
+ for (const candidate of candidates) {
8
+ if (fs.existsSync(path.join(projectDir, candidate.file))) {
9
+ return candidate.importPath;
10
+ }
11
+ }
12
+ return null;
13
+ }
14
+ export function toWorkerImportPath(projectDir, outDir, filePath) {
15
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
16
+ let rel = path.relative(outDir, absPath).replace(/\\/g, '/');
17
+ if (!rel.startsWith('.'))
18
+ rel = `./${rel}`;
19
+ return rel.replace(/\.(ts|js|mjs|cjs)$/, '');
20
+ }
21
+ export function buildWorkerEntrypointSource(opts) {
22
+ const workerClassExports = opts.workerClassEntries
23
+ .map((entry) => {
24
+ const importPath = toWorkerImportPath(opts.projectDir, opts.outDir, entry.file);
25
+ if (entry.exportKind === 'default') {
26
+ return `export { default as ${entry.className} } from '${importPath}';`;
27
+ }
28
+ return `export { ${entry.className} } from '${importPath}';`;
29
+ });
30
+ return [
31
+ '// Auto-generated by kuratchi — do not edit.',
32
+ "export { default } from './routes.js';",
33
+ ...opts.doClassNames.map((className) => `export { ${className} } from './routes.js';`),
34
+ ...workerClassExports,
35
+ '',
36
+ ].join('\n');
37
+ }
@@ -0,0 +1,14 @@
1
+ export interface WranglerSyncEntry {
2
+ binding: string;
3
+ className: string;
4
+ }
5
+ export interface WranglerSyncConfig {
6
+ workflows: WranglerSyncEntry[];
7
+ containers: WranglerSyncEntry[];
8
+ durableObjects: WranglerSyncEntry[];
9
+ }
10
+ export declare function syncWranglerConfig(opts: {
11
+ projectDir: string;
12
+ config: WranglerSyncConfig;
13
+ writeFile: (filePath: string, content: string) => void;
14
+ }): void;