@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
@@ -1,4 +1,5 @@
1
1
  import ts from 'typescript';
2
+ import { collectReferencedIdentifiers, parseNamedImportBindings } from './import-linking.js';
2
3
  function getTopLevelImportStatements(source) {
3
4
  const sourceFile = ts.createSourceFile('kuratchi-script.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
4
5
  const imports = [];
@@ -29,6 +30,23 @@ export function stripTopLevelImports(source) {
29
30
  function hasReactiveLabel(scriptBody) {
30
31
  return /\$\s*:/.test(scriptBody);
31
32
  }
33
+ const TEMPLATE_JS_CONTROL_PATTERNS = [
34
+ /^\s*for\s*\(/,
35
+ /^\s*if\s*\(/,
36
+ /^\s*switch\s*\(/,
37
+ /^\s*case\s+.+:\s*$/,
38
+ /^\s*default\s*:\s*$/,
39
+ /^\s*break\s*;\s*$/,
40
+ /^\s*continue\s*;\s*$/,
41
+ /^\s*\}\s*else\s*if\s*\(/,
42
+ /^\s*\}\s*else\s*\{?\s*$/,
43
+ /^\s*\}\s*$/,
44
+ /^\s*\w[\w.]*\s*(\+\+|--)\s*;\s*$/,
45
+ /^\s*(let|const|var)\s+/,
46
+ ];
47
+ function isTemplateJsControlLine(line) {
48
+ return TEMPLATE_JS_CONTROL_PATTERNS.some((pattern) => pattern.test(line));
49
+ }
32
50
  function splitTopLevel(input, delimiter) {
33
51
  const parts = [];
34
52
  let start = 0;
@@ -271,6 +289,286 @@ function extractReturnObjectKeys(body) {
271
289
  }
272
290
  return keys;
273
291
  }
292
+ function findTemplateTagEnd(source, start) {
293
+ let quote = null;
294
+ let escaped = false;
295
+ let braceDepth = 0;
296
+ for (let i = start; i < source.length; i++) {
297
+ const ch = source[i];
298
+ if (quote) {
299
+ if (escaped) {
300
+ escaped = false;
301
+ continue;
302
+ }
303
+ if (ch === '\\') {
304
+ escaped = true;
305
+ continue;
306
+ }
307
+ if (ch === quote)
308
+ quote = null;
309
+ continue;
310
+ }
311
+ if (ch === '"' || ch === "'" || ch === '`') {
312
+ quote = ch;
313
+ continue;
314
+ }
315
+ if (ch === '{') {
316
+ braceDepth++;
317
+ continue;
318
+ }
319
+ if (ch === '}') {
320
+ braceDepth = Math.max(0, braceDepth - 1);
321
+ continue;
322
+ }
323
+ if (ch === '>' && braceDepth === 0)
324
+ return i;
325
+ }
326
+ return -1;
327
+ }
328
+ function parseTemplateTagAttributes(source) {
329
+ const attrs = new Map();
330
+ let i = 0;
331
+ while (i < source.length) {
332
+ while (i < source.length && /\s/.test(source[i]))
333
+ i++;
334
+ if (i >= source.length)
335
+ break;
336
+ const nameStart = i;
337
+ while (i < source.length && /[^\s=/>]/.test(source[i]))
338
+ i++;
339
+ const name = source.slice(nameStart, i);
340
+ if (!name)
341
+ break;
342
+ while (i < source.length && /\s/.test(source[i]))
343
+ i++;
344
+ if (source[i] !== '=') {
345
+ attrs.set(name, '');
346
+ continue;
347
+ }
348
+ i++;
349
+ while (i < source.length && /\s/.test(source[i]))
350
+ i++;
351
+ if (i >= source.length) {
352
+ attrs.set(name, '');
353
+ break;
354
+ }
355
+ if (source[i] === '"' || source[i] === "'") {
356
+ const quote = source[i];
357
+ const valueStart = i;
358
+ i++;
359
+ while (i < source.length) {
360
+ if (source[i] === '\\') {
361
+ i += 2;
362
+ continue;
363
+ }
364
+ if (source[i] === quote) {
365
+ i++;
366
+ break;
367
+ }
368
+ i++;
369
+ }
370
+ attrs.set(name, source.slice(valueStart, i));
371
+ continue;
372
+ }
373
+ if (source[i] === '{') {
374
+ const valueStart = i;
375
+ const closeIdx = findMatchingToken(source, i, '{', '}');
376
+ if (closeIdx === -1) {
377
+ attrs.set(name, source.slice(valueStart));
378
+ break;
379
+ }
380
+ i = closeIdx + 1;
381
+ attrs.set(name, source.slice(valueStart, i));
382
+ continue;
383
+ }
384
+ const valueStart = i;
385
+ while (i < source.length && /[^\s>]/.test(source[i]))
386
+ i++;
387
+ attrs.set(name, source.slice(valueStart, i));
388
+ }
389
+ return attrs;
390
+ }
391
+ function tokenizeTemplateTags(template) {
392
+ const tags = [];
393
+ let cursor = 0;
394
+ while (cursor < template.length) {
395
+ const lt = template.indexOf('<', cursor);
396
+ if (lt === -1)
397
+ break;
398
+ if (template.startsWith('<!--', lt)) {
399
+ const commentEnd = template.indexOf('-->', lt + 4);
400
+ if (commentEnd === -1)
401
+ break;
402
+ cursor = commentEnd + 3;
403
+ continue;
404
+ }
405
+ const tagEnd = findTemplateTagEnd(template, lt + 1);
406
+ if (tagEnd === -1)
407
+ break;
408
+ const raw = template.slice(lt + 1, tagEnd).trim();
409
+ cursor = tagEnd + 1;
410
+ if (!raw || raw.startsWith('!'))
411
+ continue;
412
+ const closing = raw.startsWith('/');
413
+ const normalized = closing ? raw.slice(1).trim() : raw;
414
+ const nameMatch = normalized.match(/^([A-Za-z][\w:-]*)/);
415
+ if (!nameMatch)
416
+ continue;
417
+ const name = nameMatch[1];
418
+ const attrSource = normalized.slice(name.length).replace(/\/\s*$/, '').trim();
419
+ tags.push({
420
+ name,
421
+ attrs: closing ? new Map() : parseTemplateTagAttributes(attrSource),
422
+ closing,
423
+ });
424
+ }
425
+ return tags;
426
+ }
427
+ function extractBracedAttributeExpression(value) {
428
+ if (!value)
429
+ return null;
430
+ const trimmed = value.trim();
431
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}'))
432
+ return null;
433
+ return trimmed.slice(1, -1).trim();
434
+ }
435
+ function extractAttributeText(value) {
436
+ if (!value)
437
+ return null;
438
+ const trimmed = value.trim();
439
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
440
+ return trimmed.slice(1, -1);
441
+ }
442
+ const braced = extractBracedAttributeExpression(trimmed);
443
+ if (braced && /^[A-Za-z_$][\w$]*$/.test(braced))
444
+ return braced;
445
+ return trimmed || null;
446
+ }
447
+ function extractCallExpression(value) {
448
+ const expr = extractBracedAttributeExpression(value);
449
+ if (!expr)
450
+ return null;
451
+ const match = expr.match(/^([A-Za-z_$][\w$]*)\(([\s\S]*)\)$/);
452
+ if (!match)
453
+ return null;
454
+ return {
455
+ fnName: match[1],
456
+ argsExpr: (match[2] || '').trim(),
457
+ };
458
+ }
459
+ function extractTopLevelImportNames(source) {
460
+ const sourceFile = ts.createSourceFile('kuratchi-inline-client.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
461
+ const names = [];
462
+ for (const statement of sourceFile.statements) {
463
+ if (!ts.isImportDeclaration(statement))
464
+ continue;
465
+ const clause = statement.importClause;
466
+ if (!clause)
467
+ continue;
468
+ if (clause.name)
469
+ pushIdentifier(clause.name.text, names);
470
+ if (!clause.namedBindings)
471
+ continue;
472
+ if (ts.isNamedImports(clause.namedBindings)) {
473
+ for (const element of clause.namedBindings.elements) {
474
+ pushIdentifier(element.name.text, names);
475
+ }
476
+ }
477
+ else if (ts.isNamespaceImport(clause.namedBindings)) {
478
+ pushIdentifier(clause.namedBindings.name.text, names);
479
+ }
480
+ }
481
+ return names;
482
+ }
483
+ function extractImportModuleSpecifier(source) {
484
+ const sourceFile = ts.createSourceFile('kuratchi-import-spec.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
485
+ const statement = sourceFile.statements.find(ts.isImportDeclaration);
486
+ if (!statement || !ts.isStringLiteral(statement.moduleSpecifier))
487
+ return null;
488
+ return statement.moduleSpecifier.text;
489
+ }
490
+ function isExecutableTemplateScript(attrs) {
491
+ if (/\bsrc\s*=/i.test(attrs))
492
+ return false;
493
+ const typeMatch = attrs.match(/\btype\s*=\s*(['"])(.*?)\1/i);
494
+ const type = typeMatch?.[2]?.trim().toLowerCase();
495
+ if (!type)
496
+ return true;
497
+ return type === 'module' || type === 'text/javascript' || type === 'application/javascript';
498
+ }
499
+ function collectTemplateClientDeclaredNames(template) {
500
+ const declared = new Set();
501
+ const scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
502
+ let match;
503
+ while ((match = scriptRegex.exec(template)) !== null) {
504
+ const attrs = match[1] || '';
505
+ const body = match[2] || '';
506
+ if (!isExecutableTemplateScript(attrs))
507
+ continue;
508
+ const transpiled = transpileTypeScript(body, 'template-client-script.ts');
509
+ for (const name of extractTopLevelImportNames(transpiled))
510
+ declared.add(name);
511
+ for (const name of extractTopLevelDataVars(transpiled))
512
+ declared.add(name);
513
+ for (const name of extractTopLevelFunctionNames(transpiled))
514
+ declared.add(name);
515
+ }
516
+ return Array.from(declared);
517
+ }
518
+ function stripLeadingTopLevelScriptBlock(template) {
519
+ return template.replace(/^(\s*)<script(\s[^>]*)?\s*>[\s\S]*?<\/script>\s*/i, '');
520
+ }
521
+ function stripTemplateRawBlocks(template) {
522
+ return template
523
+ .replace(/<script\b[\s\S]*?<\/script>/gi, '')
524
+ .replace(/<style\b[\s\S]*?<\/style>/gi, '');
525
+ }
526
+ function extractBraceExpressions(line) {
527
+ const expressions = [];
528
+ let cursor = 0;
529
+ while (cursor < line.length) {
530
+ const openIdx = line.indexOf('{', cursor);
531
+ if (openIdx === -1)
532
+ break;
533
+ const closeIdx = findMatchingToken(line, openIdx, '{', '}');
534
+ if (closeIdx === -1)
535
+ break;
536
+ const expression = line.slice(openIdx + 1, closeIdx).trim();
537
+ const beforeBrace = line.slice(0, openIdx);
538
+ const charBefore = openIdx > 0 ? line[openIdx - 1] : '';
539
+ let attrName = null;
540
+ if (charBefore === '=') {
541
+ const attrMatch = beforeBrace.match(/([\w-]+)=$/);
542
+ attrName = attrMatch ? attrMatch[1] : null;
543
+ }
544
+ expressions.push({ expression, attrName });
545
+ cursor = closeIdx + 1;
546
+ }
547
+ return expressions;
548
+ }
549
+ function collectServerTemplateReferences(template) {
550
+ const refs = new Set();
551
+ const stripped = stripTemplateRawBlocks(template);
552
+ const lines = stripped.split('\n');
553
+ for (const line of lines) {
554
+ const trimmed = line.trim();
555
+ if (!trimmed)
556
+ continue;
557
+ if (isTemplateJsControlLine(trimmed)) {
558
+ for (const ref of collectReferencedIdentifiers(trimmed))
559
+ refs.add(ref);
560
+ }
561
+ for (const entry of extractBraceExpressions(line)) {
562
+ if (!entry.expression)
563
+ continue;
564
+ if (entry.attrName && /^on[A-Za-z]+$/i.test(entry.attrName))
565
+ continue;
566
+ for (const ref of collectReferencedIdentifiers(entry.expression))
567
+ refs.add(ref);
568
+ }
569
+ }
570
+ return refs;
571
+ }
274
572
  function extractExplicitLoad(scriptBody) {
275
573
  let i = 0;
276
574
  let depthParen = 0;
@@ -685,12 +983,15 @@ export function parseFile(source, options = {}) {
685
983
  // Extract all imports from script
686
984
  const serverImports = [];
687
985
  const clientImports = [];
986
+ const routeClientImports = [];
987
+ const routeClientImportBindings = [];
688
988
  const componentImports = {};
689
989
  const workerEnvAliases = [];
690
990
  const devAliases = [];
691
991
  if (script) {
692
992
  for (const statement of getTopLevelImportStatements(script)) {
693
993
  const line = statement.text.trim();
994
+ const moduleSpecifier = extractImportModuleSpecifier(line);
694
995
  // Check for component imports: import Name from '$lib/file.html' or '@kuratchi/ui/file.html'
695
996
  const libMatch = line.match(/import\s+([A-Za-z_$][\w$]*)\s+from\s+['"]\$lib\/([^'"]+\.html)['"]/s);
696
997
  const pkgMatch = !libMatch ? line.match(/import\s+([A-Za-z_$][\w$]*)\s+from\s+['"](@[^/'"]+\/[^/'"]+)\/([^'"]+\.html)['"]/s) : null;
@@ -714,6 +1015,14 @@ export function parseFile(source, options = {}) {
714
1015
  }
715
1016
  continue;
716
1017
  }
1018
+ if (moduleSpecifier?.startsWith('$client/')) {
1019
+ routeClientImports.push(line);
1020
+ for (const binding of extractTopLevelImportNames(line)) {
1021
+ if (!routeClientImportBindings.includes(binding))
1022
+ routeClientImportBindings.push(binding);
1023
+ }
1024
+ continue;
1025
+ }
717
1026
  serverImports.push(line);
718
1027
  }
719
1028
  for (const alias of extractCloudflareEnvAliases(line)) {
@@ -758,6 +1067,18 @@ export function parseFile(source, options = {}) {
758
1067
  throw buildEnvAccessError(kind, options.filePath, `Imported env from cloudflare:workers in a ${kind} script.`);
759
1068
  }
760
1069
  const transpiledScriptBody = transpileTypeScript(scriptBody, 'route-script.ts');
1070
+ const serverScriptRefs = new Set(collectReferencedIdentifiers(transpiledScriptBody));
1071
+ if (loadFunction) {
1072
+ const transpiledLoadFunction = transpileTypeScript(loadFunction, 'route-load.ts');
1073
+ for (const ref of collectReferencedIdentifiers(transpiledLoadFunction))
1074
+ serverScriptRefs.add(ref);
1075
+ }
1076
+ const leakedClientScriptBindings = routeClientImportBindings.filter((name) => serverScriptRefs.has(name));
1077
+ if (leakedClientScriptBindings.length > 0) {
1078
+ throw new Error(`[kuratchi compiler] ${options.filePath || kind}\n` +
1079
+ `Top-level $client imports cannot be used in server route code: ${leakedClientScriptBindings.join(', ')}.\n` +
1080
+ `Move this usage to a client event handler or client bridge code, or import from $shared instead.`);
1081
+ }
761
1082
  const topLevelVars = extractTopLevelDataVars(transpiledScriptBody);
762
1083
  for (const v of topLevelVars)
763
1084
  dataVars.push(v);
@@ -772,15 +1093,8 @@ export function parseFile(source, options = {}) {
772
1093
  }
773
1094
  // Server import named bindings are also data vars (available in templates)
774
1095
  for (const line of serverImports) {
775
- const namesMatch = line.match(/import\s*\{([^}]+)\}/);
776
- if (!namesMatch)
777
- continue;
778
- for (const part of namesMatch[1].split(',')) {
779
- const trimmed = part.trim();
780
- if (!trimmed)
781
- continue;
782
- const segments = trimmed.split(/\s+as\s+/);
783
- const localName = (segments[1] || segments[0]).trim();
1096
+ for (const binding of parseNamedImportBindings(line)) {
1097
+ const localName = binding.local.trim();
784
1098
  if (localName && !dataVars.includes(localName))
785
1099
  dataVars.push(localName);
786
1100
  }
@@ -791,74 +1105,140 @@ export function parseFile(source, options = {}) {
791
1105
  // This prevents commented-out code (<!-- ... -->) from being parsed as live
792
1106
  // action expressions, which would cause false "Invalid action expression" errors.
793
1107
  const templateWithoutComments = template.replace(/<!--[\s\S]*?-->/g, '');
794
- // Extract action functions referenced in template: action={fnName}
1108
+ const templateTags = tokenizeTemplateTags(templateWithoutComments);
795
1109
  const actionFunctions = [];
1110
+ const pollFunctions = [];
1111
+ const dataGetQueries = [];
1112
+ for (const tag of templateTags) {
1113
+ if (tag.closing)
1114
+ continue;
1115
+ const actionExpr = extractBracedAttributeExpression(tag.attrs.get('action'));
1116
+ if (actionExpr && /^[A-Za-z_$][\w$]*$/.test(actionExpr) && !actionFunctions.includes(actionExpr)) {
1117
+ actionFunctions.push(actionExpr);
1118
+ }
1119
+ for (const [attrName, attrValue] of tag.attrs.entries()) {
1120
+ if (/^on[A-Za-z]+$/i.test(attrName)) {
1121
+ const actionCall = extractCallExpression(attrValue);
1122
+ if (actionCall && !actionFunctions.includes(actionCall.fnName))
1123
+ actionFunctions.push(actionCall.fnName);
1124
+ }
1125
+ if (/^data-(post|put|patch|delete)$/.test(attrName)) {
1126
+ const methodCall = extractCallExpression(attrValue);
1127
+ if (methodCall && !actionFunctions.includes(methodCall.fnName))
1128
+ actionFunctions.push(methodCall.fnName);
1129
+ }
1130
+ }
1131
+ const pollCall = extractCallExpression(tag.attrs.get('data-poll'));
1132
+ if (pollCall && !pollFunctions.includes(pollCall.fnName)) {
1133
+ pollFunctions.push(pollCall.fnName);
1134
+ }
1135
+ const getCall = extractCallExpression(tag.attrs.get('data-get'));
1136
+ const asName = extractAttributeText(tag.attrs.get('data-as'));
1137
+ if (!getCall || !asName || !/^[A-Za-z_$][\w$]*$/.test(asName))
1138
+ continue;
1139
+ const key = (extractAttributeText(tag.attrs.get('data-key')) || asName).trim();
1140
+ if (!pollFunctions.includes(getCall.fnName))
1141
+ pollFunctions.push(getCall.fnName);
1142
+ if (!dataVars.includes(asName))
1143
+ dataVars.push(asName);
1144
+ const exists = dataGetQueries.some((q) => q.asName === asName);
1145
+ if (!exists)
1146
+ dataGetQueries.push({ fnName: getCall.fnName, argsExpr: getCall.argsExpr, asName, key });
1147
+ }
1148
+ for (const clientBinding of routeClientImportBindings) {
1149
+ const idx = actionFunctions.indexOf(clientBinding);
1150
+ if (idx !== -1)
1151
+ actionFunctions.splice(idx, 1);
1152
+ }
1153
+ const templateTemplateScriptSource = clientScript ? stripLeadingTopLevelScriptBlock(template) : template;
1154
+ const templateClientDeclaredNames = collectTemplateClientDeclaredNames(templateTemplateScriptSource);
1155
+ const serverTemplateRefs = collectServerTemplateReferences(template);
1156
+ const leakedRouteClientTemplateBindings = routeClientImportBindings.filter((name) => serverTemplateRefs.has(name));
1157
+ if (leakedRouteClientTemplateBindings.length > 0) {
1158
+ throw new Error(`[kuratchi compiler] ${options.filePath || kind}\n` +
1159
+ `Top-level $client imports cannot be used in server-rendered template output: ${leakedRouteClientTemplateBindings.join(', ')}.\n` +
1160
+ `Use $shared for portable helpers or move this usage into a client event handler.`);
1161
+ }
1162
+ if (templateClientDeclaredNames.length > 0) {
1163
+ const leakedNames = templateClientDeclaredNames.filter((name) => serverTemplateRefs.has(name));
1164
+ if (leakedNames.length > 0) {
1165
+ throw new Error(`[kuratchi compiler] ${options.filePath || kind}\n` +
1166
+ `Client template <script> bindings cannot be used in server-rendered template output: ${leakedNames.join(', ')}.\n` +
1167
+ `Move shared/pure helpers into the top route <script> or a $shared module.`);
1168
+ }
1169
+ }
1170
+ /*
1171
+ // Extract action functions referenced in template: action={fnName}
1172
+ const actionFunctions: string[] = [];
796
1173
  const actionRegex = /action=\{(\w+)\}/g;
797
1174
  let am;
798
1175
  while ((am = actionRegex.exec(templateWithoutComments)) !== null) {
799
- if (!actionFunctions.includes(am[1])) {
800
- actionFunctions.push(am[1]);
801
- }
1176
+ if (!actionFunctions.includes(am[1])) {
1177
+ actionFunctions.push(am[1]);
1178
+ }
802
1179
  }
803
1180
  // Also collect onX={fnName(...)} candidates (e.g. onclick, onClick, onChange)
804
1181
  // — the compiler will filter these against actual imports to determine server actions.
805
1182
  const eventActionRegex = /on[A-Za-z]+\s*=\{(\w+)\s*\(/g;
806
1183
  let em;
807
1184
  while ((em = eventActionRegex.exec(templateWithoutComments)) !== null) {
808
- if (!actionFunctions.includes(em[1])) {
809
- actionFunctions.push(em[1]);
810
- }
1185
+ if (!actionFunctions.includes(em[1])) {
1186
+ actionFunctions.push(em[1]);
1187
+ }
811
1188
  }
812
1189
  // Collect method-style action attributes: data-post/data-put/data-patch/data-delete
813
1190
  const methodActionRegex = /data-(?:post|put|patch|delete)\s*=\{(\w+)\s*\(/g;
814
1191
  let mm;
815
1192
  while ((mm = methodActionRegex.exec(templateWithoutComments)) !== null) {
816
- if (!actionFunctions.includes(mm[1])) {
817
- actionFunctions.push(mm[1]);
818
- }
1193
+ if (!actionFunctions.includes(mm[1])) {
1194
+ actionFunctions.push(mm[1]);
1195
+ }
819
1196
  }
1197
+
820
1198
  // Extract poll functions referenced in template: data-poll={fnName(args)}
821
- const pollFunctions = [];
1199
+ const pollFunctions: string[] = [];
822
1200
  const pollRegex = /data-poll=\{(\w+)\s*\(/g;
823
1201
  let pm;
824
1202
  while ((pm = pollRegex.exec(templateWithoutComments)) !== null) {
825
- if (!pollFunctions.includes(pm[1])) {
826
- pollFunctions.push(pm[1]);
827
- }
1203
+ if (!pollFunctions.includes(pm[1])) {
1204
+ pollFunctions.push(pm[1]);
1205
+ }
828
1206
  }
1207
+
829
1208
  // Extract query blocks: tags that include both data-get={fn(args)} and data-as=...
830
- const dataGetQueries = [];
1209
+ const dataGetQueries: Array<{ fnName: string; argsExpr: string; asName: string; key?: string }> = [];
831
1210
  const tagRegex = /<[^>]+>/g;
832
1211
  let tm;
833
1212
  while ((tm = tagRegex.exec(templateWithoutComments)) !== null) {
834
- const tag = tm[0];
835
- const getMatch = tag.match(/\bdata-get=\{([\s\S]*?)\}/);
836
- if (!getMatch)
837
- continue;
838
- const call = getMatch[1].trim();
839
- const callMatch = call.match(/^([A-Za-z_$][\w$]*)\(([\s\S]*)\)$/);
840
- if (!callMatch)
841
- continue;
842
- const fnName = callMatch[1];
843
- const argsExpr = (callMatch[2] || '').trim();
844
- const asMatch = tag.match(/\bdata-as="([A-Za-z_$][\w$]*)"/) ||
845
- tag.match(/\bdata-as='([A-Za-z_$][\w$]*)'/) ||
846
- tag.match(/\bdata-as=\{([A-Za-z_$][\w$]*)\}/);
847
- if (!asMatch)
848
- continue;
849
- const asName = asMatch[1];
850
- const keyMatch = tag.match(/\bdata-key="([^"]+)"/) ||
851
- tag.match(/\bdata-key='([^']+)'/) ||
852
- tag.match(/\bdata-key=\{([^}]+)\}/);
853
- const key = keyMatch?.[1]?.trim() || asName;
854
- if (!pollFunctions.includes(fnName))
855
- pollFunctions.push(fnName);
856
- if (!dataVars.includes(asName))
857
- dataVars.push(asName);
858
- const exists = dataGetQueries.some((q) => q.asName === asName);
859
- if (!exists)
860
- dataGetQueries.push({ fnName, argsExpr, asName, key });
1213
+ const tag = tm[0];
1214
+ const getMatch = tag.match(/\bdata-get=\{([\s\S]*?)\}/);
1215
+ if (!getMatch) continue;
1216
+ const call = getMatch[1].trim();
1217
+ const callMatch = call.match(/^([A-Za-z_$][\w$]*)\(([\s\S]*)\)$/);
1218
+ if (!callMatch) continue;
1219
+ const fnName = callMatch[1];
1220
+ const argsExpr = (callMatch[2] || '').trim();
1221
+
1222
+ const asMatch =
1223
+ tag.match(/\bdata-as="([A-Za-z_$][\w$]*)"/) ||
1224
+ tag.match(/\bdata-as='([A-Za-z_$][\w$]*)'/) ||
1225
+ tag.match(/\bdata-as=\{([A-Za-z_$][\w$]*)\}/);
1226
+ if (!asMatch) continue;
1227
+ const asName = asMatch[1];
1228
+
1229
+ const keyMatch =
1230
+ tag.match(/\bdata-key="([^"]+)"/) ||
1231
+ tag.match(/\bdata-key='([^']+)'/) ||
1232
+ tag.match(/\bdata-key=\{([^}]+)\}/);
1233
+ const key = keyMatch?.[1]?.trim() || asName;
1234
+
1235
+ if (!pollFunctions.includes(fnName)) pollFunctions.push(fnName);
1236
+ if (!dataVars.includes(asName)) dataVars.push(asName);
1237
+
1238
+ const exists = dataGetQueries.some((q) => q.asName === asName);
1239
+ if (!exists) dataGetQueries.push({ fnName, argsExpr, asName, key });
861
1240
  }
1241
+ */
862
1242
  return {
863
1243
  script,
864
1244
  loadFunction,
@@ -871,6 +1251,8 @@ export function parseFile(source, options = {}) {
871
1251
  pollFunctions,
872
1252
  dataGetQueries,
873
1253
  clientImports,
1254
+ routeClientImports,
1255
+ routeClientImportBindings,
874
1256
  loadReturnVars,
875
1257
  workerEnvAliases,
876
1258
  devAliases,
@@ -0,0 +1,10 @@
1
+ export interface UiConfigValues {
2
+ theme: string;
3
+ radius: string;
4
+ }
5
+ export declare function prepareRootLayoutSource(opts: {
6
+ source: string;
7
+ isDev: boolean;
8
+ themeCss: string | null;
9
+ uiConfigValues: UiConfigValues | null;
10
+ }): string;