@kuratchi/js 0.0.15 → 0.0.17

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 (70) hide show
  1. package/README.md +160 -1
  2. package/dist/cli.js +78 -45
  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 +73 -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 +158 -0
  13. package/dist/compiler/config-reading.d.ts +12 -0
  14. package/dist/compiler/config-reading.js +380 -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 +140 -0
  23. package/dist/compiler/index.d.ts +7 -7
  24. package/dist/compiler/index.js +181 -3321
  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 +436 -55
  31. package/dist/compiler/root-layout-pipeline.d.ts +10 -0
  32. package/dist/compiler/root-layout-pipeline.js +532 -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 +291 -0
  37. package/dist/compiler/route-state-pipeline.d.ts +26 -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 +91 -0
  45. package/dist/compiler/routes-module-types.d.ts +45 -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 +337 -71
  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/context.d.ts +4 -0
  59. package/dist/runtime/context.js +40 -2
  60. package/dist/runtime/do.js +21 -6
  61. package/dist/runtime/generated-worker.d.ts +55 -0
  62. package/dist/runtime/generated-worker.js +543 -0
  63. package/dist/runtime/index.d.ts +4 -1
  64. package/dist/runtime/index.js +2 -0
  65. package/dist/runtime/router.d.ts +6 -1
  66. package/dist/runtime/router.js +125 -31
  67. package/dist/runtime/security.d.ts +101 -0
  68. package/dist/runtime/security.js +298 -0
  69. package/dist/runtime/types.d.ts +29 -2
  70. package/package.json +5 -1
@@ -37,14 +37,249 @@ 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
+ // When emitCall is provided, use it (e.g., __emit(expr))
152
+ // Otherwise, use array push for O(n) performance
153
+ return emitCall ? `${emitCall}(${expression});` : `__parts.push(${expression});`;
154
+ }
155
+ function extractPollFragmentExpr(tagText, rpcNameMap) {
156
+ const pollMatch = tagText.match(/\bdata-poll=\{([\s\S]*?)\}/);
157
+ if (!pollMatch)
158
+ return null;
159
+ const inner = pollMatch[1].trim();
160
+ const callMatch = inner.match(/^([A-Za-z_$][\w$]*)\(([\s\S]*)\)$/);
161
+ if (!callMatch)
162
+ return null;
163
+ const fnName = callMatch[1];
164
+ const rpcName = rpcNameMap?.get(fnName) || fnName;
165
+ const argsExpr = (callMatch[2] || '').trim();
166
+ if (!argsExpr)
167
+ return JSON.stringify(`__poll_${rpcName}`);
168
+ return `'__poll_' + String(${argsExpr}).replace(/[^a-zA-Z0-9]/g, '_')`;
169
+ }
170
+ function findTagEnd(template, start) {
171
+ let quote = null;
172
+ let escaped = false;
173
+ let braceDepth = 0;
174
+ for (let i = start; i < template.length; i++) {
175
+ const ch = template[i];
176
+ if (quote) {
177
+ if (escaped) {
178
+ escaped = false;
179
+ continue;
180
+ }
181
+ if (ch === '\\') {
182
+ escaped = true;
183
+ continue;
184
+ }
185
+ if (ch === quote)
186
+ quote = null;
187
+ continue;
188
+ }
189
+ if (ch === '"' || ch === "'" || ch === '`') {
190
+ quote = ch;
191
+ continue;
192
+ }
193
+ if (ch === '{') {
194
+ braceDepth++;
195
+ continue;
196
+ }
197
+ if (ch === '}') {
198
+ braceDepth = Math.max(0, braceDepth - 1);
199
+ continue;
200
+ }
201
+ if (ch === '>' && braceDepth === 0) {
202
+ return i;
203
+ }
204
+ }
205
+ return -1;
206
+ }
207
+ function instrumentPollFragments(template, rpcNameMap) {
208
+ const out = [];
209
+ const stack = [];
210
+ let cursor = 0;
211
+ while (cursor < template.length) {
212
+ const lt = template.indexOf('<', cursor);
213
+ if (lt === -1) {
214
+ out.push(template.slice(cursor));
215
+ break;
216
+ }
217
+ out.push(template.slice(cursor, lt));
218
+ if (template.startsWith('<!--', lt)) {
219
+ const commentEnd = template.indexOf('-->', lt + 4);
220
+ if (commentEnd === -1) {
221
+ out.push(template.slice(lt));
222
+ break;
223
+ }
224
+ out.push(template.slice(lt, commentEnd + 3));
225
+ cursor = commentEnd + 3;
226
+ continue;
227
+ }
228
+ const tagEnd = findTagEnd(template, lt + 1);
229
+ if (tagEnd === -1) {
230
+ out.push(template.slice(lt));
231
+ break;
232
+ }
233
+ const tagText = template.slice(lt, tagEnd + 1);
234
+ const closingMatch = tagText.match(/^<\s*\/\s*([A-Za-z][\w:-]*)\s*>$/);
235
+ if (closingMatch) {
236
+ const closingTag = closingMatch[1].toLowerCase();
237
+ const last = stack[stack.length - 1];
238
+ if (last && last.tagName === closingTag) {
239
+ if (last.fragmentExpr)
240
+ out.push(FRAGMENT_CLOSE_MARKER);
241
+ stack.pop();
242
+ }
243
+ out.push(tagText);
244
+ cursor = tagEnd + 1;
245
+ continue;
246
+ }
247
+ const openMatch = tagText.match(/^<\s*([A-Za-z][\w:-]*)\b/);
248
+ out.push(tagText);
249
+ if (!openMatch) {
250
+ cursor = tagEnd + 1;
251
+ continue;
252
+ }
253
+ const tagName = openMatch[1].toLowerCase();
254
+ const isVoidLike = /\/\s*>$/.test(tagText) || /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i.test(tagName);
255
+ const fragmentExpr = extractPollFragmentExpr(tagText, rpcNameMap);
256
+ if (fragmentExpr) {
257
+ out.push(`${FRAGMENT_OPEN_MARKER}${encodeURIComponent(fragmentExpr)}-->`);
258
+ if (isVoidLike) {
259
+ out.push(FRAGMENT_CLOSE_MARKER);
260
+ }
261
+ }
262
+ if (!isVoidLike) {
263
+ stack.push({ tagName, fragmentExpr });
264
+ }
265
+ cursor = tagEnd + 1;
266
+ }
267
+ return out.join('');
268
+ }
40
269
  /**
41
270
  * Compile a template string into a JS render function body.
42
271
  *
43
272
  * The generated code expects `data` in scope (destructured load return)
44
273
  * and an `__esc` helper for HTML-escaping.
45
274
  */
