@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/README.md
CHANGED
|
@@ -16,9 +16,9 @@ cd my-app
|
|
|
16
16
|
bun run dev
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
## How it works
|
|
20
|
-
|
|
21
|
-
`kuratchi build` (or `kuratchi watch`) scans `src/routes/` and generates framework output:
|
|
19
|
+
## How it works
|
|
20
|
+
|
|
21
|
+
`kuratchi build` (or `kuratchi watch`) scans `src/routes/` and generates framework output:
|
|
22
22
|
|
|
23
23
|
| File | Purpose |
|
|
24
24
|
|---|---|
|
|
@@ -26,11 +26,13 @@ bun run dev
|
|
|
26
26
|
| `.kuratchi/worker.js` | Stable wrangler entry - re-exports the fetch handler plus all Durable Object and Agent classes |
|
|
27
27
|
| `.kuratchi/do/*.js` | Generated Durable Object RPC proxy modules for `$durable-objects/*` imports |
|
|
28
28
|
|
|
29
|
-
Point wrangler at the entry and you're done. **No `src/index.ts` needed.**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
Point wrangler at the entry and you're done. **No `src/index.ts` needed.**
|
|
30
|
+
|
|
31
|
+
For the framework's internal compiler/runtime orchestration and tracked implementation roadmap, see [ARCHITECTURE.md](./ARCHITECTURE.md).
|
|
32
|
+
|
|
33
|
+
```jsonc
|
|
34
|
+
// wrangler.jsonc
|
|
35
|
+
{
|
|
34
36
|
"main": ".kuratchi/worker.js"
|
|
35
37
|
}
|
|
36
38
|
```
|
package/dist/cli.js
CHANGED
|
@@ -6,34 +6,41 @@ import { compile } from './compiler/index.js';
|
|
|
6
6
|
import * as path from 'node:path';
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as net from 'node:net';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
9
10
|
import { spawn } from 'node:child_process';
|
|
10
11
|
const args = process.argv.slice(2);
|
|
11
12
|
const command = args[0];
|
|
12
13
|
const projectDir = process.cwd();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
14
|
+
void main().catch((err) => {
|
|
15
|
+
console.error(`[kuratchi] ${err?.message ?? err}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
});
|
|
18
|
+
async function main() {
|
|
19
|
+
switch (command) {
|
|
20
|
+
case 'build':
|
|
21
|
+
runBuild();
|
|
22
|
+
return;
|
|
23
|
+
case 'watch':
|
|
24
|
+
await runWatch(false);
|
|
25
|
+
return;
|
|
26
|
+
case 'dev':
|
|
27
|
+
await runWatch(true);
|
|
28
|
+
return;
|
|
29
|
+
case 'create':
|
|
30
|
+
await runCreate();
|
|
31
|
+
return;
|
|
32
|
+
default:
|
|
33
|
+
console.log(`
|
|
34
|
+
KuratchiJS CLI
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
kuratchi create [name] Scaffold a new KuratchiJS project
|
|
38
|
+
kuratchi build Compile routes once
|
|
39
|
+
kuratchi dev Compile, watch for changes, and start wrangler dev server
|
|
40
|
+
kuratchi watch Compile + watch only (no wrangler — for custom setups)
|
|
35
41
|
`);
|
|
36
|
-
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
37
44
|
}
|
|
38
45
|
async function runCreate() {
|
|
39
46
|
const { create } = await import('./create.js');
|
|
@@ -53,7 +60,7 @@ function runBuild(isDev = false) {
|
|
|
53
60
|
process.exit(1);
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
|
-
function runWatch(withWrangler = false) {
|
|
63
|
+
async function runWatch(withWrangler = false) {
|
|
57
64
|
runBuild(true);
|
|
58
65
|
const routesDir = path.join(projectDir, 'src', 'routes');
|
|
59
66
|
const serverDir = path.join(projectDir, 'src', 'server');
|
|
@@ -80,11 +87,10 @@ function runWatch(withWrangler = false) {
|
|
|
80
87
|
// `kuratchi dev` also starts the wrangler dev server.
|
|
81
88
|
// `kuratchi watch` is the compiler-only mode for custom setups.
|
|
82
89
|
if (withWrangler) {
|
|
83
|
-
startWranglerDev()
|
|
84
|
-
|
|
85
|
-
process.exit(1);
|
|
86
|
-
});
|
|
90
|
+
await startWranglerDev();
|
|
91
|
+
return;
|
|
87
92
|
}
|
|
93
|
+
await new Promise(() => { });
|
|
88
94
|
}
|
|
89
95
|
function hasPortFlag(inputArgs) {
|
|
90
96
|
for (let i = 0; i < inputArgs.length; i++) {
|
|
@@ -117,25 +123,13 @@ async function findOpenPort(start = 8787, end = 8899) {
|
|
|
117
123
|
}
|
|
118
124
|
async function startWranglerDev() {
|
|
119
125
|
const passthroughArgs = args.slice(1);
|
|
120
|
-
const wranglerArgs = ['
|
|
126
|
+
const wranglerArgs = ['dev', ...passthroughArgs];
|
|
121
127
|
if (!hasPortFlag(passthroughArgs)) {
|
|
122
128
|
const port = await findOpenPort();
|
|
123
129
|
wranglerArgs.push('--port', String(port));
|
|
124
130
|
console.log(`[kuratchi] Starting wrangler dev on port ${port}`);
|
|
125
131
|
}
|
|
126
|
-
|
|
127
|
-
// prematurely when launched via a script runner (e.g. `bun run dev`).
|
|
128
|
-
const isWin = process.platform === 'win32';
|
|
129
|
-
const wrangler = isWin
|
|
130
|
-
? spawn('npx ' + wranglerArgs.join(' '), {
|
|
131
|
-
cwd: projectDir,
|
|
132
|
-
stdio: ['pipe', 'inherit', 'inherit'],
|
|
133
|
-
shell: true,
|
|
134
|
-
})
|
|
135
|
-
: spawn('npx', wranglerArgs, {
|
|
136
|
-
cwd: projectDir,
|
|
137
|
-
stdio: ['pipe', 'inherit', 'inherit'],
|
|
138
|
-
});
|
|
132
|
+
const wrangler = spawnWranglerProcess(wranglerArgs);
|
|
139
133
|
const cleanup = () => {
|
|
140
134
|
if (!wrangler.killed)
|
|
141
135
|
wrangler.kill();
|
|
@@ -143,10 +137,49 @@ async function startWranglerDev() {
|
|
|
143
137
|
process.on('exit', cleanup);
|
|
144
138
|
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
145
139
|
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
140
|
+
await new Promise((resolve, reject) => {
|
|
141
|
+
wrangler.on('exit', (code) => {
|
|
142
|
+
if (code !== 0 && code !== null) {
|
|
143
|
+
reject(new Error(`wrangler exited with code ${code}`));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
resolve();
|
|
147
|
+
});
|
|
148
|
+
wrangler.on('error', (err) => {
|
|
149
|
+
reject(err);
|
|
150
|
+
});
|
|
151
|
+
}).catch((err) => {
|
|
152
|
+
console.error(`[kuratchi] Failed to start wrangler dev: ${err?.message ?? err}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function resolveWranglerBin() {
|
|
157
|
+
try {
|
|
158
|
+
const projectPackageJson = path.join(projectDir, 'package.json');
|
|
159
|
+
const projectRequire = createRequire(projectPackageJson);
|
|
160
|
+
return projectRequire.resolve('wrangler/bin/wrangler.js');
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function getNodeExecutable() {
|
|
167
|
+
if (!process.versions.bun)
|
|
168
|
+
return process.execPath;
|
|
169
|
+
return 'node';
|
|
170
|
+
}
|
|
171
|
+
function spawnWranglerProcess(wranglerArgs) {
|
|
172
|
+
const localWranglerBin = resolveWranglerBin();
|
|
173
|
+
const stdio = ['pipe', 'inherit', 'inherit'];
|
|
174
|
+
if (localWranglerBin) {
|
|
175
|
+
return spawn(getNodeExecutable(), [localWranglerBin, ...wranglerArgs], {
|
|
176
|
+
cwd: projectDir,
|
|
177
|
+
stdio,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const fallbackCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
181
|
+
return spawn(fallbackCommand, ['wrangler', ...wranglerArgs], {
|
|
182
|
+
cwd: projectDir,
|
|
183
|
+
stdio,
|
|
151
184
|
});
|
|
152
185
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
const ALL_API_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
|
|
4
|
+
export function compileApiRoute(opts) {
|
|
5
|
+
const outFileDir = path.join(opts.projectDir, '.kuratchi');
|
|
6
|
+
const absRoutePath = opts.transformModule(opts.fullPath);
|
|
7
|
+
let importPath = path.relative(outFileDir, absRoutePath).replace(/\\/g, '/');
|
|
8
|
+
if (!importPath.startsWith('.'))
|
|
9
|
+
importPath = './' + importPath;
|
|
10
|
+
const moduleId = opts.allocateModuleId();
|
|
11
|
+
opts.pushImport(`import * as ${moduleId} from '${importPath}';`);
|
|
12
|
+
const apiSource = fs.readFileSync(opts.fullPath, 'utf-8');
|
|
13
|
+
const exportedMethods = ALL_API_METHODS.filter((method) => {
|
|
14
|
+
const fnPattern = new RegExp(`export\\s+(async\\s+)?function\\s+${method}\\b`);
|
|
15
|
+
const reExportPattern = new RegExp(`export\\s*\\{[^}]*\\b\\w+\\s+as\\s+${method}\\b`);
|
|
16
|
+
const namedExportPattern = new RegExp(`export\\s*\\{[^}]*\\b${method}\\b`);
|
|
17
|
+
return fnPattern.test(apiSource) || reExportPattern.test(apiSource) || namedExportPattern.test(apiSource);
|
|
18
|
+
});
|
|
19
|
+
const methodEntries = exportedMethods
|
|
20
|
+
.map((method) => `${method}: ${moduleId}.${method}`)
|
|
21
|
+
.join(', ');
|
|
22
|
+
return `{ pattern: '${opts.pattern}', __api: true, ${methodEntries} }`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
const MIME_TYPES = {
|
|
5
|
+
'.css': 'text/css; charset=utf-8',
|
|
6
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
7
|
+
'.json': 'application/json; charset=utf-8',
|
|
8
|
+
'.svg': 'image/svg+xml',
|
|
9
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
10
|
+
};
|
|
11
|
+
export function compileAssets(assetsDir) {
|
|
12
|
+
const compiledAssets = [];
|
|
13
|
+
if (!fs.existsSync(assetsDir))
|
|
14
|
+
return compiledAssets;
|
|
15
|
+
const scanAssets = (dir, prefix) => {
|
|
16
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
scanAssets(path.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
22
|
+
const mime = MIME_TYPES[ext];
|
|
23
|
+
if (!mime)
|
|
24
|
+
continue;
|
|
25
|
+
const content = fs.readFileSync(path.join(dir, entry.name), 'utf-8');
|
|
26
|
+
const etag = '"' + crypto.createHash('md5').update(content).digest('hex').slice(0, 12) + '"';
|
|
27
|
+
const name = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
28
|
+
compiledAssets.push({ name, content, mime, etag });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
scanAssets(assetsDir, '');
|
|
32
|
+
return compiledAssets;
|
|
33
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CompiledAsset } from './asset-pipeline.js';
|
|
2
|
+
import { type RouteImportEntry } from './import-linking.js';
|
|
3
|
+
export interface ClientEventRegistration {
|
|
4
|
+
routeId: string;
|
|
5
|
+
handlerId: string;
|
|
6
|
+
argsExpr: string | null;
|
|
7
|
+
}
|
|
8
|
+
export interface ClientModuleCompiler {
|
|
9
|
+
createRegistry(scopeId: string, importEntries: RouteImportEntry[]): ClientRouteRegistry;
|
|
10
|
+
createRouteRegistry(routeIndex: number, importEntries: RouteImportEntry[]): ClientRouteRegistry;
|
|
11
|
+
getCompiledAssets(): CompiledAsset[];
|
|
12
|
+
}
|
|
13
|
+
export interface ClientRouteRegistry {
|
|
14
|
+
hasBindings(): boolean;
|
|
15
|
+
hasBindingReference(expression: string): boolean;
|
|
16
|
+
registerEventHandler(eventName: string, expression: string): ClientEventRegistration | null;
|
|
17
|
+
buildEntryAsset(): {
|
|
18
|
+
assetName: string;
|
|
19
|
+
asset: CompiledAsset;
|
|
20
|
+
} | null;
|
|
21
|
+
}
|
|
22
|
+
export declare function createClientModuleCompiler(opts: {
|
|
23
|
+
projectDir: string;
|
|
24
|
+
srcDir: string;
|
|
25
|
+
}): ClientModuleCompiler;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { collectReferencedIdentifiers, parseImportStatement } from './import-linking.js';
|
|
5
|
+
import { transpileTypeScript } from './transpile.js';
|
|
6
|
+
function resolveExistingModuleFile(absBase) {
|
|
7
|
+
const candidates = [
|
|
8
|
+
absBase,
|
|
9
|
+
absBase + '.ts',
|
|
10
|
+
absBase + '.js',
|
|
11
|
+
absBase + '.mjs',
|
|
12
|
+
absBase + '.cjs',
|
|
13
|
+
path.join(absBase, 'index.ts'),
|
|
14
|
+
path.join(absBase, 'index.js'),
|
|
15
|
+
path.join(absBase, 'index.mjs'),
|
|
16
|
+
path.join(absBase, 'index.cjs'),
|
|
17
|
+
];
|
|
18
|
+
for (const candidate of candidates) {
|
|
19
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
|
|
20
|
+
return candidate;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function buildAsset(name, content) {
|
|
25
|
+
return {
|
|
26
|
+
name,
|
|
27
|
+
content,
|
|
28
|
+
mime: 'text/javascript; charset=utf-8',
|
|
29
|
+
etag: '"' + crypto.createHash('md5').update(content).digest('hex').slice(0, 12) + '"',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function toRelativeSpecifier(fromAssetName, toAssetName) {
|
|
33
|
+
let rel = path.posix.relative(path.posix.dirname(fromAssetName), toAssetName);
|
|
34
|
+
if (!rel.startsWith('.'))
|
|
35
|
+
rel = './' + rel;
|
|
36
|
+
return rel;
|
|
37
|
+
}
|
|
38
|
+
function rewriteImportSpecifiers(source, rewriteSpecifier) {
|
|
39
|
+
let rewritten = source.replace(/(from\s+)(['"])([^'"]+)\2/g, (_match, prefix, quote, spec) => {
|
|
40
|
+
return `${prefix}${quote}${rewriteSpecifier(spec)}${quote}`;
|
|
41
|
+
});
|
|
42
|
+
rewritten = rewritten.replace(/(import\s*\(\s*)(['"])([^'"]+)\2(\s*\))/g, (_match, prefix, quote, spec, suffix) => {
|
|
43
|
+
return `${prefix}${quote}${rewriteSpecifier(spec)}${quote}${suffix}`;
|
|
44
|
+
});
|
|
45
|
+
return rewritten;
|
|
46
|
+
}
|
|
47
|
+
function resolveClientImportTarget(srcDir, importerAbs, spec) {
|
|
48
|
+
if (spec.startsWith('$')) {
|
|
49
|
+
const slashIdx = spec.indexOf('/');
|
|
50
|
+
const folder = slashIdx === -1 ? spec.slice(1) : spec.slice(1, slashIdx);
|
|
51
|
+
const rest = slashIdx === -1 ? '' : spec.slice(slashIdx + 1);
|
|
52
|
+
if (folder !== 'client' && folder !== 'shared') {
|
|
53
|
+
throw new Error(`[kuratchi compiler] Unsupported browser import realm "${spec}". Only $client/* and $shared/* may be loaded in the browser.`);
|
|
54
|
+
}
|
|
55
|
+
const abs = path.join(srcDir, folder, rest);
|
|
56
|
+
const resolved = resolveExistingModuleFile(abs);
|
|
57
|
+
if (!resolved) {
|
|
58
|
+
throw new Error(`[kuratchi compiler] Browser import not found: ${spec}`);
|
|
59
|
+
}
|
|
60
|
+
return resolved;
|
|
61
|
+
}
|
|
62
|
+
if (spec.startsWith('.')) {
|
|
63
|
+
const abs = path.resolve(path.dirname(importerAbs), spec);
|
|
64
|
+
const resolved = resolveExistingModuleFile(abs);
|
|
65
|
+
if (!resolved) {
|
|
66
|
+
throw new Error(`[kuratchi compiler] Browser import not found: ${spec}`);
|
|
67
|
+
}
|
|
68
|
+
return resolved;
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`[kuratchi compiler] Browser modules currently only support project-local imports ($client, $shared, or relative). Unsupported import: ${spec}`);
|
|
71
|
+
}
|
|
72
|
+
class CompilerBackedClientRouteRegistry {
|
|
73
|
+
compiler;
|
|
74
|
+
bindingMap = new Map();
|
|
75
|
+
clientOnlyBindings = new Set();
|
|
76
|
+
handlerByKey = new Map();
|
|
77
|
+
routeId;
|
|
78
|
+
constructor(compiler, scopeId, importEntries) {
|
|
79
|
+
this.compiler = compiler;
|
|
80
|
+
this.routeId = scopeId;
|
|
81
|
+
for (const entry of importEntries) {
|
|
82
|
+
const parsed = parseImportStatement(entry.line);
|
|
83
|
+
if (!parsed.moduleSpecifier)
|
|
84
|
+
continue;
|
|
85
|
+
const isClient = parsed.moduleSpecifier.startsWith('$client/');
|
|
86
|
+
const isShared = parsed.moduleSpecifier.startsWith('$shared/');
|
|
87
|
+
if (!isClient && !isShared)
|
|
88
|
+
continue;
|
|
89
|
+
for (const binding of parsed.bindings) {
|
|
90
|
+
this.bindingMap.set(binding.local, {
|
|
91
|
+
importLine: entry.line,
|
|
92
|
+
importerDir: entry.importerDir,
|
|
93
|
+
localName: binding.local,
|
|
94
|
+
moduleSpecifier: parsed.moduleSpecifier,
|
|
95
|
+
});
|
|
96
|
+
if (isClient)
|
|
97
|
+
this.clientOnlyBindings.add(binding.local);
|
|
98
|
+
}
|
|
99
|
+
if (parsed.namespaceImport) {
|
|
100
|
+
this.bindingMap.set(parsed.namespaceImport, {
|
|
101
|
+
importLine: entry.line,
|
|
102
|
+
importerDir: entry.importerDir,
|
|
103
|
+
localName: parsed.namespaceImport,
|
|
104
|
+
moduleSpecifier: parsed.moduleSpecifier,
|
|
105
|
+
});
|
|
106
|
+
if (isClient)
|
|
107
|
+
this.clientOnlyBindings.add(parsed.namespaceImport);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
hasBindings() {
|
|
112
|
+
return this.bindingMap.size > 0;
|
|
113
|
+
}
|
|
114
|
+
hasBindingReference(expression) {
|
|
115
|
+
const refs = collectReferencedIdentifiers(expression);
|
|
116
|
+
for (const ref of refs) {
|
|
117
|
+
if (this.bindingMap.has(ref))
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
registerEventHandler(_eventName, expression) {
|
|
123
|
+
const parsed = this.parseClientExpression(expression);
|
|
124
|
+
if (!parsed)
|
|
125
|
+
return null;
|
|
126
|
+
const binding = this.bindingMap.get(parsed.rootBinding);
|
|
127
|
+
if (!binding)
|
|
128
|
+
return null;
|
|
129
|
+
if (parsed.argsExpr) {
|
|
130
|
+
const argRefs = collectReferencedIdentifiers(parsed.argsExpr);
|
|
131
|
+
const leakedClientRefs = Array.from(argRefs).filter((ref) => this.clientOnlyBindings.has(ref));
|
|
132
|
+
if (leakedClientRefs.length > 0) {
|
|
133
|
+
throw new Error(`[kuratchi compiler] Client event arguments cannot depend on $client bindings: ${leakedClientRefs.join(', ')}.\n` +
|
|
134
|
+
`Only server/shared values can be serialized into event handler arguments.`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const key = `${parsed.calleeExpr}::${parsed.argsExpr || ''}`;
|
|
138
|
+
let existing = this.handlerByKey.get(key);
|
|
139
|
+
if (!existing) {
|
|
140
|
+
existing = {
|
|
141
|
+
id: `h${this.handlerByKey.size}`,
|
|
142
|
+
calleeExpr: parsed.calleeExpr,
|
|
143
|
+
rootBinding: parsed.rootBinding,
|
|
144
|
+
};
|
|
145
|
+
this.handlerByKey.set(key, existing);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
routeId: this.routeId,
|
|
149
|
+
handlerId: existing.id,
|
|
150
|
+
argsExpr: parsed.argsExpr,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
buildEntryAsset() {
|
|
154
|
+
if (this.handlerByKey.size === 0)
|
|
155
|
+
return null;
|
|
156
|
+
const usedImportLines = new Map();
|
|
157
|
+
for (const record of this.handlerByKey.values()) {
|
|
158
|
+
const binding = this.bindingMap.get(record.rootBinding);
|
|
159
|
+
if (!binding)
|
|
160
|
+
continue;
|
|
161
|
+
if (!usedImportLines.has(binding.importLine)) {
|
|
162
|
+
usedImportLines.set(binding.importLine, {
|
|
163
|
+
line: binding.importLine,
|
|
164
|
+
importerDir: binding.importerDir,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const assetName = `__kuratchi/client/routes/${this.routeId}.js`;
|
|
169
|
+
const importLines = [];
|
|
170
|
+
for (const entry of usedImportLines.values()) {
|
|
171
|
+
const parsed = parseImportStatement(entry.line);
|
|
172
|
+
if (!parsed.moduleSpecifier)
|
|
173
|
+
continue;
|
|
174
|
+
const targetAbs = resolveClientImportTarget(this.compiler.srcDir, path.join(entry.importerDir, '__route__.ts'), parsed.moduleSpecifier);
|
|
175
|
+
const targetAssetName = this.compiler.transformClientModule(targetAbs);
|
|
176
|
+
const relSpecifier = toRelativeSpecifier(assetName, targetAssetName);
|
|
177
|
+
importLines.push(entry.line.replace(parsed.moduleSpecifier, relSpecifier));
|
|
178
|
+
}
|
|
179
|
+
const registrationEntries = Array.from(this.handlerByKey.values()).map((record) => {
|
|
180
|
+
return `${JSON.stringify(record.id)}: (args, event, element) => ${record.calleeExpr}(...args, event, element)`;
|
|
181
|
+
});
|
|
182
|
+
const source = transpileTypeScript([
|
|
183
|
+
...importLines,
|
|
184
|
+
`window.__kuratchiClient?.register(${JSON.stringify(this.routeId)}, {`,
|
|
185
|
+
registrationEntries.map((entry) => ` ${entry},`).join('\n'),
|
|
186
|
+
`});`,
|
|
187
|
+
].join('\n'), `client-route:${this.routeId}.ts`);
|
|
188
|
+
const asset = buildAsset(assetName, source);
|
|
189
|
+
this.compiler.registerAsset(asset);
|
|
190
|
+
return { assetName, asset };
|
|
191
|
+
}
|
|
192
|
+
parseClientExpression(expression) {
|
|
193
|
+
const trimmed = expression.trim();
|
|
194
|
+
if (!trimmed)
|
|
195
|
+
return null;
|
|
196
|
+
const callMatch = trimmed.match(/^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\(([\s\S]*)\)$/);
|
|
197
|
+
if (callMatch) {
|
|
198
|
+
const calleeExpr = callMatch[1];
|
|
199
|
+
const rootBinding = calleeExpr.split('.')[0];
|
|
200
|
+
const argsExpr = (callMatch[2] || '').trim();
|
|
201
|
+
return { calleeExpr, rootBinding, argsExpr: argsExpr || null };
|
|
202
|
+
}
|
|
203
|
+
const refMatch = trimmed.match(/^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)$/);
|
|
204
|
+
if (!refMatch)
|
|
205
|
+
return null;
|
|
206
|
+
const calleeExpr = refMatch[1];
|
|
207
|
+
const rootBinding = calleeExpr.split('.')[0];
|
|
208
|
+
return { calleeExpr, rootBinding, argsExpr: null };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
class CompilerBackedClientModuleCompiler {
|
|
212
|
+
projectDir;
|
|
213
|
+
srcDir;
|
|
214
|
+
compiledAssets = new Map();
|
|
215
|
+
transformedModules = new Map();
|
|
216
|
+
constructor(projectDir, srcDir) {
|
|
217
|
+
this.projectDir = projectDir;
|
|
218
|
+
this.srcDir = srcDir;
|
|
219
|
+
}
|
|
220
|
+
createRegistry(scopeId, importEntries) {
|
|
221
|
+
return new CompilerBackedClientRouteRegistry(this, scopeId, importEntries);
|
|
222
|
+
}
|
|
223
|
+
createRouteRegistry(routeIndex, importEntries) {
|
|
224
|
+
return this.createRegistry(`route_${routeIndex}`, importEntries);
|
|
225
|
+
}
|
|
226
|
+
getCompiledAssets() {
|
|
227
|
+
return Array.from(this.compiledAssets.values());
|
|
228
|
+
}
|
|
229
|
+
registerAsset(asset) {
|
|
230
|
+
this.compiledAssets.set(asset.name, asset);
|
|
231
|
+
}
|
|
232
|
+
transformClientModule(entryAbsPath) {
|
|
233
|
+
const resolved = resolveExistingModuleFile(entryAbsPath) ?? entryAbsPath;
|
|
234
|
+
const normalized = resolved.replace(/\\/g, '/');
|
|
235
|
+
const cached = this.transformedModules.get(normalized);
|
|
236
|
+
if (cached)
|
|
237
|
+
return cached;
|
|
238
|
+
const relFromSrc = path.relative(this.srcDir, resolved).replace(/\\/g, '/');
|
|
239
|
+
const assetName = `__kuratchi/client/modules/${relFromSrc.replace(/\.(ts|js|mjs|cjs)$/i, '.js')}`;
|
|
240
|
+
this.transformedModules.set(normalized, assetName);
|
|
241
|
+
if (!fs.existsSync(resolved)) {
|
|
242
|
+
throw new Error(`[kuratchi compiler] Browser module not found: ${resolved}`);
|
|
243
|
+
}
|
|
244
|
+
const source = fs.readFileSync(resolved, 'utf-8');
|
|
245
|
+
let rewritten = transpileTypeScript(source, `client-module:${relFromSrc}`);
|
|
246
|
+
rewritten = rewriteImportSpecifiers(rewritten, (spec) => {
|
|
247
|
+
const targetAbs = resolveClientImportTarget(this.srcDir, resolved, spec);
|
|
248
|
+
const targetAssetName = this.transformClientModule(targetAbs);
|
|
249
|
+
return toRelativeSpecifier(assetName, targetAssetName);
|
|
250
|
+
});
|
|
251
|
+
this.registerAsset(buildAsset(assetName, rewritten));
|
|
252
|
+
return assetName;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
export function createClientModuleCompiler(opts) {
|
|
256
|
+
return new CompilerBackedClientModuleCompiler(opts.projectDir, opts.srcDir);
|
|
257
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface OrmDatabaseEntry {
|
|
2
|
+
binding: string;
|
|
3
|
+
schemaImportPath: string;
|
|
4
|
+
schemaExportName: string;
|
|
5
|
+
skipMigrations: boolean;
|
|
6
|
+
type: 'd1' | 'do';
|
|
7
|
+
}
|
|
8
|
+
export interface AuthConfigEntry {
|
|
9
|
+
cookieName: string;
|
|
10
|
+
secretEnvKey: string;
|
|
11
|
+
sessionEnabled: boolean;
|
|
12
|
+
hasCredentials: boolean;
|
|
13
|
+
hasActivity: boolean;
|
|
14
|
+
hasRoles: boolean;
|
|
15
|
+
hasOAuth: boolean;
|
|
16
|
+
hasGuards: boolean;
|
|
17
|
+
hasRateLimit: boolean;
|
|
18
|
+
hasTurnstile: boolean;
|
|
19
|
+
hasOrganization: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface DoConfigEntry {
|
|
22
|
+
binding: string;
|
|
23
|
+
className: string;
|
|
24
|
+
stubId?: string;
|
|
25
|
+
files?: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface WorkerClassConfigEntry {
|
|
28
|
+
binding: string;
|
|
29
|
+
className: string;
|
|
30
|
+
file: string;
|
|
31
|
+
exportKind: 'named' | 'default';
|
|
32
|
+
}
|
|
33
|
+
export interface ConventionClassEntry {
|
|
34
|
+
className: string;
|
|
35
|
+
file: string;
|
|
36
|
+
exportKind: 'named' | 'default';
|
|
37
|
+
}
|
|
38
|
+
export interface DoClassMethodEntry {
|
|
39
|
+
name: string;
|
|
40
|
+
visibility: 'public' | 'private' | 'protected';
|
|
41
|
+
isStatic: boolean;
|
|
42
|
+
isAsync: boolean;
|
|
43
|
+
hasWorkerContextCalls: boolean;
|
|
44
|
+
callsThisMethods: string[];
|
|
45
|
+
}
|
|
46
|
+
export interface DoHandlerEntry {
|
|
47
|
+
fileName: string;
|
|
48
|
+
absPath: string;
|
|
49
|
+
binding: string;
|
|
50
|
+
mode: 'class' | 'function';
|
|
51
|
+
className?: string;
|
|
52
|
+
classMethods: DoClassMethodEntry[];
|
|
53
|
+
exportedFunctions: string[];
|
|
54
|
+
}
|
|
55
|
+
export declare function toSafeIdentifier(input: string): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ComponentCompiler {
|
|
2
|
+
ensureCompiled(fileName: string): string | null;
|
|
3
|
+
collectComponentMap(componentImports: Record<string, string>): Map<string, string>;
|
|
4
|
+
getActionPropNames(fileName: string): Set<string>;
|
|
5
|
+
collectStyles(componentNames: Map<string, string>): string[];
|
|
6
|
+
resolveActionProps(template: string, componentNames: Map<string, string>, shouldInclude?: (fnName: string) => boolean): Set<string>;
|
|
7
|
+
getCompiledComponents(): string[];
|
|
8
|
+
}
|
|
9
|
+
interface CreateComponentCompilerOptions {
|
|
10
|
+
projectDir: string;
|
|
11
|
+
srcDir: string;
|
|
12
|
+
isDev: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function createComponentCompiler(options: CreateComponentCompilerOptions): ComponentCompiler;
|
|
15
|
+
export {};
|