@kuratchi/js 0.0.4 → 0.0.6

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 CHANGED
@@ -50,8 +50,6 @@ src/routes/layout.html → shared layout wrapping all routes
50
50
 
51
51
  ```html
52
52
  <script>
53
- // Imports and server-side logic run on every request.
54
- // Exported functions become actions or RPC handlers automatically.
55
53
  import { getItems, addItem, deleteItem } from '$database/items';
56
54
 
57
55
  const items = await getItems();
@@ -472,7 +470,26 @@ import {
472
470
  } from '@kuratchi/js';
473
471
  ```
474
472
 
475
- Environment bindings are accessed directly via the Cloudflare Workers API — no framework wrapper needed:
473
+ ## Environment bindings
474
+
475
+ Cloudflare env is server-only.
476
+
477
+ - Route top-level `<script>`, route `load()` functions, server actions, API handlers, and other server modules can read env.
478
+ - Templates, components, and client `<script>` blocks cannot read env directly.
479
+ - If a value must reach the browser, compute it in the server route script and reference it in the template, or return it from `load()` explicitly.
480
+
481
+ ```html
482
+ <script>
483
+ import { env } from 'cloudflare:workers';
484
+ const turnstileSiteKey = env.TURNSTILE_SITE_KEY || '';
485
+ </script>
486
+
487
+ if (turnstileSiteKey) {
488
+ <div class="cf-turnstile" data-sitekey={turnstileSiteKey}></div>
489
+ }
490
+ ```
491
+
492
+ Server modules can still access env directly:
476
493
 