46
- export function compileTemplate(template, componentNames, actionNames, rpcNameMap) {
47
- const out = ['let __html = "";'];
275
+ export function compileTemplate(template, componentNames, actionNames, rpcNameMap, options = {}) {
276
+ const emitCall = options.emitCall;
277
+ if (options.enableFragmentManifest) {
278
+ template = instrumentPollFragments(template, rpcNameMap);
279
+ }
280
+ // Use array accumulation for O(n) performance instead of O(n²) string concatenation
281
+ const useArrayAccum = !emitCall;
282
+ const out = useArrayAccum ? ['const __parts = [];'] : ['let __html = "";'];
48
283
  const lines = template.split('\n');
49
284
  let inStyle = false;
50
285
  let inScript = false;
@@ -58,7 +293,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
58
293
  if (trimmed.match(/<style[\s>]/i))
59
294
  inStyle = true;
60
295
  if (inStyle) {
61
- out.push(`__html += \`${escapeLiteral(line)}\\n\`;`);
296
+ out.push(buildAppendStatement(`\`${escapeLiteral(line)}\\n\``, emitCall));
62
297
  if (trimmed.match(/<\/style>/i))
63
298
  inStyle = false;
64
299
  continue;
@@ -70,7 +305,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
70
305
  if (trimmed.match(/<\/script>/i)) {
71
306
  const transformed = transformClientScriptBlock(scriptBuffer.join('\n'));
72
307
  for (const scriptLine of transformed.split('\n')) {
73
- out.push(`__html += \`${escapeLiteral(scriptLine)}\\n\`;`);
308
+ out.push(buildAppendStatement(`\`${escapeLiteral(scriptLine)}\\n\``, emitCall));
74
309
  }
75
310
  scriptBuffer = [];
76
311
  inScript = false;
@@ -82,7 +317,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
82
317
  if (trimmed.match(/<\/script>/i)) {
83
318
  const transformed = transformClientScriptBlock(scriptBuffer.join('\n'));
84
319
  for (const scriptLine of transformed.split('\n')) {
85
- out.push(`__html += \`${escapeLiteral(scriptLine)}\\n\`;`);
320
+ out.push(buildAppendStatement(`\`${escapeLiteral(scriptLine)}\\n\``, emitCall));
86
321
  }
87
322
  scriptBuffer = [];
88
323
  inScript = false;
@@ -92,7 +327,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
92
327
  const startedInsideQuotedAttr = !!htmlAttrQuote;
93
328
  const nextHtmlState = advanceHtmlTagState(line, inHtmlTag, htmlAttrQuote);
94
329
  if (startedInsideQuotedAttr) {
95
- out.push(`__html += \`${escapeLiteral(line)}\n\`;`);
330
+ out.push(buildAppendStatement(`\`${escapeLiteral(line)}\n\``, emitCall));
96
331
  inHtmlTag = nextHtmlState.inTag;
97
332
  htmlAttrQuote = nextHtmlState.quote;
98
333
  continue;
@@ -101,13 +336,13 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
101
336
  htmlAttrQuote = nextHtmlState.quote;
102
337
  // Skip empty lines
103
338
  if (!trimmed) {
104
- out.push('__html += "\\n";');
339
+ out.push(buildAppendStatement(`"\\n"`, emitCall));
105
340
  continue;
106
341
  }
107
342
  // One-line inline if/else with branch content:
108
343
  // if (cond) { text/html } else { text/html }
109
344
  // Compile branch content as template output instead of raw JS.
110
- const inlineIfElse = tryCompileInlineIfElseLine(trimmed, actionNames, rpcNameMap);
345
+ const inlineIfElse = tryCompileInlineIfElseLine(trimmed, actionNames, rpcNameMap, options);
111
346
  if (inlineIfElse) {
112
347
  out.push(...inlineIfElse);
113
348
  continue;
@@ -137,7 +372,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
137
372
  }
138
373
  }
139
374
  // Self-closing: <Card attr="x" />
140
- const componentLine = tryCompileComponentTag(joinedTrimmed, componentNames, actionNames, rpcNameMap);
375
+ const componentLine = tryCompileComponentTag(joinedTrimmed, componentNames, actionNames, rpcNameMap, options);
141
376
  if (componentLine) {
142
377
  i += joinedExtra;
143
378
  out.push(componentLine);
@@ -167,10 +402,12 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
167
402
  }
168
403
  // Compile children into a sub-render block
169
404
  const childTemplate = childLines.join('\n');
170
- const childBody = compileTemplate(childTemplate, componentNames, actionNames, rpcNameMap);
405
+ const childBody = compileTemplate(childTemplate, componentNames, actionNames, rpcNameMap, {
406
+ clientRouteRegistry: options.clientRouteRegistry,
407
+ });
171
408
  // Wrap in an IIFE that returns the children HTML
172
409
  const childrenExpr = `(function() { ${childBody}; return __html; })()`;
173
- out.push(`__html += ${openResult.funcName}({ ${openResult.propsStr}${openResult.propsStr ? ', ' : ''}children: ${childrenExpr} }, __esc);`);
410
+ out.push(buildAppendStatement(`${openResult.funcName}({ ${openResult.propsStr}${openResult.propsStr ? ', ' : ''}children: ${childrenExpr} }, __esc)`, emitCall));
174
411
  continue;
175
412
  }
176
413
  }
@@ -187,7 +424,11 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
187
424
  }
188
425
  i += extraLines;
189
426
  }
190
- out.push(compileHtmlLine(htmlLine, actionNames, rpcNameMap));
427
+ out.push(...compileHtmlLineStatements(htmlLine, actionNames, rpcNameMap, options));
428
+ }
429
+ // For non-emit mode, add final join to produce __html
430
+ if (!emitCall) {
431
+ out.push('let __html = __parts.join(\'\');');
191
432
  }
