@kuratchi/js 0.0.15 → 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.
- package/README.md +10 -8
- package/dist/cli.js +80 -47
- package/dist/compiler/api-route-pipeline.d.ts +8 -0
- package/dist/compiler/api-route-pipeline.js +23 -0
- package/dist/compiler/asset-pipeline.d.ts +7 -0
- package/dist/compiler/asset-pipeline.js +33 -0
- package/dist/compiler/client-module-pipeline.d.ts +25 -0
- package/dist/compiler/client-module-pipeline.js +257 -0
- package/dist/compiler/compiler-shared.d.ts +55 -0
- package/dist/compiler/compiler-shared.js +4 -0
- package/dist/compiler/component-pipeline.d.ts +15 -0
- package/dist/compiler/component-pipeline.js +163 -0
- package/dist/compiler/config-reading.d.ts +11 -0
- package/dist/compiler/config-reading.js +323 -0
- package/dist/compiler/convention-discovery.d.ts +9 -0
- package/dist/compiler/convention-discovery.js +83 -0
- package/dist/compiler/durable-object-pipeline.d.ts +9 -0
- package/dist/compiler/durable-object-pipeline.js +255 -0
- package/dist/compiler/error-page-pipeline.d.ts +1 -0
- package/dist/compiler/error-page-pipeline.js +16 -0
- package/dist/compiler/import-linking.d.ts +36 -0
- package/dist/compiler/import-linking.js +139 -0
- package/dist/compiler/index.d.ts +3 -3
- package/dist/compiler/index.js +133 -3305
- package/dist/compiler/layout-pipeline.d.ts +31 -0
- package/dist/compiler/layout-pipeline.js +155 -0
- package/dist/compiler/page-route-pipeline.d.ts +16 -0
- package/dist/compiler/page-route-pipeline.js +62 -0
- package/dist/compiler/parser.d.ts +4 -0
- package/dist/compiler/parser.js +433 -51
- package/dist/compiler/root-layout-pipeline.d.ts +10 -0
- package/dist/compiler/root-layout-pipeline.js +517 -0
- package/dist/compiler/route-discovery.d.ts +7 -0
- package/dist/compiler/route-discovery.js +87 -0
- package/dist/compiler/route-pipeline.d.ts +57 -0
- package/dist/compiler/route-pipeline.js +296 -0
- package/dist/compiler/route-state-pipeline.d.ts +25 -0
- package/dist/compiler/route-state-pipeline.js +139 -0
- package/dist/compiler/routes-module-feature-blocks.d.ts +2 -0
- package/dist/compiler/routes-module-feature-blocks.js +330 -0
- package/dist/compiler/routes-module-pipeline.d.ts +2 -0
- package/dist/compiler/routes-module-pipeline.js +6 -0
- package/dist/compiler/routes-module-runtime-shell.d.ts +2 -0
- package/dist/compiler/routes-module-runtime-shell.js +81 -0
- package/dist/compiler/routes-module-types.d.ts +44 -0
- package/dist/compiler/routes-module-types.js +1 -0
- package/dist/compiler/script-transform.d.ts +16 -0
- package/dist/compiler/script-transform.js +218 -0
- package/dist/compiler/server-module-pipeline.d.ts +13 -0
- package/dist/compiler/server-module-pipeline.js +124 -0
- package/dist/compiler/template.d.ts +13 -1
- package/dist/compiler/template.js +315 -58
- package/dist/compiler/worker-output-pipeline.d.ts +13 -0
- package/dist/compiler/worker-output-pipeline.js +37 -0
- package/dist/compiler/wrangler-sync.d.ts +14 -0
- package/dist/compiler/wrangler-sync.js +185 -0
- package/dist/runtime/app.js +15 -3
- package/dist/runtime/generated-worker.d.ts +33 -0
- package/dist/runtime/generated-worker.js +412 -0
- package/dist/runtime/index.d.ts +2 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/router.d.ts +2 -1
- package/dist/runtime/router.js +12 -3
- package/dist/runtime/types.d.ts +8 -2
- package/package.json +5 -1
package/dist/compiler/parser.js
CHANGED
|
@@ -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
|
|
776
|
-
|
|
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
|
-
|
|
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
|
-
|
|
800
|
-
|
|
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
|
-
|
|
809
|
-
|
|
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
|
-
|
|
817
|
-
|
|
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
|
-
|
|
826
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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,
|