477
494
  ```ts
478
495
  import { env } from 'cloudflare:workers';
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { parseFile } from './parser.js';
6
6
  import { compileTemplate } from './template.js';
7
+ import { transpileTypeScript } from './transpile.js';
7
8
  import { filePathToPattern } from '../runtime/router.js';
8
9
  import * as fs from 'node:fs';
9
10
  import * as path from 'node:path';
@@ -31,6 +32,26 @@ function compactInlineJs(source) {
31
32
  .replace(/\s*([{}();,:])\s*/g, '$1')
32
33
  .trim();
33
34
  }
35
+ function rewriteImportedFunctionCalls(source, fnToModule) {
36
+ let out = source;
37
+ for (const [fnName, moduleId] of Object.entries(fnToModule)) {
38
+ if (!/^[A-Za-z_$][\w$]*$/.test(fnName))
39
+ continue;
40
+ const callRegex = new RegExp(`\\b${fnName}\\s*\\(`, 'g');
41
+ out = out.replace(callRegex, `${moduleId}.${fnName}(`);
42
+ }
43
+ return out;
44
+ }
45
+ function rewriteWorkerEnvAliases(source, aliases) {
46
+ let out = source;
47
+ for (const alias of aliases) {
48
+ if (!/^[A-Za-z_$][\w$]*$/.test(alias))
49
+ continue;
50
+ const aliasRegex = new RegExp(`\\b${alias}\\b`, 'g');
51
+ out = out.replace(aliasRegex, '__env');
52
+ }
53
+ return out;
54
+ }
34
55
  function parseNamedImportBindings(line) {
35
56
  const namesMatch = line.match(/import\s*\{([^}]+)\}/);
36
57
  if (!namesMatch)
@@ -113,13 +134,16 @@ export function compile(options) {
113
134
  // Use parseFile to properly split the <script> block from the template, and to
114
135
  // separate component imports (import X from '@kuratchi/ui/x.html') from regular code.
115
136
  // This prevents import lines from being inlined verbatim in the compiled function body.
116
- const compParsed = parseFile(rawSource);
137
+ const compParsed = parseFile(rawSource, { kind: 'component', filePath });
117
138
  // propsCode = script body with all import lines stripped out
118
139
  const propsCode = compParsed.script
119
140
  ? compParsed.script
120
141
  .replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '')
121
142
  .trim()
122
143
  : '';
144
+ const transpiledPropsCode = propsCode
145
+ ? transpileTypeScript(propsCode, `component-script:${fileName}.ts`)
146
+ : '';
123
147
  // template source (parseFile already removes the <script> block)
124
148
  let source = compParsed.template;
125
149
  // Extract optional <style> block â€" CSS is scoped and injected once per route at compile time
@@ -183,7 +207,7 @@ export function compile(options) {
183
207
  // Insert scope open after 'let __html = "";' (first line of body) and scope close at end
184
208
  const bodyLines = body.split('\n');
185
209
  const scopedBody = [bodyLines[0], scopeOpen, ...bodyLines.slice(1), scopeClose].join('\n');
186
- const fnBody = propsCode ? `${propsCode}\n ${scopedBody}` : scopedBody;
210
+ const fnBody = transpiledPropsCode ? `${transpiledPropsCode}\n ${scopedBody}` : scopedBody;
187
211
  const compiled = `function ${funcName}(props, __esc) {\n ${fnBody}\n return __html;\n}`;
188
212
  compiledComponentCache.set(fileName, compiled);
189
213
  return compiled;
@@ -595,7 +619,7 @@ export function compile(options) {
595
619
  }
596
620
  source = source.replace('</body>', actionScript + '\n</body>');
597
621
  // Parse layout for <script> block (component imports + data vars)
598
- const layoutParsed = parseFile(source);
622
+ const layoutParsed = parseFile(source, { kind: 'layout', filePath: layoutFile });
599
623
  const hasLayoutScript = layoutParsed.script && (Object.keys(layoutParsed.componentImports).length > 0 || layoutParsed.hasLoad);
600
624
  if (hasLayoutScript) {
601
625
  // Dynamic layout â€" has component imports and/or data declarations
@@ -830,7 +854,7 @@ export function compile(options) {
830
854
  let compiledLayoutActions = null;
831
855
  if (compiledLayout && fs.existsSync(path.join(routesDir, 'layout.html'))) {
832
856
  const layoutSource = fs.readFileSync(path.join(routesDir, 'layout.html'), 'utf-8');
833
- const layoutParsedForImports = parseFile(layoutSource);
857
+ const layoutParsedForImports = parseFile(layoutSource, { kind: 'layout', filePath: layoutFile });
834
858
  if (layoutParsedForImports.serverImports.length > 0) {
835
859
  const layoutFileDir = routesDir;
836
860
  const outFileDir = path.join(projectDir, '.kuratchi');
@@ -920,7 +944,7 @@ export function compile(options) {
920
944
  }
921
945
  // ── Page route (page.html) ──
922
946
  const source = fs.readFileSync(fullPath, 'utf-8');
923
- const parsed = parseFile(source);
947
+ const parsed = parseFile(source, { kind: 'route', filePath: fullPath });
924
948
  // Build a mapping: functionName â†' moduleId for all imports in this route
925
949
  const fnToModule = {};
926
950
  const outFileDir = path.join(projectDir, '.kuratchi');
@@ -1357,11 +1381,23 @@ function buildRouteObject(opts) {
1357
1381
  let scriptBody = parsed.script
1358
1382
  ? parsed.script.replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '').trim()
1359
1383
  : '';
1360
- for (const [fnName, moduleId] of Object.entries(fnToModule)) {
1361
- if (!/^[A-Za-z_$][\w$]*$/.test(fnName))
1362
- continue;
1363
- const callRegex = new RegExp(`\\b${fnName}\\s*\\(`, 'g');
1364
- scriptBody = scriptBody.replace(callRegex, `${moduleId}.${fnName}(`);
1384
+ scriptBody = rewriteImportedFunctionCalls(scriptBody, fnToModule);
1385
+ scriptBody = rewriteWorkerEnvAliases(scriptBody, parsed.workerEnvAliases);
1386
+ let explicitLoadFunction = parsed.loadFunction
1387
+ ? parsed.loadFunction.replace(/^export\s+/, '').trim()
1388
+ : '';
1389
+ if (explicitLoadFunction) {
1390
+ explicitLoadFunction = rewriteImportedFunctionCalls(explicitLoadFunction, fnToModule);
1391
+ explicitLoadFunction = rewriteWorkerEnvAliases(explicitLoadFunction, parsed.workerEnvAliases);
1392
+ }
1393
+ if (explicitLoadFunction && /\bawait\b/.test(scriptBody)) {
1394
+ throw new Error(`[kuratchi compiler] ${pattern}\nTop-level await cannot be mixed with export async function load(). Move async server work into load().`);
1395
+ }
1396
+ if (scriptBody) {
1397
+ scriptBody = transpileTypeScript(scriptBody, `route-script:${pattern}.ts`);
1398
+ }
1399
+ if (explicitLoadFunction) {
1400
+ explicitLoadFunction = transpileTypeScript(explicitLoadFunction, `route-load:${pattern}.ts`);
1365
1401
  }
1366
1402
  const scriptUsesAwait = /\bawait\b/.test(scriptBody);
1367
1403
  const scriptReturnVars = parsed.script
@@ -1370,7 +1406,10 @@ function buildRouteObject(opts) {
1370
1406
  // Load function â€" internal server prepass for async route script bodies
1371
1407
  // and data-get query state hydration.
1372
1408
  const hasDataGetQueries = Array.isArray(parsed.dataGetQueries) && parsed.dataGetQueries.length > 0;
1373
- if ((scriptBody && scriptUsesAwait) || hasDataGetQueries) {
1409
+ if (explicitLoadFunction) {
1410
+ parts.push(` load: ${explicitLoadFunction}`);
1411
+ }
1412
+ else if ((scriptBody && scriptUsesAwait) || hasDataGetQueries) {
1374
1413
  let loadBody = '';
1375
1414
  if (scriptBody && scriptUsesAwait) {
1376
1415
  loadBody = scriptBody;
@@ -10,6 +10,8 @@
10
10
  export interface ParsedFile {
11
11
  /** Script content (compile-time code, minus imports) */
12
12
  script: string | null;
13
+ /** Explicit server-side load function exported from the route script */
14
+ loadFunction: string | null;
13
15
  /** Template — the full HTML document with inline JS flow control */
14
16
  template: string;
15
17
  /** All imports from the script block */
@@ -34,6 +36,14 @@ export interface ParsedFile {
34
36
  }>;
35
37
  /** Imports found in a top-level client script block */
36
38
  clientImports: string[];
39
+ /** Top-level names returned from explicit load() */
40
+ loadReturnVars: string[];
41
+ /** Local aliases for Cloudflare Workers env imported from cloudflare:workers */
42
+ workerEnvAliases: string[];
43
+ }
44
+ interface ParseFileOptions {
45
+ kind?: 'route' | 'layout' | 'component';
46
+ filePath?: string;
37
47
  }
38
48
  /**
39
49
  * Parse a .html route file.
@@ -42,4 +52,5 @@ export interface ParsedFile {
42
52
  * If it includes reactive `$:` labels, it is preserved in template output.
43
53
  * Everything else (the HTML document) becomes the template.
44
54
  */
45
- export declare function parseFile(source: string): ParsedFile;
55
+ export declare function parseFile(source: string, options?: ParseFileOptions): ParsedFile;
56
+ export {};
@@ -108,6 +108,220 @@ function pushIdentifier(name, out) {
108
108
  if (!out.includes(name))
109
109
  out.push(name);
110
110
  }
111
+ function findMatchingToken(input, openPos, openChar, closeChar) {
112
+ let depth = 0;
113
+ let quote = null;
114
+ let escaped = false;
115
+ for (let i = openPos; i < input.length; i++) {
116
+ const ch = input[i];
117
+ if (quote) {
118
+ if (escaped) {
119
+ escaped = false;
120
+ continue;
121
+ }
122
+ if (ch === '\\') {
123
+ escaped = true;
124
+ continue;
125
+ }
126
+ if (ch === quote)
127
+ quote = null;
128
+ continue;
129
+ }
130
+ if (ch === '"' || ch === "'" || ch === '`') {
131
+ quote = ch;
132
+ continue;
133
+ }
134
+ if (ch === openChar)
135
+ depth++;
136
+ else if (ch === closeChar) {
137
+ depth--;
138
+ if (depth === 0)
139
+ return i;
140
+ }
141
+ }
142
+ return -1;
143
+ }
144
+ function extractCloudflareEnvAliases(importLine) {
145
+ if (!/from\s+['"]cloudflare:workers['"]/.test(importLine))
146
+ return [];
147
+ const namedMatch = importLine.match(/import\s*\{([\s\S]*?)\}\s*from\s+['"]cloudflare:workers['"]/);
148
+ if (!namedMatch)
149
+ return [];
150
+ const aliases = [];
151
+ for (const rawPart of splitTopLevel(namedMatch[1], ',')) {
152
+ const part = rawPart.trim();
153
+ if (!part)
154
+ continue;
155
+ const envMatch = part.match(/^env(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
156
+ if (envMatch)
157
+ aliases.push(envMatch[1] || 'env');
158
+ }
159
+ return aliases;
160
+ }
161
+ function extractReturnObjectKeys(body) {
162
+ const keys = [];
163
+ let i = 0;
164
+ let depthParen = 0;
165
+ let depthBracket = 0;
166
+ let depthBrace = 0;
167
+ let quote = null;
168
+ let escaped = false;
169
+ while (i < body.length) {
170
+ const ch = body[i];
171
+ if (quote) {
172
+ if (escaped) {
173
+ escaped = false;
174
+ i++;
175
+ continue;
176
+ }
177
+ if (ch === '\\') {
178
+ escaped = true;
179
+ i++;
180
+ continue;
181
+ }
182
+ if (ch === quote)
183
+ quote = null;
184
+ i++;
185
+ continue;
186
+ }
187
+ if (ch === '"' || ch === "'" || ch === '`') {
188
+ quote = ch;
189
+ i++;
190
+ continue;
191
+ }
192
+ if (ch === '(')
193
+ depthParen++;
194
+ else if (ch === ')')
195
+ depthParen = Math.max(0, depthParen - 1);
196
+ else if (ch === '[')
197
+ depthBracket++;
198
+ else if (ch === ']')
199
+ depthBracket = Math.max(0, depthBracket - 1);
200
+ else if (ch === '{')
201
+ depthBrace++;
202
+ else if (ch === '}')
203
+ depthBrace = Math.max(0, depthBrace - 1);
204
+ if (depthParen !== 0 || depthBracket !== 0 || depthBrace !== 0) {
205
+ i++;
206
+ continue;
207
+ }
208
+ const rest = body.slice(i);
209
+ const returnMatch = /^return\b/.exec(rest);
210
+ if (!returnMatch) {
211
+ i++;
212
+ continue;
213
+ }
214
+ i += returnMatch[0].length;
215
+ while (i < body.length && /\s/.test(body[i]))
216
+ i++;
217
+ if (body[i] !== '{')
218
+ continue;
219
+ const closeIdx = findMatchingToken(body, i, '{', '}');
220
+ if (closeIdx === -1)
221
+ break;
222
+ const objectBody = body.slice(i + 1, closeIdx);
223
+ for (const rawProp of splitTopLevel(objectBody, ',')) {
224
+ const prop = rawProp.trim();
225
+ if (!prop || prop.startsWith('...'))
226
+ continue;
227
+ const keyMatch = prop.match(/^([A-Za-z_$][\w$]*)\s*(?::|$)/);
228
+ if (keyMatch)
229
+ pushIdentifier(keyMatch[1], keys);
230
+ }
231
+ break;
232
+ }
233
+ return keys;
234
+ }
235
+ function extractExplicitLoad(scriptBody) {
236
+ let i = 0;
237
+ let depthParen = 0;
238
+ let depthBracket = 0;
239
+ let depthBrace = 0;
240
+ let quote = null;
241
+ let escaped = false;
242
+ while (i < scriptBody.length) {
243
+ const ch = scriptBody[i];
244
+ if (quote) {
245
+ if (escaped) {
246
+ escaped = false;
247
+ i++;
248
+ continue;
249
+ }
250
+ if (ch === '\\') {
251
+ escaped = true;
252
+ i++;
253
+ continue;
254
+ }
255
+ if (ch === quote)
256
+ quote = null;
257
+ i++;
258
+ continue;
259
+ }
260
+ if (ch === '"' || ch === "'" || ch === '`') {
261
+ quote = ch;
262
+ i++;
263
+ continue;
264
+ }
265
+ if (ch === '(')
266
+ depthParen++;
267
+ else if (ch === ')')
268
+ depthParen = Math.max(0, depthParen - 1);
269
+ else if (ch === '[')
270
+ depthBracket++;
271
+ else if (ch === ']')
272
+ depthBracket = Math.max(0, depthBracket - 1);
273
+ else if (ch === '{')
274
+ depthBrace++;
275
+ else if (ch === '}')
276
+ depthBrace = Math.max(0, depthBrace - 1);
277
+ if (depthParen !== 0 || depthBracket !== 0 || depthBrace !== 0) {
278
+ i++;
279
+ continue;
280
+ }
281
+ const rest = scriptBody.slice(i);
282
+ const fnMatch = /^export\s+(async\s+)?function\s+load\s*/.exec(rest);
283
+ if (!fnMatch) {
284
+ i++;
285
+ continue;
286
+ }
287
+ const openParen = i + fnMatch[0].length;
288
+ if (scriptBody[openParen] !== '(') {
289
+ throw new Error('[kuratchi compiler] Could not parse exported load() declaration.');
290
+ }
291
+ const closeParen = findMatchingToken(scriptBody, openParen, '(', ')');
292
+ if (closeParen === -1) {
293
+ throw new Error('[kuratchi compiler] Could not parse exported load() parameters.');
294
+ }
295
+ let openBrace = closeParen + 1;
296
+ while (openBrace < scriptBody.length && /\s/.test(scriptBody[openBrace]))
297
+ openBrace++;
298
+ if (scriptBody[openBrace] !== '{') {
299
+ throw new Error('[kuratchi compiler] export load() must use a function body.');
300
+ }
301
+ const closeBrace = findMatchingToken(scriptBody, openBrace, '{', '}');
302
+ if (closeBrace === -1) {
303
+ throw new Error('[kuratchi compiler] Could not parse exported load() body.');
304
+ }
305
+ return {
306
+ loadFunction: scriptBody.slice(i, closeBrace + 1).trim(),
307
+ remainingScript: `${scriptBody.slice(0, i)}\n${scriptBody.slice(closeBrace + 1)}`.trim(),
308
+ returnVars: extractReturnObjectKeys(scriptBody.slice(openBrace + 1, closeBrace)),
309
+ };
310
+ }
311
+ return { loadFunction: null, remainingScript: scriptBody.trim(), returnVars: [] };
312
+ }
313
+ function hasFrameworkEnvEscapeHatch(source) {
314
+ return /\bglobalThis\.__cloudflare_env__\b/.test(source) || /\b__cloudflare_env__\b/.test(source);
315
+ }
316
+ function buildEnvAccessError(kind, filePath, detail) {
317
+ const label = filePath || kind;
318
+ const guidance = kind === 'route'
319
+ ? 'Route top-level <script> is server-side. Client/reactive scripts cannot access Cloudflare env.'
320
+ : kind === 'layout'
321
+ ? 'Layout scripts cannot access Cloudflare env directly.'
322
+ : 'Component scripts cannot access Cloudflare env directly.';
323
+ return new Error(`[kuratchi compiler] ${label}\n${detail}\n${guidance}\nRead env on the server and pass the value into the template explicitly.`);
324
+ }
111
325
  function collectPatternNames(pattern, out) {
112
326
  const p = pattern.trim();
113
327
  if (!p)
@@ -410,7 +624,8 @@ function extractTopLevelFunctionNames(scriptBody) {
410
624
  * If it includes reactive `$:` labels, it is preserved in template output.
411
625
  * Everything else (the HTML document) becomes the template.
412
626
  */
413
- export function parseFile(source) {
627
+ export function parseFile(source, options = {}) {
628
+ const kind = options.kind || 'route';
414
629
  let script = null;
415
630
  let clientScript = null;
416
631
  let template = source;
@@ -432,6 +647,7 @@ export function parseFile(source) {
432
647
  const serverImports = [];
433
648
  const clientImports = [];
434
649
  const componentImports = {};
650
+ const workerEnvAliases = [];
435
651
  if (script) {
436
652
  // Support both single-line and multiline static imports.
437
653
  const importRegex = /^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm;
@@ -455,31 +671,62 @@ export function parseFile(source) {
455
671
  else {
456
672
  serverImports.push(line);
457
673
  }
674
+ for (const alias of extractCloudflareEnvAliases(line)) {
675
+ if (!workerEnvAliases.includes(alias))
676
+ workerEnvAliases.push(alias);
677
+ }
458
678
  }
459
679
  }
460
680
  if (clientScript) {
461
681
  const importRegex = /^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm;
462
682
  let m;
463
683
  while ((m = importRegex.exec(clientScript)) !== null) {
464
- clientImports.push(m[0].trim());
684
+ const line = m[0].trim();
685
+ clientImports.push(line);
686
+ if (extractCloudflareEnvAliases(line).length > 0) {
687
+ throw buildEnvAccessError(kind, options.filePath, 'Client <script> blocks cannot import env from cloudflare:workers.');
688
+ }
689
+ }
690
+ if (hasFrameworkEnvEscapeHatch(clientScript)) {
691
+ throw buildEnvAccessError(kind, options.filePath, 'Client <script> blocks cannot access framework env internals.');
465
692
  }
466
693
  }
467
694
  // Extract top-level variable declarations from script body (after removing imports)
468
695
  const dataVars = [];
696
+ const loadReturnVars = [];
469
697
  let scriptBody = '';
698
+ let loadFunction = null;
470
699
  if (script) {
471
700
  // Remove import lines to get the script body
472
- scriptBody = script.replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '').trim();
473
- const topLevelVars = extractTopLevelDataVars(scriptBody);
701
+ const rawScriptBody = script.replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '').trim();
702
+ const explicitLoad = extractExplicitLoad(rawScriptBody);
703
+ scriptBody = explicitLoad.remainingScript;
704
+ loadFunction = explicitLoad.loadFunction;
705
+ for (const name of explicitLoad.returnVars) {
706
+ if (!loadReturnVars.includes(name))
707
+ loadReturnVars.push(name);
708
+ }
709
+ if (hasFrameworkEnvEscapeHatch(rawScriptBody)) {
710
+ throw buildEnvAccessError(kind, options.filePath, 'Route/component scripts cannot access framework env internals.');
711
+ }
712
+ if (workerEnvAliases.length > 0 && kind !== 'route') {
713
+ throw buildEnvAccessError(kind, options.filePath, `Imported env from cloudflare:workers in a ${kind} script.`);
714
+ }
715
+ const transpiledScriptBody = transpileTypeScript(scriptBody, 'route-script.ts');
716
+ const topLevelVars = extractTopLevelDataVars(transpiledScriptBody);
474
717
  for (const v of topLevelVars)
475
718
  dataVars.push(v);
476
- const topLevelFns = extractTopLevelFunctionNames(scriptBody);
719
+ const topLevelFns = extractTopLevelFunctionNames(transpiledScriptBody);
477
720
  for (const fn of topLevelFns) {
478
721
  if (!dataVars.includes(fn))
479
722
  dataVars.push(fn);
480
723
  }
724
+ for (const name of loadReturnVars) {
725
+ if (!dataVars.includes(name))
726
+ dataVars.push(name);
727
+ }
481
728
  }
482
- const hasLoad = scriptBody.length > 0;
729
+ const hasLoad = scriptBody.length > 0 || !!loadFunction;
483
730
  // Strip HTML comments from the template before scanning for action references.
484
731
  // This prevents commented-out code (<!-- ... -->) from being parsed as live
485
732
  // action expressions, which would cause false "Invalid action expression" errors.
@@ -552,5 +799,20 @@ export function parseFile(source) {
552
799
  if (!exists)
553
800
  dataGetQueries.push({ fnName, argsExpr, asName, key });
554
801
  }
555
- return { script, template, serverImports, hasLoad, actionFunctions, dataVars, componentImports, pollFunctions, dataGetQueries, clientImports };
802
+ return {
803
+ script,
804
+ loadFunction,
805
+ template,
806
+ serverImports,
807
+ hasLoad,
808
+ actionFunctions,
809
+ dataVars,
810
+ componentImports,
811
+ pollFunctions,
812
+ dataGetQueries,
813
+ clientImports,
814
+ loadReturnVars,
815
+ workerEnvAliases,
816
+ };
556
817
  }
818
+ import { transpileTypeScript } from './transpile.js';
@@ -174,8 +174,10 @@ function transformClientScriptBlock(block) {
174
174
  const openTag = match[1];
175
175
  const body = match[2];
176
176
  const closeTag = match[3];
177
- if (!/\$\s*:/.test(body))
178
- return block;
177
+ if (!/\$\s*:/.test(body)) {
178
+ const transpiled = transpileTypeScript(body, 'client-script.ts');
179
+ return `${openTag}${transpiled}${closeTag}`;
180
+ }
179
181
  const out = [];
180
182
  const lines = body.split('\n');
181
183
  const reactiveVars = new Set();
@@ -270,7 +272,8 @@ function transformClientScriptBlock(block) {
270
272
  break;
271
273
  }
272
274
  out.splice(insertAt, 0, 'const __k$ = window.__kuratchiReactive;');
273
- return `${openTag}${out.join('\n')}${closeTag}`;
275
+ const transpiled = transpileTypeScript(out.join('\n'), 'client-script.ts');
276
+ return `${openTag}${transpiled}${closeTag}`;
274
277
  }
275
278
  function braceDelta(line) {
276
279
  let delta = 0;
@@ -822,3 +825,4 @@ export function generateRenderFunction(template) {
822
825
  ${body}
823
826
  }`;
824
827
  }
828
+ import { transpileTypeScript } from './transpile.js';
@@ -0,0 +1 @@
1
+ export declare function transpileTypeScript(source: string, contextLabel: string, compilerOptions?: Record<string, unknown>): string;
@@ -0,0 +1,61 @@
1
+ import { createRequire } from 'node:module';
2
+ import * as fs from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ const require = createRequire(import.meta.url);
5
+ let tsImpl;
6
+ let bunTranspiler;
7
+ function transpileWithBun(source) {
8
+ const BunRuntime = globalThis.Bun;
9
+ if (!BunRuntime?.Transpiler)
10
+ return null;
11
+ if (!bunTranspiler) {
12
+ bunTranspiler = new BunRuntime.Transpiler({ loader: 'ts' });
13
+ }
14
+ const output = bunTranspiler.transformSync(source);
15
+ return typeof output === 'string' ? output.trim() : String(output).trim();
16
+ }
17
+ function getTypeScript() {
18
+ if (!tsImpl) {
19
+ const localTsPath = fileURLToPath(new URL('../../node_modules/typescript/lib/typescript.js', import.meta.url));
20
+ if (fs.existsSync(localTsPath)) {
21
+ tsImpl = require(localTsPath);
22
+ }
23
+ else {
24
+ tsImpl = require(require.resolve('typescript', { paths: [process.cwd()] }));
25
+ }
26
+ }
27
+ return tsImpl;
28
+ }
29
+ function formatDiagnostic(diag) {
30
+ if (!diag.file || typeof diag.start !== 'number') {
31
+ return getTypeScript().flattenDiagnosticMessageText(diag.messageText, '\n');
32
+ }
33
+ const pos = diag.file.getLineAndCharacterOfPosition(diag.start);
34
+ const message = getTypeScript().flattenDiagnosticMessageText(diag.messageText, '\n');
35
+ return `${diag.file.fileName}:${pos.line + 1}:${pos.character + 1} ${message}`;
36
+ }
37
+ export function transpileTypeScript(source, contextLabel, compilerOptions = {}) {
38
+ if (!source.trim())
39
+ return source;
40
+ const bunOutput = transpileWithBun(source);
41
+ if (bunOutput !== null)
42
+ return bunOutput;
43
+ const ts = getTypeScript();
44
+ const result = ts.transpileModule(source, {
45
+ compilerOptions: {
46
+ target: ts.ScriptTarget.ES2022,
47
+ module: ts.ModuleKind.ESNext,
48
+ importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Remove,
49
+ verbatimModuleSyntax: false,
50
+ ...compilerOptions,
51
+ },
52
+ reportDiagnostics: true,
53
+ fileName: contextLabel,
54
+ });
55
+ const diagnostics = (result.diagnostics || []).filter((diag) => diag.category === ts.DiagnosticCategory.Error);
56
+ if (diagnostics.length > 0) {
57
+ const rendered = diagnostics.map(formatDiagnostic).join('\n');
58
+ throw new Error(`[kuratchi compiler] TypeScript transpile failed for ${contextLabel}\n${rendered}`);
59
+ }
60
+ return result.outputText.trim();
61
+ }
package/package.json CHANGED
@@ -1,48 +1,50 @@
1
- {
2
- "name": "@kuratchi/js",
3
- "version": "0.0.4",
4
- "description": "A thin, Cloudflare Workers-native web framework with Svelte-inspired syntax",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "bin": {
9
- "kuratchi": "./dist/cli.js"
10
- },
11
- "files": [
12
- "dist",
13
- "README.md"
14
- ],
15
- "scripts": {
16
- "build": "tsc -p tsconfig.build.json",
17
- "check": "tsc -p tsconfig.build.json --noEmit",
18
- "test": "bun test",
19
- "test:watch": "bun test --watch",
20
- "prepublishOnly": "npm run build"
21
- },
22
- "exports": {
23
- ".": {
24
- "types": "./dist/index.d.ts",
25
- "import": "./dist/index.js"
26
- },
27
- "./runtime/context.js": {
28
- "types": "./dist/runtime/context.d.ts",
29
- "import": "./dist/runtime/context.js"
30
- },
31
- "./runtime/do.js": {
32
- "types": "./dist/runtime/do.d.ts",
33
- "import": "./dist/runtime/do.js"
34
- },
35
- "./package.json": "./package.json"
36
- },
37
- "engines": {
38
- "node": "\u003e=18"
39
- },
40
- "publishConfig": {
41
- "access": "public"
42
- },
43
- "devDependencies": {
44
- "@cloudflare/workers-types": "^4.20260223.0",
45
- "@types/node": "^24.4.0",
46
- "typescript": "^5.8.0"
47
- }
48
- }
1
+ {
2
+ "name": "@kuratchi/js",
3
+ "version": "0.0.6",
4
+ "description": "A thin, Cloudflare Workers-native web framework with Svelte-inspired syntax",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "kuratchi": "./dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.build.json",
17
+ "check": "tsc -p tsconfig.build.json --noEmit",
18
+ "test": "bun test",
19
+ "test:watch": "bun test --watch",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ },
27
+ "./runtime/context.js": {
28
+ "types": "./dist/runtime/context.d.ts",
29
+ "import": "./dist/runtime/context.js"
30
+ },
31
+ "./runtime/do.js": {
32
+ "types": "./dist/runtime/do.d.ts",
33
+ "import": "./dist/runtime/do.js"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "typescript": "^5.8.0"
45
+ },
46
+ "devDependencies": {
47
+ "@cloudflare/workers-types": "^4.20260223.0",
48
+ "@types/node": "^24.4.0"
49
+ }
50
+ }