192
433
  return out.join('\n');
193
434
  }
@@ -262,9 +503,9 @@ function transformClientScriptBlock(block) {
262
503
  const openTag = match[1];
263
504
  const body = match[2];
264
505
  const closeTag = match[3];
506
+ // TypeScript is preserved — wrangler's esbuild handles transpilation
265
507
  if (!/\$\s*:/.test(body)) {
266
- const transpiled = transpileTypeScript(body, 'client-script.ts');
267
- return `${openTag}${transpiled}${closeTag}`;
508
+ return `${openTag}${body}${closeTag}`;
268
509
  }
269
510
  const out = [];
270
511
  const lines = body.split('\n');
@@ -360,8 +601,8 @@ function transformClientScriptBlock(block) {
360
601
  break;
361
602
  }
362
603
  out.splice(insertAt, 0, 'const __k$ = window.__kuratchiReactive;');
363
- const transpiled = transpileTypeScript(out.join('\n'), 'client-script.ts');
364
- return `${openTag}${transpiled}${closeTag}`;
604
+ // TypeScript is preserved — wrangler's esbuild handles transpilation
605
+ return `${openTag}${out.join('\n')}${closeTag}`;
365
606
  }
366
607
  function braceDelta(line) {
367
608
  let delta = 0;
@@ -441,14 +682,13 @@ function findMatching(src, openPos, openChar, closeChar) {
441
682
  }
442
683
  return -1;
443
684
  }
