@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 +20 -3
- package/dist/compiler/index.js +50 -11
- package/dist/compiler/parser.d.ts +12 -1
- package/dist/compiler/parser.js +269 -7
- package/dist/compiler/template.js +7 -3
- package/dist/compiler/transpile.d.ts +1 -0
- package/dist/compiler/transpile.js +61 -0
- package/package.json +50 -48
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
|
|
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';
|
package/dist/compiler/index.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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 (
|
|
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 {};
|
package/dist/compiler/parser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
473
|
-
const
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"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": "
|
|
39
|
-
},
|
|
40
|
-
"publishConfig": {
|
|
41
|
-
"access": "public"
|
|
42
|
-
},
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
}
|