444
- function compileInlineBranchContent(content, actionNames, rpcNameMap) {
685
+ function compileInlineBranchContent(content, actionNames, rpcNameMap, options = {}) {
445
686
  const c = content.trim();
446
687
  if (!c)
447
688
  return [];
448
- const compiled = compileHtmlLine(c, actionNames, rpcNameMap);
449
- return [compiled.replace(/\\n`;/, '`;')];
689
+ return compileHtmlLineStatements(c, actionNames, rpcNameMap, { ...options, appendNewline: false });
450
690
  }
451
- function tryCompileInlineIfElseLine(line, actionNames, rpcNameMap) {
691
+ function tryCompileInlineIfElseLine(line, actionNames, rpcNameMap, options = {}) {
452
692
  if (!line.startsWith('if'))
453
693
  return null;
454
694
  const ifMatch = line.match(/^if\s*\(/);
@@ -489,9 +729,9 @@ function tryCompileInlineIfElseLine(line, actionNames, rpcNameMap) {
489
729
  const elseContent = line.slice(elseOpen + 1, elseClose);
490
730
  const out = [];
491
731
  out.push(`if (${condition}) {`);
492
- out.push(...compileInlineBranchContent(thenContent, actionNames, rpcNameMap));
732
+ out.push(...compileInlineBranchContent(thenContent, actionNames, rpcNameMap, options));
493
733
  out.push(`} else {`);
494
- out.push(...compileInlineBranchContent(elseContent, actionNames, rpcNameMap));
734
+ out.push(...compileInlineBranchContent(elseContent, actionNames, rpcNameMap, options));
495
735
  out.push(`}`);
496
736
  return out;
497
737
  }
@@ -575,7 +815,7 @@ function parseComponentAttrs(attrsStr, actionNames) {
575
815
  * Matches: <StatCard attr="literal" attr={expr} />
576
816
  * Generates: __html += __c_stat_card({ attr: "literal", attr: (expr) }, __esc);
577
817
  */
578
- function tryCompileComponentTag(line, componentNames, actionNames, rpcNameMap) {
818
+ function tryCompileComponentTag(line, componentNames, actionNames, rpcNameMap, options = {}) {
579
819
  // Match self-closing tags: <TagName ...attrs... />
580
820
  const selfCloseMatch = line.match(/^<([A-Z]\w*)\s*(.*?)\s*\/>\s*$/);
581
821
  // Match open+close on same line with no content: <TagName ...attrs...></TagName>
@@ -590,7 +830,7 @@ function tryCompileComponentTag(line, componentNames, actionNames, rpcNameMap) {
590
830
  return null;
591
831
  const funcName = componentFuncName(fileName);
592
832
  const propsStr = parseComponentAttrs(match[2].trim(), actionNames);
593
- return `__html += ${funcName}({ ${propsStr} }, __esc);`;
833
+ return buildAppendStatement(`${funcName}({ ${propsStr} }, __esc)`, options.emitCall);
594
834
  }
595
835
  if (inlinePairMatch) {
596
836
  const tagName = inlinePairMatch[1];
@@ -601,9 +841,11 @@ function tryCompileComponentTag(line, componentNames, actionNames, rpcNameMap) {
601
841
  const propsStr = parseComponentAttrs(inlinePairMatch[2].trim(), actionNames);
602
842
  const innerContent = inlinePairMatch[3];
603
843
  // Compile the inline content as a mini-template to handle {expr} interpolation
604
- const childBody = compileTemplate(innerContent, componentNames, actionNames, rpcNameMap);
844
+ const childBody = compileTemplate(innerContent, componentNames, actionNames, rpcNameMap, {
845
+ clientRouteRegistry: options.clientRouteRegistry,
846
+ });
605
847
  const childrenExpr = `(function() { ${childBody}; return __html; })()`;
606
- return `__html += ${funcName}({ ${propsStr}${propsStr ? ', ' : ''}children: ${childrenExpr} }, __esc);`;
848
+ return buildAppendStatement(`${funcName}({ ${propsStr}${propsStr ? ', ' : ''}children: ${childrenExpr} }, __esc)`, options.emitCall);
607
849
  }
608
850
  return null;
609
851
  }
@@ -642,17 +884,47 @@ function expandShorthands(line) {
642
884
  });
643
885
  return line;
644
886
  }
887
+ function compileHtmlLineStatements(line, actionNames, rpcNameMap, options = {}) {
888
+ const markerRegex = /<!--__KURATCHI_FRAGMENT_(OPEN:([\s\S]*?)|CLOSE)-->/g;
889
+ const statements = [];
890
+ let cursor = 0;
891
+ let sawLiteral = false;
892
+ let match;
893
+ while ((match = markerRegex.exec(line)) !== null) {
894
+ const literal = line.slice(cursor, match.index);
895
+ if (literal) {
896
+ statements.push(compileHtmlSegment(literal, actionNames, rpcNameMap, { ...options, appendNewline: false }));
897
+ sawLiteral = true;
898
+ }
899
+ if (match[1].startsWith('OPEN:')) {
900
+ const encodedExpr = match[2] || '';
901
+ statements.push(`__pushFragment(${decodeURIComponent(encodedExpr)});`);
902
+ }
903
+ else {
904
+ statements.push(`__popFragment();`);
905
+ }
906
+ cursor = match.index + match[0].length;
907
+ }
908
+ const tail = line.slice(cursor);
909
+ if (tail || sawLiteral || options.appendNewline !== false) {
910
+ const appendNewline = options.appendNewline !== false;
911
+ if (tail || appendNewline) {
912
+ statements.push(compileHtmlSegment(tail, actionNames, rpcNameMap, { ...options, appendNewline }));
913
+ }
914
+ }
915
+ return statements.filter(Boolean);
916
+ }
645
917
  /**
646
- * Compile a single HTML line, replacing {expr} with escaped output,
918
+ * Compile a single HTML segment, replacing {expr} with escaped output,
647
919
  * {@html expr} with sanitized HTML, and {@raw expr} with raw output.
648
920
  * Handles attribute values like value={x}.
649
921
  */
650
- function compileHtmlLine(line, actionNames, rpcNameMap) {
922
+ function compileHtmlSegment(line, actionNames, rpcNameMap, options = {}) {
651
923
  // Expand shorthand syntax before main compilation
652
924
  line = expandShorthands(line);
653
925
  let result = '';
654
926
  let pos = 0;
655
- let hasExpr = false;
927
+ let pendingActionHiddenInput = null;
656
928
  while (pos < line.length) {
657
929
  const braceIdx = findNextTemplateBrace(line, pos);
658
930
  if (braceIdx === -1) {
@@ -671,19 +943,16 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
671
943
  if (inner.startsWith('@html ')) {
672
944
  const expr = inner.slice(6).trim();
673
945
  result += `\${__sanitizeHtml(${expr})}`;
674
- hasExpr = true;
675
946
  }
676
947
  else if (inner.startsWith('@raw ')) {
677
948
  // Unsafe raw HTML: {@raw expr}
678
949
  const expr = inner.slice(5).trim();
679
950
  result += `\${__rawHtml(${expr})}`;
680
- hasExpr = true;
681
951
  }
682
952
  else if (inner.startsWith('=html ')) {
683
953
  // Legacy alias for raw HTML: {=html expr}
684
954
  const expr = inner.slice(6).trim();
685
955
  result += `\${__rawHtml(${expr})}`;
686
- hasExpr = true;
687
956
  }
688
957
  else {
689
958
  // Check if this is an attribute value: attr={expr}
@@ -697,7 +966,6 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
697
966
  if (attrName === 'data-dialog-data') {
698
967
  // data-dialog-data={expr} → data-dialog-data="JSON.stringify(expr)" (HTML-escaped)
699
968
  result += `"\${__esc(JSON.stringify(${inner}))}"`;
700
- hasExpr = true;
701
969
  pos = closeIdx + 1;
702
970
  continue;
703
971
  }
@@ -719,7 +987,6 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
719
987
  else {
720
988
  result += ` data-refresh="\${__esc(${inner})}"`;
721
989
  }
722
- hasExpr = true;
723
990
  pos = closeIdx + 1;
724
991
  continue;
725
992
  }
@@ -737,7 +1004,6 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
737
1004
  else {
738
1005
  result += ` ${attrName}="${escapeLiteral(inner)}"`;
739
1006
  }
740
- hasExpr = true;
741
1007
  pos = closeIdx + 1;
742
1008
  continue;
743
1009
  }
@@ -759,12 +1025,11 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
759
1025
  // Non-call expression mode (e.g., data-get={someUrl})
760
1026
  result += ` ${attrName}="\${__esc(${inner})}"`;
761
1027
  }
762
- hasExpr = true;
763
1028
  pos = closeIdx + 1;
764
1029
  continue;
765
1030
  }
766
1031
  else if (attrName === 'data-poll') {
767
- // data-poll={fn(args)} → data-poll="fnName" data-poll-args="[serialized]" data-poll-id="stable-id"
1032
+ // data-poll={fn(args)} → data-poll="fnName" data-poll-args="[serialized]" data-poll-id="signed-id"
768
1033
  const pollCallMatch = inner.match(/^(\w+)\((.*)\)$/);
769
1034
  if (pollCallMatch) {
770
1035
  const fnName = pollCallMatch[1];
@@ -772,19 +1037,18 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
772
1037
  const argsExpr = pollCallMatch[2].trim();
773
1038
  // Remove the trailing "data-poll=" we already appended
774
1039
  result = result.replace(/\s*data-poll=$/, '');
775
- // Emit data-poll, data-poll-args, and stable data-poll-id (based on fn + args expression)
1040
+ // Emit data-poll, data-poll-args, and signed data-poll-id (signed at runtime for security)
776
1041
  result += ` data-poll="${rpcName}"`;
777
1042
  if (argsExpr) {
778
1043
  result += ` data-poll-args="\${__esc(JSON.stringify([${argsExpr}]))}"`;
779
- // Stable ID based on args so same data produces same ID across renders
780
- result += ` data-poll-id="\${__esc('__poll_' + String(${argsExpr}).replace(/[^a-zA-Z0-9]/g, '_'))}"`;
1044
+ // Sign the fragment ID at runtime with __signFragment(fragmentId, routePath)
1045
+ result += ` data-poll-id="\${__signFragment('__poll_' + String(${argsExpr}).replace(/[^a-zA-Z0-9]/g, '_'))}"`;
781
1046
  }
782
1047
  else {
783
- // No args - use function name as ID
784
- result += ` data-poll-id="__poll_${rpcName}"`;
1048
+ // No args - sign with function name as base ID
1049
+ result += ` data-poll-id="\${__signFragment('__poll_${rpcName}')}"`;
785
1050
  }
786
1051
  }
787
- hasExpr = true;
788
1052
  pos = closeIdx + 1;
789
1053
  continue;
790
1054
  }
@@ -798,25 +1062,17 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
798
1062
  if (!isServerAction) {
799
1063
  throw new Error(`Invalid action expression: "${inner}". Use action={myActionFn} for server actions.`);
800
1064
  }
801
- // Remove trailing `action=` from output and inject _action hidden field.
1065
+ // Remove trailing `action=` from output and inject _action hidden field + CSRF token.
802
1066
  result = result.replace(/\s*action=$/, '');
1067
+ const actionValue = actionNames === undefined
1068
+ ? `\${__esc(${inner})}`
1069
+ : inner;
1070
+ // Inject both _action and _csrf hidden fields for server action forms
1071
+ pendingActionHiddenInput = `\\n<input type="hidden" name="_action" value="${actionValue}">\\n<input type="hidden" name="_csrf" value="\${__getCsrfToken()}">`;
803
1072
  pos = closeIdx + 1;
804
- const tagEnd = line.indexOf('>', pos);
805
- if (tagEnd !== -1) {
806
- result += escapeLiteral(line.slice(pos, tagEnd + 1));
807
- // In a route context, inner is the literal action function name (a string key).
808
- // In a component context (actionNames === undefined), inner is a prop name whose
809
- // value at runtime is the action name string — emit it as a dynamic expression.
810
- const actionValue = actionNames === undefined
811
- ? `\${__esc(${inner})}`
812
- : inner;
813
- result += `\\n<input type="hidden" name="_action" value="${actionValue}">`;
814
- pos = tagEnd + 1;
815
- }
816
- hasExpr = true;
817
1073
  continue;
818
1074
  }
819
- else if (/^on[A-Za-z]+$/.test(attrName)) {
1075
+ else if (/^on[A-Za-z]+$/i.test(attrName)) {
820
1076
  // onClick/onChange/...={expr} — check if it's a server action or plain client JS
821
1077
  const eventName = attrName.slice(2).toLowerCase();
822
1078
  const actionCallMatch = inner.match(/^(\w+)\((.*)\)$/);
@@ -831,12 +1087,25 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
831
1087
  result += ` data-action="${fnName}" data-args="\${__esc(JSON.stringify([${argsExpr}]))}" data-action-event="${eventName}"`;
832
1088
  }
833
1089
  else {
834
- // Plain client-side event handler: onClick={myFn()}
835
- // Emit as native inline handler (lowercased event attribute).
836
- result = result.replace(new RegExp(`\\s*${attrName}=$`), ` on${eventName}=`);
837
- result += `"${escapeLiteral(inner)}"`;
1090
+ const clientRegistration = options.clientRouteRegistry?.registerEventHandler(eventName, inner) ?? null;
1091
+ if (clientRegistration) {
1092
+ result = result.replace(new RegExp(`\\s*${attrName}=$`), '');
1093
+ result += ` data-client-route="${clientRegistration.routeId}" data-client-handler="${clientRegistration.handlerId}" data-client-event="${eventName}"`;
1094
+ if (clientRegistration.argsExpr) {
1095
+ result += ` data-client-args="\${__esc(JSON.stringify([${clientRegistration.argsExpr}]))}"`;
1096
+ }
1097
+ }
1098
+ else if (options.clientRouteRegistry?.hasBindingReference(inner)) {
1099
+ throw new Error(`Unsupported client handler expression: "${inner}". ` +
1100
+ `Top-level $client/$shared event handlers must be a direct function reference or call, like onClick={openDialog()} or onClick={helpers.openDialog()}.`);
1101
+ }
1102
+ else {
1103
+ // Plain client-side event handler: onClick={myFn()}
1104
+ // Emit as native inline handler (lowercased event attribute).
1105
+ result = result.replace(new RegExp(`\\s*${attrName}=$`), ` on${eventName}=`);
1106
+ result += `"${escapeLiteral(inner)}"`;
1107
+ }
838
1108
  }
839
- hasExpr = true;
840
1109
  pos = closeIdx + 1;
841
1110
  continue;
842
1111
  }
@@ -852,17 +1121,14 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
852
1121
  else {
853
1122
  result += `\${__esc(${inner})}`;
854
1123
  }
855
- hasExpr = true;
856
1124
  }
857
1125
  pos = closeIdx + 1;
858
1126
  }
859
- // If the line had expressions, use template literal; otherwise plain string
860
- if (hasExpr) {
861
- return `__html += \`${result}\\n\`;`;
862
- }
863
- else {
864
- return `__html += \`${result}\\n\`;`;
1127
+ if (pendingActionHiddenInput && result.includes('>')) {
1128
+ const gtIndex = result.indexOf('>');
1129
+ result = result.slice(0, gtIndex + 1) + pendingActionHiddenInput + result.slice(gtIndex + 1);
865
1130
  }
1131
+ return buildAppendStatement(`\`${result}${options.appendNewline === false ? '' : '\\n'}\``, options.emitCall);
866
1132
  }
867
1133
  /** Escape characters that would break a JS template literal. */
868
1134
  function escapeLiteral(text) {
@@ -942,4 +1208,4 @@ export function generateRenderFunction(template) {
942
1208
  ${body}
943
1209
  }`;
944
1210
  }
945
- import { transpileTypeScript } from './transpile.js';
1211
+ // TypeScript transpilation removed wrangler's esbuild handles it
@@ -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;