@kuratchi/js 0.0.15 → 0.0.17
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 +160 -1
- package/dist/cli.js +78 -45
- 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 +73 -0
- package/dist/compiler/compiler-shared.js +4 -0
- package/dist/compiler/component-pipeline.d.ts +15 -0
- package/dist/compiler/component-pipeline.js +158 -0
- package/dist/compiler/config-reading.d.ts +12 -0
- package/dist/compiler/config-reading.js +380 -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 +140 -0
- package/dist/compiler/index.d.ts +7 -7
- package/dist/compiler/index.js +181 -3321
- 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 +436 -55
- package/dist/compiler/root-layout-pipeline.d.ts +10 -0
- package/dist/compiler/root-layout-pipeline.js +532 -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 +291 -0
- package/dist/compiler/route-state-pipeline.d.ts +26 -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 +91 -0
- package/dist/compiler/routes-module-types.d.ts +45 -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 +337 -71
- 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/context.d.ts +4 -0
- package/dist/runtime/context.js +40 -2
- package/dist/runtime/do.js +21 -6
- package/dist/runtime/generated-worker.d.ts +55 -0
- package/dist/runtime/generated-worker.js +543 -0
- package/dist/runtime/index.d.ts +4 -1
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/router.d.ts +6 -1
- package/dist/runtime/router.js +125 -31
- package/dist/runtime/security.d.ts +101 -0
- package/dist/runtime/security.js +298 -0
- package/dist/runtime/types.d.ts +29 -2
- package/package.json +5 -1
package/dist/compiler/index.js
CHANGED
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Compiler
|
|
2
|
+
* Compiler ?" scans a project's routes/ directory, parses .html files,
|
|
3
3
|
* and generates a single Worker entry point.
|
|
4
4
|
*/
|
|
5
|
-
import { parseFile
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { parseFile } from './parser.js';
|
|
6
|
+
import { compileAssets } from './asset-pipeline.js';
|
|
7
|
+
import { compileApiRoute } from './api-route-pipeline.js';
|
|
8
|
+
import { createClientModuleCompiler } from './client-module-pipeline.js';
|
|
9
|
+
import { createComponentCompiler } from './component-pipeline.js';
|
|
10
|
+
import { readAssetsPrefix, readAuthConfig, readDoConfig, readOrmConfig, readSecurityConfig, readUiConfigValues, readUiTheme, } from './config-reading.js';
|
|
11
|
+
import { discoverContainerFiles, discoverConventionClassFiles, discoverWorkflowFiles, } from './convention-discovery.js';
|
|
12
|
+
import { discoverDurableObjects, generateHandlerProxy } from './durable-object-pipeline.js';
|
|
13
|
+
import { compileErrorPages } from './error-page-pipeline.js';
|
|
14
|
+
import { compileLayoutPlan, finalizeLayoutPlan } from './layout-pipeline.js';
|
|
15
|
+
import { compilePageRoute } from './page-route-pipeline.js';
|
|
16
|
+
import { discoverRoutes as discoverRoutesPipeline } from './route-discovery.js';
|
|
17
|
+
import { prepareRootLayoutSource } from './root-layout-pipeline.js';
|
|
18
|
+
import { generateRoutesModule as generateRoutesModulePipeline } from './routes-module-pipeline.js';
|
|
19
|
+
import { assembleRouteState } from './route-state-pipeline.js';
|
|
20
|
+
import { createServerModuleCompiler } from './server-module-pipeline.js';
|
|
21
|
+
import { buildWorkerEntrypointSource, resolveRuntimeImportPath as resolveRuntimeImportPathPipeline, } from './worker-output-pipeline.js';
|
|
22
|
+
import { syncWranglerConfig as syncWranglerConfigPipeline } from './wrangler-sync.js';
|
|
8
23
|
import { filePathToPattern } from '../runtime/router.js';
|
|
9
24
|
import * as fs from 'node:fs';
|
|
25
|
+
import * as fsp from 'node:fs/promises';
|
|
10
26
|
import * as path from 'node:path';
|
|
11
|
-
import * as crypto from 'node:crypto';
|
|
12
27
|
export { parseFile } from './parser.js';
|
|
13
28
|
export { compileTemplate, generateRenderFunction } from './template.js';
|
|
14
29
|
const FRAMEWORK_PACKAGE_NAME = getFrameworkPackageName();
|
|
15
30
|
const RUNTIME_CONTEXT_IMPORT = `${FRAMEWORK_PACKAGE_NAME}/runtime/context.js`;
|
|
16
31
|
const RUNTIME_DO_IMPORT = `${FRAMEWORK_PACKAGE_NAME}/runtime/do.js`;
|
|
32
|
+
const RUNTIME_WORKER_IMPORT = `${FRAMEWORK_PACKAGE_NAME}/runtime/generated-worker.js`;
|
|
17
33
|
function getFrameworkPackageName() {
|
|
18
34
|
try {
|
|
19
35
|
const raw = fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf-8');
|
|
@@ -24,79 +40,41 @@ function getFrameworkPackageName() {
|
|
|
24
40
|
return '@kuratchi/js';
|
|
25
41
|
}
|
|
26
42
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const aliasRegex = new RegExp(`(?<!\\.)\\b${alias}\\b`, 'g');
|
|
52
|
-
out = out.replace(aliasRegex, '__env');
|
|
53
|
-
}
|
|
54
|
-
return out;
|
|
55
|
-
}
|
|
56
|
-
function buildDevAliasDeclarations(aliases, isDev) {
|
|
57
|
-
if (!aliases || aliases.length === 0)
|
|
58
|
-
return '';
|
|
59
|
-
return aliases.map((alias) => `const ${alias} = ${isDev ? 'true' : 'false'};`).join('\n');
|
|
60
|
-
}
|
|
61
|
-
function parseNamedImportBindings(line) {
|
|
62
|
-
const namesMatch = line.match(/import\s*\{([^}]+)\}/);
|
|
63
|
-
if (!namesMatch)
|
|
64
|
-
return [];
|
|
65
|
-
return namesMatch[1]
|
|
66
|
-
.split(',')
|
|
67
|
-
.map(n => n.trim())
|
|
68
|
-
.filter(Boolean)
|
|
69
|
-
.map(n => {
|
|
70
|
-
const parts = n.split(/\s+as\s+/).map(p => p.trim()).filter(Boolean);
|
|
71
|
-
return {
|
|
72
|
-
imported: parts[0] || '',
|
|
73
|
-
local: parts[1] || parts[0] || '',
|
|
74
|
-
};
|
|
75
|
-
})
|
|
76
|
-
.filter((binding) => !!binding.imported && !!binding.local);
|
|
77
|
-
}
|
|
78
|
-
function filterClientImportsForServer(imports, neededFns) {
|
|
79
|
-
const selected = [];
|
|
80
|
-
for (const line of imports) {
|
|
81
|
-
const bindings = parseNamedImportBindings(line);
|
|
82
|
-
if (bindings.length === 0)
|
|
83
|
-
continue;
|
|
84
|
-
if (bindings.some((binding) => neededFns.has(binding.local))) {
|
|
85
|
-
selected.push(line);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return selected;
|
|
43
|
+
/**
|
|
44
|
+
* Pre-read all route files and their layouts in parallel for better I/O performance.
|
|
45
|
+
* Returns a Map from file path to content.
|
|
46
|
+
*/
|
|
47
|
+
async function preReadFiles(routesDir, routeFiles) {
|
|
48
|
+
const filesToRead = new Set();
|
|
49
|
+
// Collect all unique files to read
|
|
50
|
+
for (const rf of routeFiles) {
|
|
51
|
+
filesToRead.add(path.join(routesDir, rf.file));
|
|
52
|
+
for (const layout of rf.layouts) {
|
|
53
|
+
filesToRead.add(path.join(routesDir, layout));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Also include root layout if it exists
|
|
57
|
+
const rootLayout = path.join(routesDir, 'layout.html');
|
|
58
|
+
if (fs.existsSync(rootLayout)) {
|
|
59
|
+
filesToRead.add(rootLayout);
|
|
60
|
+
}
|
|
61
|
+
// Read all files in parallel
|
|
62
|
+
const entries = await Promise.all(Array.from(filesToRead).map(async (filePath) => {
|
|
63
|
+
const content = await fsp.readFile(filePath, 'utf-8');
|
|
64
|
+
return [filePath, content];
|
|
65
|
+
}));
|
|
66
|
+
return new Map(entries);
|
|
89
67
|
}
|
|
90
68
|
/**
|
|
91
|
-
* Compile a project's src/routes/ into .kuratchi/routes.
|
|
69
|
+
* Compile a project's src/routes/ into .kuratchi/routes.ts
|
|
92
70
|
*
|
|
93
|
-
* The generated module exports { app }
|
|
71
|
+
* The generated module exports { app } — an object with a fetch() method
|
|
94
72
|
* that handles routing, load functions, form actions, and rendering.
|
|
95
|
-
* Returns the path to .kuratchi/worker.
|
|
96
|
-
* re-exports everything from routes.
|
|
73
|
+
* Returns the path to .kuratchi/worker.ts — the stable wrangler entry point that
|
|
74
|
+
* re-exports everything from routes.ts (default fetch handler + named DO class exports).
|
|
97
75
|
* No src/index.ts is needed in user projects.
|
|
98
76
|
*/
|
|
99
|
-
export function compile(options) {
|
|
77
|
+
export async function compile(options) {
|
|
100
78
|
const { projectDir } = options;
|
|
101
79
|
const srcDir = path.join(projectDir, 'src');
|
|
102
80
|
const routesDir = path.join(srcDir, 'routes');
|
|
@@ -104,649 +82,54 @@ export function compile(options) {
|
|
|
104
82
|
throw new Error(`Routes directory not found: ${routesDir}`);
|
|
105
83
|
}
|
|
106
84
|
// Discover all .html route files
|
|
107
|
-
const routeFiles =
|
|
108
|
-
//
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
let funcName;
|
|
122
|
-
// Package component: "@kuratchi/ui:badge" �' resolve from package
|
|
123
|
-
const pkgMatch = fileName.match(/^(@[^:]+):(.+)$/);
|
|
124
|
-
if (pkgMatch) {
|
|
125
|
-
const pkgName = pkgMatch[1]; // e.g. "@kuratchi/ui"
|
|
126
|
-
const componentFile = pkgMatch[2]; // e.g. "badge"
|
|
127
|
-
funcName = '__c_' + componentFile.replace(/[\/\-]/g, '_');
|
|
128
|
-
// Resolve the package's src/lib/ directory
|
|
129
|
-
filePath = resolvePackageComponent(projectDir, pkgName, componentFile);
|
|
130
|
-
if (!filePath || !fs.existsSync(filePath))
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
// Local component: resolve from src/lib/
|
|
135
|
-
funcName = '__c_' + fileName.replace(/[\/\-]/g, '_');
|
|
136
|
-
filePath = path.join(libDir, fileName + '.html');
|
|
137
|
-
if (!fs.existsSync(filePath))
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
// Generate a short scope hash for scoped CSS
|
|
141
|
-
const scopeHash = 'dz-' + crypto.createHash('md5').update(fileName).digest('hex').slice(0, 6);
|
|
142
|
-
const rawSource = fs.readFileSync(filePath, 'utf-8');
|
|
143
|
-
// Use parseFile to properly split the <script> block from the template, and to
|
|
144
|
-
// separate component imports (import X from '@kuratchi/ui/x.html') from regular code.
|
|
145
|
-
// This prevents import lines from being inlined verbatim in the compiled function body.
|
|
146
|
-
const compParsed = parseFile(rawSource, { kind: 'component', filePath });
|
|
147
|
-
// propsCode = script body with all import lines stripped out
|
|
148
|
-
const propsCode = compParsed.script
|
|
149
|
-
? stripTopLevelImports(compParsed.script)
|
|
150
|
-
: '';
|
|
151
|
-
const devDecls = buildDevAliasDeclarations(compParsed.devAliases, !!options.isDev);
|
|
152
|
-
const effectivePropsCode = [devDecls, propsCode].filter(Boolean).join('\n');
|
|
153
|
-
const transpiledPropsCode = propsCode
|
|
154
|
-
? transpileTypeScript(effectivePropsCode, `component-script:${fileName}.ts`)
|
|
155
|
-
: devDecls
|
|
156
|
-
? transpileTypeScript(devDecls, `component-script:${fileName}.ts`)
|
|
157
|
-
: '';
|
|
158
|
-
// template source (parseFile already removes the <script> block)
|
|
159
|
-
let source = compParsed.template;
|
|
160
|
-
// Extract optional <style> block �" CSS is scoped and injected once per route at compile time
|
|
161
|
-
let styleBlock = '';
|
|
162
|
-
const styleMatch = source.match(/<style[\s>][\s\S]*?<\/style>/i);
|
|
163
|
-
if (styleMatch) {
|
|
164
|
-
styleBlock = styleMatch[0];
|
|
165
|
-
source = source.replace(styleMatch[0], '').trim();
|
|
166
|
-
}
|
|
167
|
-
// Scope the CSS: prefix every selector with .dz-{hash}
|
|
168
|
-
let scopedStyle = '';
|
|
169
|
-
if (styleBlock) {
|
|
170
|
-
// Extract the CSS content between <style> and </style>
|
|
171
|
-
const cssContent = styleBlock.replace(/<style[^>]*>/i, '').replace(/<\/style>/i, '').trim();
|
|
172
|
-
// Prefix each rule's selector(s) with the scope class
|
|
173
|
-
const scopedCSS = cssContent.replace(/([^{}]+)\{/g, (match, selectors) => {
|
|
174
|
-
const scoped = selectors
|
|
175
|
-
.split(',')
|
|
176
|
-
.map((s) => `.${scopeHash} ${s.trim()}`)
|
|
177
|
-
.join(', ');
|
|
178
|
-
return scoped + ' {';
|
|
179
|
-
});
|
|
180
|
-
scopedStyle = `<style>${scopedCSS}</style>`;
|
|
181
|
-
}
|
|
182
|
-
// Store escaped scoped CSS separately for compile-time injection into routes
|
|
183
|
-
const escapedStyle = scopedStyle
|
|
184
|
-
? scopedStyle.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${')
|
|
185
|
-
: '';
|
|
186
|
-
componentStyleCache.set(fileName, escapedStyle);
|
|
187
|
-
// Replace <slot></slot> and <slot /> with children output
|
|
188
|
-
source = source.replace(/<slot\s*><\/slot>/g, '{@raw props.children || ""}');
|
|
189
|
-
source = source.replace(/<slot\s*\/>/g, '{@raw props.children || ""}');
|
|
190
|
-
// Build a sub-component map from the component's own component imports so that
|
|
191
|
-
// <Alert>, <Badge>, <Dialog>, etc. get expanded instead of emitted as raw tags.
|
|
192
|
-
const subComponentNames = new Map();
|
|
193
|
-
for (const [subPascal, subFileName] of Object.entries(compParsed.componentImports)) {
|
|
194
|
-
compileComponent(subFileName); // compile on first use (cached)
|
|
195
|
-
subComponentNames.set(subPascal, subFileName);
|
|
196
|
-
// Collect sub-component styles so they're available when the route gathers styles
|
|
197
|
-
const subStyle = componentStyleCache.get(subFileName);
|
|
198
|
-
if (subStyle) {
|
|
199
|
-
const existing = componentStyleCache.get(fileName) || '';
|
|
200
|
-
if (!existing.includes(subStyle)) {
|
|
201
|
-
componentStyleCache.set(fileName, existing + subStyle);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
// Scan the component template for action={propName} uses.
|
|
206
|
-
// These prop names are "action props" �" when the route passes actionProp={routeFn},
|
|
207
|
-
// the compiler knows to add routeFn to the route's actionFunctions so it ends up
|
|
208
|
-
// in the route's actions map and can be dispatched at runtime.
|
|
209
|
-
const actionPropNames = new Set();
|
|
210
|
-
for (const match of source.matchAll(/\baction=\{([A-Za-z_$][\w$]*)\}/g)) {
|
|
211
|
-
actionPropNames.add(match[1]);
|
|
212
|
-
}
|
|
213
|
-
componentActionCache.set(fileName, actionPropNames);
|
|
214
|
-
const body = compileTemplate(source, subComponentNames, undefined, undefined);
|
|
215
|
-
// Wrap component output in a scoped div
|
|
216
|
-
const scopeOpen = `__html += '<div class="${scopeHash}">';`;
|
|
217
|
-
const scopeClose = `__html += '</div>';`;
|
|
218
|
-
// Insert scope open after 'let __html = "";' (first line of body) and scope close at end
|
|
219
|
-
const bodyLines = body.split('\n');
|
|
220
|
-
const scopedBody = [bodyLines[0], scopeOpen, ...bodyLines.slice(1), scopeClose].join('\n');
|
|
221
|
-
const fnBody = transpiledPropsCode ? `${transpiledPropsCode}\n ${scopedBody}` : scopedBody;
|
|
222
|
-
const compiled = `function ${funcName}(props, __esc) {\n ${fnBody}\n return __html;\n}`;
|
|
223
|
-
compiledComponentCache.set(fileName, compiled);
|
|
224
|
-
return compiled;
|
|
225
|
-
}
|
|
226
|
-
// App layout: src/routes/layout.html (convention �" wraps all routes automatically)
|
|
85
|
+
const routeFiles = discoverRoutesPipeline(routesDir);
|
|
86
|
+
// Pre-read all files in parallel for better I/O performance
|
|
87
|
+
const fileContents = await preReadFiles(routesDir, routeFiles);
|
|
88
|
+
const componentCompiler = createComponentCompiler({
|
|
89
|
+
projectDir,
|
|
90
|
+
srcDir,
|
|
91
|
+
isDev: !!options.isDev,
|
|
92
|
+
});
|
|
93
|
+
const clientModuleCompiler = createClientModuleCompiler({
|
|
94
|
+
projectDir,
|
|
95
|
+
srcDir,
|
|
96
|
+
});
|
|
97
|
+
const assetsPrefix = readAssetsPrefix(projectDir);
|
|
98
|
+
// App layout: src/routes/layout.html (convention ?" wraps all routes automatically)
|
|
227
99
|
const layoutFile = path.join(routesDir, 'layout.html');
|
|
228
100
|
let compiledLayout = null;
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
// Inject UI theme CSS if configured in kuratchi.config.ts
|
|
101
|
+
let layoutPlan = null;
|
|
102
|
+
if (fileContents.has(layoutFile)) {
|
|
103
|
+
const layoutImportSource = fileContents.get(layoutFile);
|
|
233
104
|
const themeCSS = readUiTheme(projectDir);
|
|
234
105
|
const uiConfigValues = readUiConfigValues(projectDir);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// - server actions bound via onX={serverAction(...)} -> [data-action][data-action-event]
|
|
253
|
-
// - declarative confirm="..."
|
|
254
|
-
// - declarative checkbox groups: data-select-all / data-select-item
|
|
255
|
-
const bridgeSource = `(function(){
|
|
256
|
-
function by(sel, root){ return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
|
|
257
|
-
var __refreshSeq = Object.create(null);
|
|
258
|
-
function syncGroup(group){
|
|
259
|
-
var items = by('[data-select-item]').filter(function(el){ return el.getAttribute('data-select-item') === group; });
|
|
260
|
-
var masters = by('[data-select-all]').filter(function(el){ return el.getAttribute('data-select-all') === group; });
|
|
261
|
-
if(!items.length || !masters.length) return;
|
|
262
|
-
var all = items.every(function(i){ return !!i.checked; });
|
|
263
|
-
var any = items.some(function(i){ return !!i.checked; });
|
|
264
|
-
masters.forEach(function(m){ m.checked = all; m.indeterminate = any && !all; });
|
|
265
|
-
}
|
|
266
|
-
function inferQueryKey(getName, argsRaw){
|
|
267
|
-
if(!getName) return '';
|
|
268
|
-
return 'query:' + String(getName) + '|' + (argsRaw || '[]');
|
|
269
|
-
}
|
|
270
|
-
function blockKey(el){
|
|
271
|
-
if(!el || !el.getAttribute) return '';
|
|
272
|
-
var explicit = el.getAttribute('data-key');
|
|
273
|
-
if(explicit) return 'key:' + explicit;
|
|
274
|
-
var inferred = inferQueryKey(el.getAttribute('data-get'), el.getAttribute('data-get-args'));
|
|
275
|
-
if(inferred) return inferred;
|
|
276
|
-
var asName = el.getAttribute('data-as');
|
|
277
|
-
if(asName) return 'as:' + asName;
|
|
278
|
-
return '';
|
|
279
|
-
}
|
|
280
|
-
function escHtml(v){
|
|
281
|
-
return String(v || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
282
|
-
}
|
|
283
|
-
function setBlocksLoading(blocks){
|
|
284
|
-
blocks.forEach(function(el){
|
|
285
|
-
el.setAttribute('aria-busy','true');
|
|
286
|
-
el.setAttribute('data-kuratchi-loading','1');
|
|
287
|
-
var text = el.getAttribute('data-loading-text');
|
|
288
|
-
if(text && !el.hasAttribute('data-as')){ el.innerHTML = '<p>' + escHtml(text) + '</p>'; return; }
|
|
289
|
-
el.style.opacity = '0.6';
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
function clearBlocksLoading(blocks){
|
|
293
|
-
blocks.forEach(function(el){
|
|
294
|
-
el.removeAttribute('aria-busy');
|
|
295
|
-
el.removeAttribute('data-kuratchi-loading');
|
|
296
|
-
el.style.opacity = '';
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
function replaceBlocksWithKey(key){
|
|
300
|
-
if(!key || typeof DOMParser === 'undefined'){ location.reload(); return Promise.resolve(); }
|
|
301
|
-
var oldBlocks = by('[data-get]').filter(function(el){ return blockKey(el) === key; });
|
|
302
|
-
if(!oldBlocks.length){ location.reload(); return Promise.resolve(); }
|
|
303
|
-
var first = oldBlocks[0];
|
|
304
|
-
var qFn = first ? (first.getAttribute('data-get') || '') : '';
|
|
305
|
-
var qArgs = first ? String(first.getAttribute('data-get-args') || '[]') : '[]';
|
|
306
|
-
var seq = (__refreshSeq[key] || 0) + 1;
|
|
307
|
-
__refreshSeq[key] = seq;
|
|
308
|
-
setBlocksLoading(oldBlocks);
|
|
309
|
-
var headers = { 'x-kuratchi-refresh': '1' };
|
|
310
|
-
if(qFn){ headers['x-kuratchi-query-fn'] = String(qFn); headers['x-kuratchi-query-args'] = qArgs; }
|
|
311
|
-
return fetch(location.pathname + location.search, { headers: headers })
|
|
312
|
-
.then(function(r){ if(!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
|
|
313
|
-
.then(function(html){
|
|
314
|
-
if(__refreshSeq[key] !== seq) return;
|
|
315
|
-
var doc = new DOMParser().parseFromString(html, 'text/html');
|
|
316
|
-
var newBlocks = by('[data-get]', doc).filter(function(el){ return blockKey(el) === key; });
|
|
317
|
-
if(!oldBlocks.length || !newBlocks.length){ location.reload(); return; }
|
|
318
|
-
for(var i=0;i<oldBlocks.length;i++){ if(newBlocks[i]) oldBlocks[i].outerHTML = newBlocks[i].outerHTML; }
|
|
319
|
-
by('[data-select-all]').forEach(function(m){ var g=m.getAttribute('data-select-all'); if(g) syncGroup(g); });
|
|
320
|
-
})
|
|
321
|
-
.catch(function(){
|
|
322
|
-
if(__refreshSeq[key] === seq) clearBlocksLoading(oldBlocks);
|
|
323
|
-
location.reload();
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
function replaceBlocksByDescriptor(fnName, argsRaw){
|
|
327
|
-
if(!fnName || typeof DOMParser === 'undefined'){ location.reload(); return Promise.resolve(); }
|
|
328
|
-
var normalizedArgs = String(argsRaw || '[]');
|
|
329
|
-
var oldBlocks = by('[data-get]').filter(function(el){
|
|
330
|
-
return (el.getAttribute('data-get') || '') === String(fnName) &&
|
|
331
|
-
String(el.getAttribute('data-get-args') || '[]') === normalizedArgs;
|
|
332
|
-
});
|
|
333
|
-
if(!oldBlocks.length){ location.reload(); return Promise.resolve(); }
|
|
334
|
-
var key = 'fn:' + String(fnName) + '|' + normalizedArgs;
|
|
335
|
-
var seq = (__refreshSeq[key] || 0) + 1;
|
|
336
|
-
__refreshSeq[key] = seq;
|
|
337
|
-
setBlocksLoading(oldBlocks);
|
|
338
|
-
return fetch(location.pathname + location.search, {
|
|
339
|
-
headers: {
|
|
340
|
-
'x-kuratchi-refresh': '1',
|
|
341
|
-
'x-kuratchi-query-fn': String(fnName),
|
|
342
|
-
'x-kuratchi-query-args': normalizedArgs,
|
|
343
|
-
}
|
|
344
|
-
})
|
|
345
|
-
.then(function(r){ if(!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
|
|
346
|
-
.then(function(html){
|
|
347
|
-
if(__refreshSeq[key] !== seq) return;
|
|
348
|
-
var doc = new DOMParser().parseFromString(html, 'text/html');
|
|
349
|
-
var newBlocks = by('[data-get]', doc).filter(function(el){
|
|
350
|
-
return (el.getAttribute('data-get') || '') === String(fnName) &&
|
|
351
|
-
String(el.getAttribute('data-get-args') || '[]') === normalizedArgs;
|
|
352
|
-
});
|
|
353
|
-
if(!newBlocks.length){ location.reload(); return; }
|
|
354
|
-
for(var i=0;i<oldBlocks.length;i++){ if(newBlocks[i]) oldBlocks[i].outerHTML = newBlocks[i].outerHTML; }
|
|
355
|
-
by('[data-select-all]').forEach(function(m){ var g=m.getAttribute('data-select-all'); if(g) syncGroup(g); });
|
|
356
|
-
})
|
|
357
|
-
.catch(function(){
|
|
358
|
-
if(__refreshSeq[key] === seq) clearBlocksLoading(oldBlocks);
|
|
359
|
-
location.reload();
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
function refreshByDescriptor(fnName, argsRaw){
|
|
363
|
-
if(!fnName) { location.reload(); return Promise.resolve(); }
|
|
364
|
-
return replaceBlocksByDescriptor(fnName, argsRaw || '[]');
|
|
365
|
-
}
|
|
366
|
-
function refreshNearest(el){
|
|
367
|
-
var host = el && el.closest ? el.closest('[data-get]') : null;
|
|
368
|
-
if(!host){ location.reload(); return Promise.resolve(); }
|
|
369
|
-
return replaceBlocksWithKey(blockKey(host));
|
|
370
|
-
}
|
|
371
|
-
function refreshTargets(raw){
|
|
372
|
-
if(!raw){ location.reload(); return Promise.resolve(); }
|
|
373
|
-
var keys = String(raw).split(',').map(function(v){ return v.trim(); }).filter(Boolean);
|
|
374
|
-
if(!keys.length){ location.reload(); return Promise.resolve(); }
|
|
375
|
-
return Promise.all(keys.map(function(k){ return replaceBlocksWithKey('key:' + k); })).then(function(){});
|
|
376
|
-
}
|
|
377
|
-
function act(e){
|
|
378
|
-
if(e.type === 'click'){
|
|
379
|
-
var g = e.target && e.target.closest ? e.target.closest('[data-get]') : null;
|
|
380
|
-
if(g && !g.hasAttribute('data-as') && !g.hasAttribute('data-action')){
|
|
381
|
-
var getUrl = g.getAttribute('data-get');
|
|
382
|
-
if(getUrl){
|
|
383
|
-
if(/^[a-z][a-z0-9+\-.]*:/i.test(getUrl) && !/^https?:/i.test(getUrl)) return;
|
|
384
|
-
e.preventDefault();
|
|
385
|
-
location.assign(getUrl);
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
var r = e.target && e.target.closest ? e.target.closest('[data-refresh]') : null;
|
|
390
|
-
if(r && !r.hasAttribute('data-action')){
|
|
391
|
-
e.preventDefault();
|
|
392
|
-
var rf = r.getAttribute('data-refresh');
|
|
393
|
-
var ra = r.getAttribute('data-refresh-args');
|
|
394
|
-
if(ra !== null){ refreshByDescriptor(rf, ra || '[]'); return; }
|
|
395
|
-
if(rf && rf.trim()){ refreshTargets(rf); return; }
|
|
396
|
-
refreshNearest(r);
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
var sel = '[data-action][data-action-event="' + e.type + '"]';
|
|
401
|
-
var b = e.target && e.target.closest ? e.target.closest(sel) : null;
|
|
402
|
-
if(!b) return;
|
|
403
|
-
e.preventDefault();
|
|
404
|
-
var fd = new FormData();
|
|
405
|
-
fd.append('_action', b.getAttribute('data-action') || '');
|
|
406
|
-
fd.append('_args', b.getAttribute('data-args') || '[]');
|
|
407
|
-
var m = b.getAttribute('data-action-method');
|
|
408
|
-
if(m) fd.append('_method', String(m).toUpperCase());
|
|
409
|
-
fetch(location.pathname, { method: 'POST', body: fd })
|
|
410
|
-
.then(function(r){
|
|
411
|
-
if(!r.ok){
|
|
412
|
-
return r.json().then(function(j){ throw new Error((j && j.error) || ('HTTP ' + r.status)); }).catch(function(){ throw new Error('HTTP ' + r.status); });
|
|
413
|
-
}
|
|
414
|
-
return r.json();
|
|
415
|
-
})
|
|
416
|
-
.then(function(j){
|
|
417
|
-
if(j && j.redirectTo){ location.assign(j.redirectTo); return; }
|
|
418
|
-
if(!b.hasAttribute('data-refresh')) return;
|
|
419
|
-
var refreshFn = b.getAttribute('data-refresh');
|
|
420
|
-
var refreshArgs = b.getAttribute('data-refresh-args');
|
|
421
|
-
if(refreshArgs !== null){ return refreshByDescriptor(refreshFn, refreshArgs || '[]'); }
|
|
422
|
-
if(refreshFn && refreshFn.trim()){ return refreshTargets(refreshFn); }
|
|
423
|
-
return refreshNearest(b);
|
|
424
|
-
})
|
|
425
|
-
.catch(function(err){ console.error('[kuratchi] client action error:', err); });
|
|
426
|
-
}
|
|
427
|
-
['click','change','input','focus','blur'].forEach(function(ev){ document.addEventListener(ev, act, true); });
|
|
428
|
-
function autoLoadQueries(){
|
|
429
|
-
var seen = Object.create(null);
|
|
430
|
-
by('[data-get][data-as]').forEach(function(el){
|
|
431
|
-
var fn = el.getAttribute('data-get');
|
|
432
|
-
if(!fn) return;
|
|
433
|
-
var args = String(el.getAttribute('data-get-args') || '[]');
|
|
434
|
-
var key = String(fn) + '|' + args;
|
|
435
|
-
if(seen[key]) return;
|
|
436
|
-
seen[key] = true;
|
|
437
|
-
replaceBlocksByDescriptor(fn, args);
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
if(document.readyState === 'loading'){
|
|
441
|
-
document.addEventListener('DOMContentLoaded', autoLoadQueries, { once: true });
|
|
442
|
-
} else {
|
|
443
|
-
autoLoadQueries();
|
|
444
|
-
}
|
|
445
|
-
document.addEventListener('click', function(e){
|
|
446
|
-
var b = e.target && e.target.closest ? e.target.closest('[command="fill-dialog"]') : null;
|
|
447
|
-
if(!b) return;
|
|
448
|
-
var targetId = b.getAttribute('commandfor');
|
|
449
|
-
if(!targetId) return;
|
|
450
|
-
var dialog = document.getElementById(targetId);
|
|
451
|
-
if(!dialog) return;
|
|
452
|
-
var raw = b.getAttribute('data-dialog-data');
|
|
453
|
-
if(!raw) return;
|
|
454
|
-
var data;
|
|
455
|
-
try { data = JSON.parse(raw); } catch(_err) { return; }
|
|
456
|
-
Object.keys(data).forEach(function(k){
|
|
457
|
-
var inp = dialog.querySelector('[name="col_' + k + '"]');
|
|
458
|
-
if(inp){
|
|
459
|
-
inp.value = data[k] === null || data[k] === undefined ? '' : String(data[k]);
|
|
460
|
-
inp.placeholder = data[k] === null || data[k] === undefined ? 'NULL' : '';
|
|
461
|
-
}
|
|
462
|
-
var hidden = dialog.querySelector('#dialog-field-' + k);
|
|
463
|
-
if(hidden){
|
|
464
|
-
hidden.value = data[k] === null || data[k] === undefined ? '' : String(data[k]);
|
|
465
|
-
}
|
|
466
|
-
});
|
|
467
|
-
var rowidInp = dialog.querySelector('[name="rowid"]');
|
|
468
|
-
if(rowidInp && data.rowid !== undefined) rowidInp.value = String(data.rowid);
|
|
469
|
-
if(typeof dialog.showModal === 'function') dialog.showModal();
|
|
470
|
-
}, true);
|
|
471
|
-
(function initPoll(){
|
|
472
|
-
// Parse human-readable interval: 2s, 500ms, 1m, 30s (default 30s)
|
|
473
|
-
function parseInterval(str){
|
|
474
|
-
if(!str) return 30000;
|
|
475
|
-
var m = str.match(/^(\d+(?:\.\d+)?)(ms|s|m)?$/i);
|
|
476
|
-
if(!m) return 30000;
|
|
477
|
-
var n = parseFloat(m[1]);
|
|
478
|
-
var u = (m[2] || 's').toLowerCase();
|
|
479
|
-
if(u === 'ms') return n;
|
|
480
|
-
if(u === 'm') return n * 60000;
|
|
481
|
-
return n * 1000;
|
|
482
|
-
}
|
|
483
|
-
function bindPollEl(el){
|
|
484
|
-
if(!el || !el.getAttribute) return;
|
|
485
|
-
if(el.getAttribute('data-kuratchi-poll-bound') === '1') return;
|
|
486
|
-
var fn = el.getAttribute('data-poll');
|
|
487
|
-
if(!fn) return;
|
|
488
|
-
el.setAttribute('data-kuratchi-poll-bound', '1');
|
|
489
|
-
var pollId = el.getAttribute('data-poll-id');
|
|
490
|
-
if(!pollId) return; // Server must provide stable poll ID
|
|
491
|
-
var baseIv = parseInterval(el.getAttribute('data-interval'));
|
|
492
|
-
var maxIv = Math.min(baseIv * 10, 300000); // cap at 5 minutes
|
|
493
|
-
var backoff = el.getAttribute('data-backoff') !== 'false';
|
|
494
|
-
var prevHtml = el.innerHTML;
|
|
495
|
-
var currentIv = baseIv;
|
|
496
|
-
(function tick(){
|
|
497
|
-
setTimeout(function(){
|
|
498
|
-
// Request only the fragment, not the full page
|
|
499
|
-
fetch(location.pathname + location.search, { headers: { 'x-kuratchi-fragment': pollId } })
|
|
500
|
-
.then(function(r){ return r.text(); })
|
|
501
|
-
.then(function(html){
|
|
502
|
-
if(prevHtml !== html){
|
|
503
|
-
el.innerHTML = html;
|
|
504
|
-
prevHtml = html;
|
|
505
|
-
currentIv = baseIv; // Reset backoff on change
|
|
506
|
-
} else if(backoff && currentIv < maxIv){
|
|
507
|
-
currentIv = Math.min(currentIv * 1.5, maxIv);
|
|
508
|
-
}
|
|
509
|
-
tick();
|
|
510
|
-
})
|
|
511
|
-
.catch(function(){ currentIv = baseIv; tick(); });
|
|
512
|
-
}, currentIv);
|
|
513
|
-
})();
|
|
514
|
-
}
|
|
515
|
-
function scan(){
|
|
516
|
-
by('[data-poll]').forEach(bindPollEl);
|
|
517
|
-
}
|
|
518
|
-
scan();
|
|
519
|
-
setInterval(scan, 500);
|
|
520
|
-
})();
|
|
521
|
-
function confirmClick(e){
|
|
522
|
-
var el = e.target && e.target.closest ? e.target.closest('[confirm]') : null;
|
|
523
|
-
if(!el) return;
|
|
524
|
-
var msg = el.getAttribute('confirm');
|
|
525
|
-
if(!msg) return;
|
|
526
|
-
if(!window.confirm(msg)){ e.preventDefault(); e.stopPropagation(); }
|
|
527
|
-
}
|
|
528
|
-
document.addEventListener('click', confirmClick, true);
|
|
529
|
-
document.addEventListener('submit', function(e){
|
|
530
|
-
var f = e.target && e.target.matches && e.target.matches('form[confirm]') ? e.target : null;
|
|
531
|
-
if(!f) return;
|
|
532
|
-
var msg = f.getAttribute('confirm');
|
|
533
|
-
if(!msg) return;
|
|
534
|
-
if(!window.confirm(msg)){ e.preventDefault(); e.stopPropagation(); }
|
|
535
|
-
}, true);
|
|
536
|
-
document.addEventListener('submit', function(e){
|
|
537
|
-
if(e.defaultPrevented) return;
|
|
538
|
-
var f = e.target;
|
|
539
|
-
if(!f || !f.querySelector) return;
|
|
540
|
-
var aInput = f.querySelector('input[name="_action"]');
|
|
541
|
-
if(!aInput) return;
|
|
542
|
-
var aName = aInput.value;
|
|
543
|
-
if(!aName) return;
|
|
544
|
-
f.setAttribute('data-action-loading', aName);
|
|
545
|
-
Array.prototype.slice.call(f.querySelectorAll('button[type="submit"],button:not([type="button"]):not([type="reset"])')).forEach(function(b){ b.disabled = true; });
|
|
546
|
-
}, true);
|
|
547
|
-
document.addEventListener('change', function(e){
|
|
548
|
-
var t = e.target;
|
|
549
|
-
if(!t || !t.getAttribute) return;
|
|
550
|
-
var gAll = t.getAttribute('data-select-all');
|
|
551
|
-
if(gAll){
|
|
552
|
-
by('[data-select-item]').filter(function(i){ return i.getAttribute('data-select-item') === gAll; }).forEach(function(i){ i.checked = !!t.checked; });
|
|
553
|
-
syncGroup(gAll);
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
var gItem = t.getAttribute('data-select-item');
|
|
557
|
-
if(gItem) syncGroup(gItem);
|
|
558
|
-
}, true);
|
|
559
|
-
by('[data-select-all]').forEach(function(m){ var g = m.getAttribute('data-select-all'); if(g) syncGroup(g); });
|
|
560
|
-
})();`;
|
|
561
|
-
const reactiveRuntimeSource = `(function(g){
|
|
562
|
-
if(g.__kuratchiReactive) return;
|
|
563
|
-
const targetMap = new WeakMap();
|
|
564
|
-
const proxyMap = new WeakMap();
|
|
565
|
-
let active = null;
|
|
566
|
-
const queue = new Set();
|
|
567
|
-
let flushing = false;
|
|
568
|
-
function queueRun(fn){
|
|
569
|
-
queue.add(fn);
|
|
570
|
-
if(flushing) return;
|
|
571
|
-
flushing = true;
|
|
572
|
-
Promise.resolve().then(function(){
|
|
573
|
-
try {
|
|
574
|
-
const jobs = Array.from(queue);
|
|
575
|
-
queue.clear();
|
|
576
|
-
for (const job of jobs) job();
|
|
577
|
-
} finally {
|
|
578
|
-
flushing = false;
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
function cleanup(effect){
|
|
583
|
-
const deps = effect.__deps || [];
|
|
584
|
-
for (const dep of deps) dep.delete(effect);
|
|
585
|
-
effect.__deps = [];
|
|
586
|
-
}
|
|
587
|
-
function track(target, key){
|
|
588
|
-
if(!active) return;
|
|
589
|
-
let depsMap = targetMap.get(target);
|
|
590
|
-
if(!depsMap){ depsMap = new Map(); targetMap.set(target, depsMap); }
|
|
591
|
-
let dep = depsMap.get(key);
|
|
592
|
-
if(!dep){ dep = new Set(); depsMap.set(key, dep); }
|
|
593
|
-
if(dep.has(active)) return;
|
|
594
|
-
dep.add(active);
|
|
595
|
-
if(!active.__deps) active.__deps = [];
|
|
596
|
-
active.__deps.push(dep);
|
|
597
|
-
}
|
|
598
|
-
function trigger(target, key){
|
|
599
|
-
const depsMap = targetMap.get(target);
|
|
600
|
-
if(!depsMap) return;
|
|
601
|
-
const effects = new Set();
|
|
602
|
-
const add = function(k){
|
|
603
|
-
const dep = depsMap.get(k);
|
|
604
|
-
if(dep) dep.forEach(function(e){ effects.add(e); });
|
|
605
|
-
};
|
|
606
|
-
add(key);
|
|
607
|
-
add('*');
|
|
608
|
-
effects.forEach(function(e){ queueRun(e); });
|
|
609
|
-
}
|
|
610
|
-
function isObject(value){ return value !== null && typeof value === 'object'; }
|
|
611
|
-
function proxify(value){
|
|
612
|
-
if(!isObject(value)) return value;
|
|
613
|
-
if(proxyMap.has(value)) return proxyMap.get(value);
|
|
614
|
-
const proxy = new Proxy(value, {
|
|
615
|
-
get(target, key, receiver){
|
|
616
|
-
track(target, key);
|
|
617
|
-
const out = Reflect.get(target, key, receiver);
|
|
618
|
-
return isObject(out) ? proxify(out) : out;
|
|
619
|
-
},
|
|
620
|
-
set(target, key, next, receiver){
|
|
621
|
-
const prev = target[key];
|
|
622
|
-
const result = Reflect.set(target, key, next, receiver);
|
|
623
|
-
if(prev !== next) trigger(target, key);
|
|
624
|
-
if(Array.isArray(target) && key !== 'length') trigger(target, 'length');
|
|
625
|
-
return result;
|
|
626
|
-
},
|
|
627
|
-
deleteProperty(target, key){
|
|
628
|
-
const had = Object.prototype.hasOwnProperty.call(target, key);
|
|
629
|
-
const result = Reflect.deleteProperty(target, key);
|
|
630
|
-
if(had) trigger(target, key);
|
|
631
|
-
return result;
|
|
632
|
-
}
|
|
633
|
-
});
|
|
634
|
-
proxyMap.set(value, proxy);
|
|
635
|
-
return proxy;
|
|
636
|
-
}
|
|
637
|
-
function effect(fn){
|
|
638
|
-
const run = function(){
|
|
639
|
-
cleanup(run);
|
|
640
|
-
active = run;
|
|
641
|
-
try { fn(); } finally { active = null; }
|
|
642
|
-
};
|
|
643
|
-
run.__deps = [];
|
|
644
|
-
run();
|
|
645
|
-
return function(){ cleanup(run); };
|
|
646
|
-
}
|
|
647
|
-
function state(initial){ return proxify(initial); }
|
|
648
|
-
function replace(_prev, next){ return proxify(next); }
|
|
649
|
-
g.__kuratchiReactive = { state, effect, replace };
|
|
650
|
-
})(window);`;
|
|
651
|
-
const actionScript = `<script>${options.isDev ? bridgeSource : compactInlineJs(bridgeSource)}</script>`;
|
|
652
|
-
const reactiveRuntimeScript = `<script>${options.isDev ? reactiveRuntimeSource : compactInlineJs(reactiveRuntimeSource)}</script>`;
|
|
653
|
-
if (source.includes('</head>')) {
|
|
654
|
-
source = source.replace('</head>', reactiveRuntimeScript + '\n</head>');
|
|
655
|
-
}
|
|
656
|
-
else {
|
|
657
|
-
source = reactiveRuntimeScript + '\n' + source;
|
|
658
|
-
}
|
|
659
|
-
source = source.replace('</body>', actionScript + '\n</body>');
|
|
660
|
-
// Parse layout for <script> block (component imports + data vars)
|
|
661
|
-
const layoutParsed = parseFile(source, { kind: 'layout', filePath: layoutFile });
|
|
662
|
-
const hasLayoutScript = layoutParsed.script && (Object.keys(layoutParsed.componentImports).length > 0 || layoutParsed.hasLoad);
|
|
663
|
-
if (hasLayoutScript) {
|
|
664
|
-
// Dynamic layout �" has component imports and/or data declarations
|
|
665
|
-
// Compile component imports from layout
|
|
666
|
-
for (const [pascalName, fileName] of Object.entries(layoutParsed.componentImports)) {
|
|
667
|
-
compileComponent(fileName);
|
|
668
|
-
layoutComponentNames.set(pascalName, fileName);
|
|
669
|
-
}
|
|
670
|
-
// Replace <slot></slot> with content parameter injection
|
|
671
|
-
let layoutTemplate = layoutParsed.template.replace(/<slot\s*><\/slot>/g, '{@raw __content}');
|
|
672
|
-
layoutTemplate = layoutTemplate.replace(/<slot\s*\/>/g, '{@raw __content}');
|
|
673
|
-
// Build layout action names so action={fn} works in layouts, including action props
|
|
674
|
-
// passed through child components like <Dashboard footerSignOutAction={signOut}>.
|
|
675
|
-
const layoutActionNames = new Set(layoutParsed.actionFunctions);
|
|
676
|
-
for (const [pascalName, compFileName] of layoutComponentNames.entries()) {
|
|
677
|
-
const actionPropNames = componentActionCache.get(compFileName);
|
|
678
|
-
const compTagRegex = new RegExp(`<${pascalName}\\b([\\s\\S]*?)(?:/?)>`, 'g');
|
|
679
|
-
for (const tagMatch of layoutParsed.template.matchAll(compTagRegex)) {
|
|
680
|
-
const attrs = tagMatch[1];
|
|
681
|
-
if (actionPropNames && actionPropNames.size > 0) {
|
|
682
|
-
for (const propName of actionPropNames) {
|
|
683
|
-
const propRegex = new RegExp(`\\b${propName}=\\{([A-Za-z_$][\\w$]*)\\}`);
|
|
684
|
-
const propMatch = attrs.match(propRegex);
|
|
685
|
-
if (propMatch) {
|
|
686
|
-
layoutActionNames.add(propMatch[1]);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
// Compile the layout template with component + action support
|
|
693
|
-
const layoutRenderBody = compileTemplate(layoutTemplate, layoutComponentNames, layoutActionNames);
|
|
694
|
-
// Collect component CSS for layout
|
|
695
|
-
const layoutComponentStyles = [];
|
|
696
|
-
for (const fileName of layoutComponentNames.values()) {
|
|
697
|
-
const css = componentStyleCache.get(fileName);
|
|
698
|
-
if (css)
|
|
699
|
-
layoutComponentStyles.push(css);
|
|
700
|
-
}
|
|
701
|
-
// Inject component CSS after 'let __html = "";'
|
|
702
|
-
let finalLayoutBody = layoutRenderBody;
|
|
703
|
-
if (layoutComponentStyles.length > 0) {
|
|
704
|
-
const lines = layoutRenderBody.split('\n');
|
|
705
|
-
const styleLines = layoutComponentStyles.map(css => `__html += \`${css}\\n\`;`);
|
|
706
|
-
finalLayoutBody = [lines[0], ...styleLines, ...lines.slice(1)].join('\n');
|
|
707
|
-
}
|
|
708
|
-
// Build the layout script body (data vars, etc.)
|
|
709
|
-
let layoutScriptBody = stripTopLevelImports(layoutParsed.script);
|
|
710
|
-
const layoutDevDecls = buildDevAliasDeclarations(layoutParsed.devAliases, !!options.isDev);
|
|
711
|
-
layoutScriptBody = [layoutDevDecls, layoutScriptBody].filter(Boolean).join('\n');
|
|
712
|
-
compiledLayout = `function __layout(__content) {
|
|
713
|
-
const __esc = (v) => { if (v == null) return ''; return String(v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); };
|
|
714
|
-
${layoutScriptBody ? layoutScriptBody + '\n ' : ''}${finalLayoutBody}
|
|
715
|
-
return __html;
|
|
716
|
-
}`;
|
|
717
|
-
}
|
|
718
|
-
else {
|
|
719
|
-
// Static layout �" no components, use fast string split (original behavior)
|
|
720
|
-
const slotMarker = '<slot></slot>';
|
|
721
|
-
const slotIdx = source.indexOf(slotMarker);
|
|
722
|
-
if (slotIdx === -1) {
|
|
723
|
-
throw new Error('layout.html must contain <slot></slot>');
|
|
724
|
-
}
|
|
725
|
-
const escLayout = (s) => s.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, () => '\\$');
|
|
726
|
-
const before = escLayout(source.slice(0, slotIdx));
|
|
727
|
-
const after = escLayout(source.slice(slotIdx + slotMarker.length));
|
|
728
|
-
compiledLayout = `const __layoutBefore = \`${before}\`;\nconst __layoutAfter = \`${after}\`;\nfunction __layout(content) {\n return __layoutBefore + content + __layoutAfter;\n}`;
|
|
729
|
-
}
|
|
106
|
+
const source = prepareRootLayoutSource({
|
|
107
|
+
source: layoutImportSource,
|
|
108
|
+
isDev: !!options.isDev,
|
|
109
|
+
themeCss: themeCSS,
|
|
110
|
+
uiConfigValues,
|
|
111
|
+
});
|
|
112
|
+
layoutPlan = compileLayoutPlan({
|
|
113
|
+
renderSource: source,
|
|
114
|
+
importSource: layoutImportSource,
|
|
115
|
+
layoutFile,
|
|
116
|
+
isDev: !!options.isDev,
|
|
117
|
+
componentCompiler,
|
|
118
|
+
clientModuleCompiler,
|
|
119
|
+
assetsPrefix,
|
|
120
|
+
clientScopeId: 'layout_root',
|
|
121
|
+
});
|
|
122
|
+
compiledLayout = layoutPlan.compiledLayout;
|
|
730
123
|
}
|
|
731
124
|
// Custom error pages: src/routes/NNN.html (e.g. 404.html, 500.html, 401.html, 403.html)
|
|
732
|
-
// Only compiled if the user explicitly creates them
|
|
733
|
-
const compiledErrorPages =
|
|
734
|
-
for (const file of fs.readdirSync(routesDir)) {
|
|
735
|
-
const match = file.match(/^(\d{3})\.html$/);
|
|
736
|
-
if (!match)
|
|
737
|
-
continue;
|
|
738
|
-
const status = parseInt(match[1], 10);
|
|
739
|
-
const source = fs.readFileSync(path.join(routesDir, file), 'utf-8');
|
|
740
|
-
const body = compileTemplate(source);
|
|
741
|
-
// 500.html receives `error` as a variable; others don't need it
|
|
742
|
-
compiledErrorPages.set(status, `function __error_${status}(error) {\n ${body}\n return __html;\n}`);
|
|
743
|
-
}
|
|
744
|
-
// Read assets prefix from kuratchi.config.ts (default: /assets/)
|
|
745
|
-
const assetsPrefix = readAssetsPrefix(projectDir);
|
|
125
|
+
// Only compiled if the user explicitly creates them ?" otherwise the framework's built-in default is used
|
|
126
|
+
const compiledErrorPages = compileErrorPages(routesDir);
|
|
746
127
|
// Read kuratchi.config.ts at build time to discover ORM database configs
|
|
747
128
|
const ormDatabases = readOrmConfig(projectDir);
|
|
748
129
|
// Read auth config from kuratchi.config.ts
|
|
749
130
|
const authConfig = readAuthConfig(projectDir);
|
|
131
|
+
// Read security config from kuratchi.config.ts
|
|
132
|
+
const securityConfig = readSecurityConfig(projectDir);
|
|
750
133
|
// Auto-discover Durable Objects from .do.ts files (config optional, only needed for stubId)
|
|
751
134
|
const configDoEntries = readDoConfig(projectDir);
|
|
752
135
|
const { config: doConfig, handlers: doHandlers } = discoverDurableObjects(srcDir, configDoEntries, ormDatabases);
|
|
@@ -765,14 +148,17 @@ export function compile(options) {
|
|
|
765
148
|
if (!fs.existsSync(doProxyDir))
|
|
766
149
|
fs.mkdirSync(doProxyDir, { recursive: true });
|
|
767
150
|
for (const handler of doHandlers) {
|
|
768
|
-
const proxyCode = generateHandlerProxy(handler,
|
|
769
|
-
|
|
151
|
+
const proxyCode = generateHandlerProxy(handler, {
|
|
152
|
+
projectDir,
|
|
153
|
+
runtimeDoImport: RUNTIME_DO_IMPORT,
|
|
154
|
+
});
|
|
155
|
+
const proxyFile = path.join(doProxyDir, handler.fileName + '.ts');
|
|
770
156
|
const proxyFileDir = path.dirname(proxyFile);
|
|
771
157
|
if (!fs.existsSync(proxyFileDir))
|
|
772
158
|
fs.mkdirSync(proxyFileDir, { recursive: true });
|
|
773
159
|
writeIfChanged(proxyFile, proxyCode);
|
|
774
160
|
const handlerAbsNoExt = handler.absPath.replace(/\\/g, '/').replace(/\.ts$/, '');
|
|
775
|
-
const proxyAbsNoExt = proxyFile.replace(/\\/g, '/').replace(/\.
|
|
161
|
+
const proxyAbsNoExt = proxyFile.replace(/\\/g, '/').replace(/\.ts$/, '');
|
|
776
162
|
registerDoProxyPath(handlerAbsNoExt, proxyAbsNoExt);
|
|
777
163
|
// Backward-compatible alias for '.do' suffix.
|
|
778
164
|
registerDoProxyPath(handlerAbsNoExt.replace(/\.do$/, ''), proxyAbsNoExt.replace(/\.do$/, ''));
|
|
@@ -781,213 +167,42 @@ export function compile(options) {
|
|
|
781
167
|
registerDoProxyPath(path.join(srcDir, 'durable-objects', handler.fileName.replace(/\.do$/, '')).replace(/\\/g, '/'), proxyAbsNoExt.replace(/\.do$/, ''));
|
|
782
168
|
if (handler.fileName.endsWith('.do')) {
|
|
783
169
|
const aliasFileName = handler.fileName.slice(0, -3);
|
|
784
|
-
const aliasProxyFile = path.join(doProxyDir, aliasFileName + '.
|
|
785
|
-
const aliasCode = `// Auto-generated alias for .do handler\nexport * from './${handler.fileName}.
|
|
170
|
+
const aliasProxyFile = path.join(doProxyDir, aliasFileName + '.ts');
|
|
171
|
+
const aliasCode = `// Auto-generated alias for .do handler\nexport * from './${handler.fileName}.ts';\n`;
|
|
786
172
|
const aliasProxyDir = path.dirname(aliasProxyFile);
|
|
787
173
|
if (!fs.existsSync(aliasProxyDir))
|
|
788
174
|
fs.mkdirSync(aliasProxyDir, { recursive: true });
|
|
789
175
|
writeIfChanged(aliasProxyFile, aliasCode);
|
|
790
|
-
registerDoProxyPath(handlerAbsNoExt.replace(/\.do$/, ''), aliasProxyFile.replace(/\\/g, '/').replace(/\.
|
|
176
|
+
registerDoProxyPath(handlerAbsNoExt.replace(/\.do$/, ''), aliasProxyFile.replace(/\\/g, '/').replace(/\.ts$/, ''));
|
|
791
177
|
}
|
|
792
178
|
}
|
|
793
179
|
}
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
absBase + '.cjs',
|
|
801
|
-
path.join(absBase, 'index.ts'),
|
|
802
|
-
path.join(absBase, 'index.js'),
|
|
803
|
-
path.join(absBase, 'index.mjs'),
|
|
804
|
-
path.join(absBase, 'index.cjs'),
|
|
805
|
-
];
|
|
806
|
-
for (const candidate of candidates) {
|
|
807
|
-
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
|
|
808
|
-
return candidate;
|
|
809
|
-
}
|
|
810
|
-
return null;
|
|
811
|
-
};
|
|
812
|
-
const toModuleSpecifier = (fromFileAbs, toFileAbs) => {
|
|
813
|
-
let rel = path.relative(path.dirname(fromFileAbs), toFileAbs).replace(/\\/g, '/');
|
|
814
|
-
if (!rel.startsWith('.'))
|
|
815
|
-
rel = './' + rel;
|
|
816
|
-
return rel;
|
|
817
|
-
};
|
|
818
|
-
const transformedServerModules = new Map();
|
|
819
|
-
const modulesOutDir = path.join(projectDir, '.kuratchi', 'modules');
|
|
820
|
-
const resolveDoProxyTarget = (absPath) => {
|
|
821
|
-
const normalizedNoExt = absPath.replace(/\\/g, '/').replace(/\.[^.\/]+$/, '');
|
|
822
|
-
const proxyNoExt = doHandlerProxyPaths.get(normalizedNoExt);
|
|
823
|
-
if (!proxyNoExt)
|
|
824
|
-
return null;
|
|
825
|
-
return resolveExistingModuleFile(proxyNoExt) ?? (fs.existsSync(proxyNoExt + '.js') ? proxyNoExt + '.js' : null);
|
|
826
|
-
};
|
|
827
|
-
const resolveImportTarget = (importerAbs, spec) => {
|
|
828
|
-
if (spec.startsWith('$')) {
|
|
829
|
-
const slashIdx = spec.indexOf('/');
|
|
830
|
-
const folder = slashIdx === -1 ? spec.slice(1) : spec.slice(1, slashIdx);
|
|
831
|
-
const rest = slashIdx === -1 ? '' : spec.slice(slashIdx + 1);
|
|
832
|
-
if (!folder)
|
|
833
|
-
return null;
|
|
834
|
-
const abs = path.join(srcDir, folder, rest);
|
|
835
|
-
return resolveExistingModuleFile(abs) ?? abs;
|
|
836
|
-
}
|
|
837
|
-
if (spec.startsWith('.')) {
|
|
838
|
-
const abs = path.resolve(path.dirname(importerAbs), spec);
|
|
839
|
-
return resolveExistingModuleFile(abs) ?? abs;
|
|
840
|
-
}
|
|
841
|
-
return null;
|
|
842
|
-
};
|
|
843
|
-
const transformServerModule = (entryAbsPath) => {
|
|
844
|
-
const resolved = resolveExistingModuleFile(entryAbsPath) ?? entryAbsPath;
|
|
845
|
-
const normalized = resolved.replace(/\\/g, '/');
|
|
846
|
-
const cached = transformedServerModules.get(normalized);
|
|
847
|
-
if (cached)
|
|
848
|
-
return cached;
|
|
849
|
-
const relFromProject = path.relative(projectDir, resolved);
|
|
850
|
-
const outPath = path.join(modulesOutDir, relFromProject);
|
|
851
|
-
transformedServerModules.set(normalized, outPath);
|
|
852
|
-
const outDir = path.dirname(outPath);
|
|
853
|
-
if (!fs.existsSync(outDir))
|
|
854
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
855
|
-
if (!/\.(ts|js|mjs|cjs)$/i.test(resolved) || !fs.existsSync(resolved)) {
|
|
856
|
-
const passthrough = resolved;
|
|
857
|
-
transformedServerModules.set(normalized, passthrough);
|
|
858
|
-
return passthrough;
|
|
859
|
-
}
|
|
860
|
-
const source = fs.readFileSync(resolved, 'utf-8');
|
|
861
|
-
const rewriteSpecifier = (spec) => {
|
|
862
|
-
const target = resolveImportTarget(resolved, spec);
|
|
863
|
-
if (!target)
|
|
864
|
-
return spec;
|
|
865
|
-
const doProxyTarget = resolveDoProxyTarget(target);
|
|
866
|
-
if (doProxyTarget)
|
|
867
|
-
return toModuleSpecifier(outPath, doProxyTarget);
|
|
868
|
-
const normalizedTarget = target.replace(/\\/g, '/');
|
|
869
|
-
const inProject = normalizedTarget.startsWith(projectDir.replace(/\\/g, '/') + '/');
|
|
870
|
-
if (!inProject)
|
|
871
|
-
return spec;
|
|
872
|
-
const targetResolved = resolveExistingModuleFile(target) ?? target;
|
|
873
|
-
if (!/\.(ts|js|mjs|cjs)$/i.test(targetResolved))
|
|
874
|
-
return spec;
|
|
875
|
-
const rewrittenTarget = transformServerModule(targetResolved);
|
|
876
|
-
return toModuleSpecifier(outPath, rewrittenTarget);
|
|
877
|
-
};
|
|
878
|
-
let rewritten = source.replace(/(from\s+)(['"])([^'"]+)\2/g, (_m, p1, q, spec) => {
|
|
879
|
-
return `${p1}${q}${rewriteSpecifier(spec)}${q}`;
|
|
880
|
-
});
|
|
881
|
-
rewritten = rewritten.replace(/(import\s*\(\s*)(['"])([^'"]+)\2(\s*\))/g, (_m, p1, q, spec, p4) => {
|
|
882
|
-
return `${p1}${q}${rewriteSpecifier(spec)}${q}${p4}`;
|
|
883
|
-
});
|
|
884
|
-
writeIfChanged(outPath, rewritten);
|
|
885
|
-
return outPath;
|
|
886
|
-
};
|
|
887
|
-
const resolveCompiledImportPath = (origPath, importerDir, outFileDir) => {
|
|
888
|
-
const isBareModule = !origPath.startsWith('.') && !origPath.startsWith('/') && !origPath.startsWith('$');
|
|
889
|
-
if (isBareModule)
|
|
890
|
-
return origPath;
|
|
891
|
-
let absImport;
|
|
892
|
-
if (origPath.startsWith('$')) {
|
|
893
|
-
const slashIdx = origPath.indexOf('/');
|
|
894
|
-
const folder = slashIdx === -1 ? origPath.slice(1) : origPath.slice(1, slashIdx);
|
|
895
|
-
const rest = slashIdx === -1 ? '' : origPath.slice(slashIdx + 1);
|
|
896
|
-
absImport = path.join(srcDir, folder, rest);
|
|
897
|
-
}
|
|
898
|
-
else {
|
|
899
|
-
absImport = path.resolve(importerDir, origPath);
|
|
900
|
-
}
|
|
901
|
-
const doProxyTarget = resolveDoProxyTarget(absImport);
|
|
902
|
-
const target = doProxyTarget ?? transformServerModule(absImport);
|
|
903
|
-
let relPath = path.relative(outFileDir, target).replace(/\\/g, '/');
|
|
904
|
-
if (!relPath.startsWith('.'))
|
|
905
|
-
relPath = './' + relPath;
|
|
906
|
-
return relPath;
|
|
907
|
-
};
|
|
180
|
+
const serverModuleCompiler = createServerModuleCompiler({
|
|
181
|
+
projectDir,
|
|
182
|
+
srcDir,
|
|
183
|
+
doHandlerProxyPaths,
|
|
184
|
+
writeFile: writeIfChanged,
|
|
185
|
+
});
|
|
908
186
|
// Parse and compile each route
|
|
909
187
|
const compiledRoutes = [];
|
|
910
188
|
const allImports = [];
|
|
911
189
|
let moduleCounter = 0;
|
|
912
|
-
// Layout server import resolution
|
|
190
|
+
// Layout server import resolution ?" resolve non-component imports to module IDs
|
|
913
191
|
let isLayoutAsync = false;
|
|
914
192
|
let compiledLayoutActions = null;
|
|
915
|
-
if (
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const moduleId = `__m${moduleCounter++}`;
|
|
929
|
-
allImports.push(`import * as ${moduleId} from '${importPath}';`);
|
|
930
|
-
const namesMatch = imp.match(/import\s*\{([^}]+)\}/);
|
|
931
|
-
if (namesMatch) {
|
|
932
|
-
const names = namesMatch[1]
|
|
933
|
-
.split(',')
|
|
934
|
-
.map(n => n.trim())
|
|
935
|
-
.filter(Boolean)
|
|
936
|
-
.map(n => {
|
|
937
|
-
const parts = n.split(/\s+as\s+/).map(p => p.trim()).filter(Boolean);
|
|
938
|
-
return parts[1] || parts[0] || '';
|
|
939
|
-
})
|
|
940
|
-
.filter(Boolean);
|
|
941
|
-
for (const name of names) {
|
|
942
|
-
layoutFnToModule[name] = moduleId;
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
const starMatch = imp.match(/import\s*\*\s*as\s+(\w+)/);
|
|
946
|
-
if (starMatch) {
|
|
947
|
-
layoutFnToModule[starMatch[1]] = moduleId;
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
// Rewrite function calls in the compiled layout body
|
|
951
|
-
for (const [fnName, moduleId] of Object.entries(layoutFnToModule)) {
|
|
952
|
-
if (!/^[A-Za-z_$][\w$]*$/.test(fnName))
|
|
953
|
-
continue;
|
|
954
|
-
const callRegex = new RegExp(`\\b${fnName}\\s*\\(`, 'g');
|
|
955
|
-
compiledLayout = compiledLayout.replace(callRegex, `${moduleId}.${fnName}(`);
|
|
956
|
-
}
|
|
957
|
-
// Generate layout actions map for action={fn} in layouts and action props passed
|
|
958
|
-
// through layout components.
|
|
959
|
-
const layoutActionNames = new Set(layoutParsedForImports.actionFunctions);
|
|
960
|
-
for (const [pascalName, compFileName] of layoutComponentNames.entries()) {
|
|
961
|
-
const actionPropNames = componentActionCache.get(compFileName);
|
|
962
|
-
const compTagRegex = new RegExp(`<${pascalName}\\b([\\s\\S]*?)(?:/?)>`, 'g');
|
|
963
|
-
for (const tagMatch of layoutParsedForImports.template.matchAll(compTagRegex)) {
|
|
964
|
-
const attrs = tagMatch[1];
|
|
965
|
-
if (actionPropNames && actionPropNames.size > 0) {
|
|
966
|
-
for (const propName of actionPropNames) {
|
|
967
|
-
const propRegex = new RegExp(`\\b${propName}=\\{([A-Za-z_$][\\w$]*)\\}`);
|
|
968
|
-
const propMatch = attrs.match(propRegex);
|
|
969
|
-
if (propMatch) {
|
|
970
|
-
layoutActionNames.add(propMatch[1]);
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
if (layoutActionNames.size > 0) {
|
|
977
|
-
const actionEntries = Array.from(layoutActionNames)
|
|
978
|
-
.filter(fn => fn in layoutFnToModule)
|
|
979
|
-
.map(fn => `'${fn}': ${layoutFnToModule[fn]}.${fn}`)
|
|
980
|
-
.join(', ');
|
|
981
|
-
if (actionEntries) {
|
|
982
|
-
compiledLayoutActions = `{ ${actionEntries} }`;
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
// Detect if the compiled layout uses await �' make it async
|
|
987
|
-
isLayoutAsync = /\bawait\b/.test(compiledLayout);
|
|
988
|
-
if (isLayoutAsync) {
|
|
989
|
-
compiledLayout = compiledLayout.replace(/^function __layout\(/, 'async function __layout(');
|
|
990
|
-
}
|
|
193
|
+
if (layoutPlan) {
|
|
194
|
+
const finalizedLayout = finalizeLayoutPlan({
|
|
195
|
+
plan: layoutPlan,
|
|
196
|
+
layoutFile,
|
|
197
|
+
projectDir,
|
|
198
|
+
resolveCompiledImportPath: serverModuleCompiler.resolveCompiledImportPath,
|
|
199
|
+
allocateModuleId: () => `__m${moduleCounter++}`,
|
|
200
|
+
pushImport: (statement) => allImports.push(statement),
|
|
201
|
+
componentCompiler,
|
|
202
|
+
});
|
|
203
|
+
compiledLayout = finalizedLayout.compiledLayout;
|
|
204
|
+
compiledLayoutActions = finalizedLayout.compiledLayoutActions;
|
|
205
|
+
isLayoutAsync = finalizedLayout.isLayoutAsync;
|
|
991
206
|
}
|
|
992
207
|
for (let i = 0; i < routeFiles.length; i++) {
|
|
993
208
|
const rf = routeFiles[i];
|
|
@@ -995,274 +210,60 @@ export function compile(options) {
|
|
|
995
210
|
const pattern = filePathToPattern(rf.name);
|
|
996
211
|
// -- API route (index.ts / index.js) --
|
|
997
212
|
if (rf.type === 'api') {
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
allImports.push(`import * as ${moduleId} from '${importPath}';`);
|
|
1007
|
-
// Scan the source for exported method handlers (only include what exists)
|
|
1008
|
-
const apiSource = fs.readFileSync(fullPath, 'utf-8');
|
|
1009
|
-
const allMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
|
|
1010
|
-
const exportedMethods = allMethods.filter(m => {
|
|
1011
|
-
// Match: export function GET, export async function GET, export { ... as GET }
|
|
1012
|
-
const fnPattern = new RegExp(`export\\s+(async\\s+)?function\\s+${m}\\b`);
|
|
1013
|
-
const reExportPattern = new RegExp(`export\\s*\\{[^}]*\\b\\w+\\s+as\\s+${m}\\b`);
|
|
1014
|
-
const namedExportPattern = new RegExp(`export\\s*\\{[^}]*\\b${m}\\b`);
|
|
1015
|
-
return fnPattern.test(apiSource) || reExportPattern.test(apiSource) || namedExportPattern.test(apiSource);
|
|
1016
|
-
});
|
|
1017
|
-
const methodEntries = exportedMethods
|
|
1018
|
-
.map(m => `${m}: ${moduleId}.${m}`)
|
|
1019
|
-
.join(', ');
|
|
1020
|
-
compiledRoutes.push(`{ pattern: '${pattern}', __api: true, ${methodEntries} }`);
|
|
213
|
+
compiledRoutes.push(compileApiRoute({
|
|
214
|
+
pattern,
|
|
215
|
+
fullPath,
|
|
216
|
+
projectDir,
|
|
217
|
+
transformModule: serverModuleCompiler.transformModule,
|
|
218
|
+
allocateModuleId: () => `__m${moduleCounter++}`,
|
|
219
|
+
pushImport: (statement) => allImports.push(statement),
|
|
220
|
+
}));
|
|
1021
221
|
continue;
|
|
1022
222
|
}
|
|
1023
223
|
// -- Page route (page.html) --
|
|
1024
|
-
const source = fs.readFileSync(fullPath, 'utf-8');
|
|
224
|
+
const source = fileContents.get(fullPath) ?? fs.readFileSync(fullPath, 'utf-8');
|
|
1025
225
|
const parsed = parseFile(source, { kind: 'route', filePath: fullPath });
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
})
|
|
1033
|
-
|
|
1034
|
-
line,
|
|
1035
|
-
importerDir: path.dirname(fullPath),
|
|
1036
|
-
}));
|
|
1037
|
-
const mergedActionFunctions = [...parsed.actionFunctions];
|
|
1038
|
-
const mergedDataVars = [...parsed.dataVars];
|
|
1039
|
-
const mergedPollFunctions = [...parsed.pollFunctions];
|
|
1040
|
-
const mergedDataGetQueries = parsed.dataGetQueries.map((query) => ({ ...query }));
|
|
1041
|
-
const mergedComponentImports = { ...parsed.componentImports };
|
|
1042
|
-
const mergedWorkerEnvAliases = [...parsed.workerEnvAliases];
|
|
1043
|
-
const mergedDevAliases = [...parsed.devAliases];
|
|
1044
|
-
for (const layoutRelPath of rf.layouts) {
|
|
1045
|
-
if (layoutRelPath === 'layout.html')
|
|
1046
|
-
continue;
|
|
1047
|
-
const layoutPath = path.join(routesDir, layoutRelPath);
|
|
1048
|
-
if (!fs.existsSync(layoutPath))
|
|
1049
|
-
continue;
|
|
1050
|
-
const layoutSource = fs.readFileSync(layoutPath, 'utf-8');
|
|
1051
|
-
const layoutParsed = parseFile(layoutSource, { kind: 'layout', filePath: layoutPath });
|
|
1052
|
-
if (layoutParsed.loadFunction) {
|
|
1053
|
-
throw new Error(`${layoutRelPath} cannot export load(); nested layouts currently share the child route load lifecycle.`);
|
|
1054
|
-
}
|
|
1055
|
-
const layoutSlot = layoutParsed.template.match(/<slot\s*><\/slot>|<slot\s*\/>/);
|
|
1056
|
-
if (!layoutSlot) {
|
|
1057
|
-
throw new Error(`${layoutRelPath} must contain <slot></slot> or <slot />`);
|
|
1058
|
-
}
|
|
1059
|
-
if (layoutParsed.script) {
|
|
1060
|
-
routeScriptParts.push(layoutParsed.script);
|
|
1061
|
-
routeScriptSegments.push({ script: layoutParsed.script, dataVars: [...layoutParsed.dataVars] });
|
|
1062
|
-
}
|
|
1063
|
-
for (const line of layoutParsed.serverImports) {
|
|
1064
|
-
routeServerImportEntries.push({ line, importerDir: path.dirname(layoutPath) });
|
|
1065
|
-
}
|
|
1066
|
-
for (const line of layoutParsed.clientImports) {
|
|
1067
|
-
routeClientImportEntries.push({ line, importerDir: path.dirname(layoutPath) });
|
|
1068
|
-
}
|
|
1069
|
-
for (const fnName of layoutParsed.actionFunctions) {
|
|
1070
|
-
if (!mergedActionFunctions.includes(fnName))
|
|
1071
|
-
mergedActionFunctions.push(fnName);
|
|
1072
|
-
}
|
|
1073
|
-
for (const varName of layoutParsed.dataVars) {
|
|
1074
|
-
if (!mergedDataVars.includes(varName))
|
|
1075
|
-
mergedDataVars.push(varName);
|
|
1076
|
-
}
|
|
1077
|
-
for (const fnName of layoutParsed.pollFunctions) {
|
|
1078
|
-
if (!mergedPollFunctions.includes(fnName))
|
|
1079
|
-
mergedPollFunctions.push(fnName);
|
|
1080
|
-
}
|
|
1081
|
-
for (const query of layoutParsed.dataGetQueries) {
|
|
1082
|
-
if (!mergedDataGetQueries.some((existing) => existing.asName === query.asName)) {
|
|
1083
|
-
mergedDataGetQueries.push({ ...query });
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
for (const [pascalName, fileName] of Object.entries(layoutParsed.componentImports)) {
|
|
1087
|
-
mergedComponentImports[pascalName] = fileName;
|
|
1088
|
-
}
|
|
1089
|
-
for (const alias of layoutParsed.workerEnvAliases) {
|
|
1090
|
-
if (!mergedWorkerEnvAliases.includes(alias))
|
|
1091
|
-
mergedWorkerEnvAliases.push(alias);
|
|
1092
|
-
}
|
|
1093
|
-
for (const alias of layoutParsed.devAliases) {
|
|
1094
|
-
if (!mergedDevAliases.includes(alias))
|
|
1095
|
-
mergedDevAliases.push(alias);
|
|
1096
|
-
}
|
|
1097
|
-
effectiveTemplate = layoutParsed.template.replace(layoutSlot[0], effectiveTemplate);
|
|
1098
|
-
}
|
|
1099
|
-
if (parsed.script) {
|
|
1100
|
-
routeScriptParts.push(parsed.script);
|
|
1101
|
-
routeScriptSegments.push({ script: parsed.script, dataVars: [...parsed.dataVars] });
|
|
1102
|
-
}
|
|
1103
|
-
const routeImportDecls = [];
|
|
1104
|
-
const routeImportDeclMap = new Map();
|
|
1105
|
-
const routeScriptReferenceSource = [...routeScriptParts.map((script) => stripTopLevelImports(script)), parsed.loadFunction || ''].join('\n');
|
|
1106
|
-
const mergedParsed = {
|
|
1107
|
-
...parsed,
|
|
1108
|
-
template: effectiveTemplate,
|
|
1109
|
-
script: routeScriptParts.length > 0 ? routeScriptParts.join('\n\n') : parsed.script,
|
|
1110
|
-
serverImports: routeServerImportEntries.map((entry) => entry.line),
|
|
1111
|
-
clientImports: routeClientImportEntries.map((entry) => entry.line),
|
|
1112
|
-
actionFunctions: mergedActionFunctions,
|
|
1113
|
-
dataVars: mergedDataVars,
|
|
1114
|
-
componentImports: mergedComponentImports,
|
|
1115
|
-
pollFunctions: mergedPollFunctions,
|
|
1116
|
-
dataGetQueries: mergedDataGetQueries,
|
|
1117
|
-
workerEnvAliases: mergedWorkerEnvAliases,
|
|
1118
|
-
devAliases: mergedDevAliases,
|
|
1119
|
-
scriptImportDecls: routeImportDecls,
|
|
1120
|
-
scriptSegments: routeScriptSegments,
|
|
1121
|
-
};
|
|
1122
|
-
// Build a mapping: functionName ? moduleId for all imports in this route
|
|
1123
|
-
const fnToModule = {};
|
|
1124
|
-
const outFileDir = path.join(projectDir, '.kuratchi');
|
|
1125
|
-
const neededServerFns = new Set([
|
|
1126
|
-
...mergedActionFunctions,
|
|
1127
|
-
...mergedPollFunctions,
|
|
1128
|
-
...mergedDataGetQueries.map((q) => q.fnName),
|
|
1129
|
-
]);
|
|
1130
|
-
const routeServerImports = routeServerImportEntries.length > 0
|
|
1131
|
-
? routeServerImportEntries
|
|
1132
|
-
: routeClientImportEntries.filter((entry) => filterClientImportsForServer([entry.line], neededServerFns).length > 0);
|
|
1133
|
-
if (routeServerImports.length > 0) {
|
|
1134
|
-
for (const entry of routeServerImports) {
|
|
1135
|
-
const imp = entry.line;
|
|
1136
|
-
const pathMatch = imp.match(/from\s+['"]([^'"]+)['"]/);
|
|
1137
|
-
if (!pathMatch)
|
|
1138
|
-
continue;
|
|
1139
|
-
const origPath = pathMatch[1];
|
|
1140
|
-
const importPath = resolveCompiledImportPath(origPath, entry.importerDir, outFileDir);
|
|
1141
|
-
const moduleId = `__m${moduleCounter++}`;
|
|
1142
|
-
allImports.push(`import * as ${moduleId} from '${importPath}';`);
|
|
1143
|
-
const namedBindings = parseNamedImportBindings(imp);
|
|
1144
|
-
if (namedBindings.length > 0) {
|
|
1145
|
-
for (const binding of namedBindings) {
|
|
1146
|
-
fnToModule[binding.local] = moduleId;
|
|
1147
|
-
if (routeScriptReferenceSource.includes(binding.local) && !routeImportDeclMap.has(binding.local)) {
|
|
1148
|
-
routeImportDeclMap.set(binding.local, `const ${binding.local} = ${moduleId}.${binding.imported};`);
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
const starMatch = imp.match(/import\s*\*\s*as\s+(\w+)/);
|
|
1153
|
-
if (starMatch) {
|
|
1154
|
-
fnToModule[starMatch[1]] = moduleId;
|
|
1155
|
-
if (routeScriptReferenceSource.includes(starMatch[1]) && !routeImportDeclMap.has(starMatch[1])) {
|
|
1156
|
-
routeImportDeclMap.set(starMatch[1], `const ${starMatch[1]} = ${moduleId};`);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
routeImportDecls.push(...routeImportDeclMap.values());
|
|
1162
|
-
const routeComponentNames = new Map();
|
|
1163
|
-
for (const [pascalName, fileName] of Object.entries(mergedComponentImports)) {
|
|
1164
|
-
compileComponent(fileName);
|
|
1165
|
-
routeComponentNames.set(pascalName, fileName);
|
|
1166
|
-
}
|
|
1167
|
-
for (const [pascalName, compFileName] of routeComponentNames.entries()) {
|
|
1168
|
-
const actionPropNames = componentActionCache.get(compFileName);
|
|
1169
|
-
const compTagRegex = new RegExp(`<${pascalName}\\b([\\s\\S]*?)(?:/?)>`, 'g');
|
|
1170
|
-
for (const tagMatch of effectiveTemplate.matchAll(compTagRegex)) {
|
|
1171
|
-
const attrs = tagMatch[1];
|
|
1172
|
-
if (actionPropNames && actionPropNames.size > 0) {
|
|
1173
|
-
for (const propName of actionPropNames) {
|
|
1174
|
-
const propRegex = new RegExp(`\\b${propName}=\\{([A-Za-z_$][\\w$]*)\\}`);
|
|
1175
|
-
const propMatch = attrs.match(propRegex);
|
|
1176
|
-
if (propMatch) {
|
|
1177
|
-
const routeFnName = propMatch[1];
|
|
1178
|
-
if (routeFnName in fnToModule && !mergedActionFunctions.includes(routeFnName)) {
|
|
1179
|
-
mergedActionFunctions.push(routeFnName);
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
const dataVarsSet = new Set(mergedDataVars);
|
|
1187
|
-
const actionNames = new Set(mergedActionFunctions.filter(fn => fn in fnToModule || dataVarsSet.has(fn)));
|
|
1188
|
-
const rpcNameMap = new Map();
|
|
1189
|
-
let rpcCounter = 0;
|
|
1190
|
-
for (const fnName of mergedPollFunctions) {
|
|
1191
|
-
if (!rpcNameMap.has(fnName)) {
|
|
1192
|
-
rpcNameMap.set(fnName, `rpc_${i}_${rpcCounter++}`);
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
for (const q of mergedDataGetQueries) {
|
|
1196
|
-
if (!rpcNameMap.has(q.fnName)) {
|
|
1197
|
-
rpcNameMap.set(q.fnName, `rpc_${i}_${rpcCounter++}`);
|
|
1198
|
-
}
|
|
1199
|
-
q.rpcId = rpcNameMap.get(q.fnName);
|
|
1200
|
-
}
|
|
1201
|
-
const renderBody = compileTemplate(effectiveTemplate, routeComponentNames, actionNames, rpcNameMap);
|
|
1202
|
-
// Collect component CSS for this route (compile-time dedup)
|
|
1203
|
-
const routeComponentStyles = [];
|
|
1204
|
-
for (const fileName of routeComponentNames.values()) {
|
|
1205
|
-
const css = componentStyleCache.get(fileName);
|
|
1206
|
-
if (css)
|
|
1207
|
-
routeComponentStyles.push(css);
|
|
1208
|
-
}
|
|
1209
|
-
// Build the route module object
|
|
1210
|
-
const routeObj = buildRouteObject({
|
|
1211
|
-
index: i,
|
|
226
|
+
const routeState = assembleRouteState({
|
|
227
|
+
parsed,
|
|
228
|
+
fullPath,
|
|
229
|
+
routesDir,
|
|
230
|
+
layoutRelativePaths: rf.layouts,
|
|
231
|
+
fileContents,
|
|
232
|
+
});
|
|
233
|
+
compiledRoutes.push(compilePageRoute({
|
|
1212
234
|
pattern,
|
|
1213
|
-
|
|
235
|
+
routeIndex: i,
|
|
236
|
+
projectDir,
|
|
1214
237
|
isDev: !!options.isDev,
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
238
|
+
routeState,
|
|
239
|
+
componentCompiler,
|
|
240
|
+
clientModuleCompiler,
|
|
241
|
+
assetsPrefix,
|
|
242
|
+
resolveCompiledImportPath: serverModuleCompiler.resolveCompiledImportPath,
|
|
243
|
+
allocateModuleId: () => `__m${moduleCounter++}`,
|
|
244
|
+
pushImport: (statement) => allImports.push(statement),
|
|
245
|
+
}));
|
|
1221
246
|
}
|
|
1222
247
|
// Scan src/assets/ for static files to embed (recursive)
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
'.css': 'text/css; charset=utf-8',
|
|
1228
|
-
'.js': 'text/javascript; charset=utf-8',
|
|
1229
|
-
'.json': 'application/json; charset=utf-8',
|
|
1230
|
-
'.svg': 'image/svg+xml',
|
|
1231
|
-
'.txt': 'text/plain; charset=utf-8',
|
|
1232
|
-
};
|
|
1233
|
-
const scanAssets = (dir, prefix) => {
|
|
1234
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1235
|
-
if (entry.isDirectory()) {
|
|
1236
|
-
scanAssets(path.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
|
|
1237
|
-
continue;
|
|
1238
|
-
}
|
|
1239
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
1240
|
-
const mime = mimeTypes[ext];
|
|
1241
|
-
if (!mime)
|
|
1242
|
-
continue;
|
|
1243
|
-
const content = fs.readFileSync(path.join(dir, entry.name), 'utf-8');
|
|
1244
|
-
const etag = '"' + crypto.createHash('md5').update(content).digest('hex').slice(0, 12) + '"';
|
|
1245
|
-
const name = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1246
|
-
compiledAssets.push({ name, content, mime, etag });
|
|
1247
|
-
}
|
|
1248
|
-
};
|
|
1249
|
-
scanAssets(assetsDir, '');
|
|
1250
|
-
}
|
|
248
|
+
const compiledAssets = [
|
|
249
|
+
...compileAssets(path.join(srcDir, 'assets')),
|
|
250
|
+
...clientModuleCompiler.getCompiledAssets(),
|
|
251
|
+
];
|
|
1251
252
|
// Collect only the components that were actually imported by routes
|
|
1252
|
-
const compiledComponents =
|
|
253
|
+
const compiledComponents = componentCompiler.getCompiledComponents();
|
|
1253
254
|
// Generate the routes module
|
|
1254
|
-
const rawRuntimeImportPath =
|
|
255
|
+
const rawRuntimeImportPath = resolveRuntimeImportPathPipeline(projectDir);
|
|
1255
256
|
let runtimeImportPath;
|
|
1256
257
|
if (rawRuntimeImportPath) {
|
|
1257
|
-
// Resolve the runtime file's absolute path and pass it through
|
|
258
|
+
// Resolve the runtime file's absolute path and pass it through the server module compiler
|
|
1258
259
|
// so that $durable-objects/* and other project imports get rewritten to their proxies.
|
|
1259
260
|
const runtimeAbs = path.resolve(path.join(projectDir, '.kuratchi'), rawRuntimeImportPath);
|
|
1260
|
-
const transformedRuntimePath =
|
|
1261
|
-
const outFile = options.outFile ?? path.join(projectDir, '.kuratchi', 'routes.
|
|
1262
|
-
runtimeImportPath = toModuleSpecifier(outFile, transformedRuntimePath);
|
|
261
|
+
const transformedRuntimePath = serverModuleCompiler.transformModule(runtimeAbs);
|
|
262
|
+
const outFile = options.outFile ?? path.join(projectDir, '.kuratchi', 'routes.ts');
|
|
263
|
+
runtimeImportPath = serverModuleCompiler.toModuleSpecifier(outFile, transformedRuntimePath);
|
|
1263
264
|
}
|
|
1264
265
|
const hasRuntime = !!runtimeImportPath;
|
|
1265
|
-
const output =
|
|
266
|
+
const output = generateRoutesModulePipeline({
|
|
1266
267
|
projectDir,
|
|
1267
268
|
serverImports: allImports,
|
|
1268
269
|
compiledRoutes,
|
|
@@ -1272,6 +273,7 @@ export function compile(options) {
|
|
|
1272
273
|
compiledErrorPages,
|
|
1273
274
|
ormDatabases,
|
|
1274
275
|
authConfig,
|
|
276
|
+
securityConfig,
|
|
1275
277
|
doConfig,
|
|
1276
278
|
doHandlers,
|
|
1277
279
|
workflowConfig,
|
|
@@ -1281,44 +283,41 @@ export function compile(options) {
|
|
|
1281
283
|
hasRuntime,
|
|
1282
284
|
runtimeImportPath,
|
|
1283
285
|
assetsPrefix,
|
|
286
|
+
runtimeContextImport: RUNTIME_CONTEXT_IMPORT,
|
|
287
|
+
runtimeDoImport: RUNTIME_DO_IMPORT,
|
|
288
|
+
runtimeWorkerImport: RUNTIME_WORKER_IMPORT,
|
|
1284
289
|
});
|
|
1285
|
-
// Write to .kuratchi/routes.
|
|
1286
|
-
const outFile = options.outFile ?? path.join(projectDir, '.kuratchi', 'routes.
|
|
290
|
+
// Write to .kuratchi/routes.ts
|
|
291
|
+
const outFile = options.outFile ?? path.join(projectDir, '.kuratchi', 'routes.ts');
|
|
1287
292
|
const outDir = path.dirname(outFile);
|
|
1288
293
|
if (!fs.existsSync(outDir)) {
|
|
1289
294
|
fs.mkdirSync(outDir, { recursive: true });
|
|
1290
295
|
}
|
|
1291
296
|
writeIfChanged(outFile, output);
|
|
1292
|
-
// Generate .kuratchi/worker.
|
|
1293
|
-
// routes.
|
|
1294
|
-
// worker.
|
|
1295
|
-
// stable filename while routes.
|
|
1296
|
-
const workerFile = path.join(outDir, 'worker.
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
return `export { ${entry.className} } from '${importPath}';`;
|
|
1304
|
-
});
|
|
1305
|
-
const workerLines = [
|
|
1306
|
-
'// Auto-generated by kuratchi \u2014 do not edit.',
|
|
1307
|
-
"export { default } from './routes.js';",
|
|
1308
|
-
...doConfig.map(c => `export { ${c.className} } from './routes.js';`),
|
|
1309
|
-
...workerClassExports,
|
|
1310
|
-
'',
|
|
1311
|
-
];
|
|
1312
|
-
writeIfChanged(workerFile, workerLines.join('\n'));
|
|
297
|
+
// Generate .kuratchi/worker.ts — the stable wrangler entry point.
|
|
298
|
+
// routes.ts already exports the default fetch handler and all named DO classes;
|
|
299
|
+
// worker.ts explicitly re-exports them so wrangler.jsonc can reference a
|
|
300
|
+
// stable filename while routes.ts is freely regenerated.
|
|
301
|
+
const workerFile = path.join(outDir, 'worker.ts');
|
|
302
|
+
writeIfChanged(workerFile, buildWorkerEntrypointSource({
|
|
303
|
+
projectDir,
|
|
304
|
+
outDir,
|
|
305
|
+
doClassNames: doConfig.map((entry) => entry.className),
|
|
306
|
+
workerClassEntries: [...agentConfig, ...containerConfig, ...workflowConfig],
|
|
307
|
+
}));
|
|
1313
308
|
// Auto-sync wrangler.jsonc with workflow/container/DO config from kuratchi.config.ts
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
309
|
+
syncWranglerConfigPipeline({
|
|
310
|
+
projectDir,
|
|
311
|
+
config: {
|
|
312
|
+
workflows: workflowConfig,
|
|
313
|
+
containers: containerConfig,
|
|
314
|
+
durableObjects: doConfig,
|
|
315
|
+
},
|
|
316
|
+
writeFile: writeIfChanged,
|
|
1318
317
|
});
|
|
1319
318
|
return workerFile;
|
|
1320
319
|
}
|
|
1321
|
-
//
|
|
320
|
+
// ?"??"? Helpers ?"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"??"?
|
|
1322
321
|
/**
|
|
1323
322
|
* Write a file only if its content has changed.
|
|
1324
323
|
* Prevents unnecessary filesystem events that would retrigger wrangler's file watcher.
|
|
@@ -1331,2142 +330,3 @@ function writeIfChanged(filePath, content) {
|
|
|
1331
330
|
}
|
|
1332
331
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
1333
332
|
}
|
|
1334
|
-
function skipWhitespace(source, start) {
|
|
1335
|
-
let i = start;
|
|
1336
|
-
while (i < source.length && /\s/.test(source[i]))
|
|
1337
|
-
i++;
|
|
1338
|
-
return i;
|
|
1339
|
-
}
|
|
1340
|
-
function extractBalancedBody(source, start, openChar, closeChar) {
|
|
1341
|
-
if (source[start] !== openChar)
|
|
1342
|
-
return null;
|
|
1343
|
-
let depth = 0;
|
|
1344
|
-
for (let i = start; i < source.length; i++) {
|
|
1345
|
-
if (source[i] === openChar)
|
|
1346
|
-
depth++;
|
|
1347
|
-
else if (source[i] === closeChar) {
|
|
1348
|
-
depth--;
|
|
1349
|
-
if (depth === 0)
|
|
1350
|
-
return source.slice(start + 1, i);
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
return null;
|
|
1354
|
-
}
|
|
1355
|
-
function readConfigBlock(source, key) {
|
|
1356
|
-
const keyRegex = new RegExp(`\\b${key}\\s*:`);
|
|
1357
|
-
const keyMatch = keyRegex.exec(source);
|
|
1358
|
-
if (!keyMatch)
|
|
1359
|
-
return null;
|
|
1360
|
-
const colonIdx = source.indexOf(':', keyMatch.index);
|
|
1361
|
-
if (colonIdx === -1)
|
|
1362
|
-
return null;
|
|
1363
|
-
const valueIdx = skipWhitespace(source, colonIdx + 1);
|
|
1364
|
-
if (valueIdx >= source.length)
|
|
1365
|
-
return null;
|
|
1366
|
-
if (source[valueIdx] === '{') {
|
|
1367
|
-
throw new Error(`[kuratchi] "${key}" config must use an adapter call (e.g. ${key}: kuratchi${key[0].toUpperCase()}${key.slice(1)}Config({...})).`);
|
|
1368
|
-
}
|
|
1369
|
-
const callOpen = source.indexOf('(', valueIdx);
|
|
1370
|
-
if (callOpen === -1)
|
|
1371
|
-
return null;
|
|
1372
|
-
const argIdx = skipWhitespace(source, callOpen + 1);
|
|
1373
|
-
if (argIdx >= source.length)
|
|
1374
|
-
return null;
|
|
1375
|
-
if (source[argIdx] === ')')
|
|
1376
|
-
return { kind: 'call-empty', body: '' };
|
|
1377
|
-
if (source[argIdx] === '{') {
|
|
1378
|
-
const body = extractBalancedBody(source, argIdx, '{', '}');
|
|
1379
|
-
if (body == null)
|
|
1380
|
-
return null;
|
|
1381
|
-
return { kind: 'call-object', body };
|
|
1382
|
-
}
|
|
1383
|
-
return { kind: 'call-empty', body: '' };
|
|
1384
|
-
}
|
|
1385
|
-
/**
|
|
1386
|
-
* Read ui.theme from kuratchi.config.ts and return the theme CSS content.
|
|
1387
|
-
* Returns null if no theme is configured.
|
|
1388
|
-
*/
|
|
1389
|
-
function readUiTheme(projectDir) {
|
|
1390
|
-
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
1391
|
-
if (!fs.existsSync(configPath))
|
|
1392
|
-
return null;
|
|
1393
|
-
const source = fs.readFileSync(configPath, 'utf-8');
|
|
1394
|
-
const uiBlock = readConfigBlock(source, 'ui');
|
|
1395
|
-
if (!uiBlock)
|
|
1396
|
-
return null;
|
|
1397
|
-
// Adapter form defaults to the bundled Kuratchi UI theme when ui config is present.
|
|
1398
|
-
const themeMatch = uiBlock.body.match(/theme\s*:\s*['"]([^'"]+)['"]/);
|
|
1399
|
-
const themeValue = themeMatch?.[1] ?? 'default';
|
|
1400
|
-
if (themeValue === 'default' || themeValue === 'dark' || themeValue === 'light' || themeValue === 'system') {
|
|
1401
|
-
// Resolve @kuratchi/ui/src/styles/theme.css from package
|
|
1402
|
-
const candidates = [
|
|
1403
|
-
path.join(projectDir, 'node_modules', '@kuratchi/ui', 'src', 'styles', 'theme.css'),
|
|
1404
|
-
path.join(path.resolve(projectDir, '../..'), 'packages', 'kuratchi-ui', 'src', 'styles', 'theme.css'),
|
|
1405
|
-
path.join(path.resolve(projectDir, '../..'), 'node_modules', '@kuratchi/ui', 'src', 'styles', 'theme.css'),
|
|
1406
|
-
];
|
|
1407
|
-
for (const candidate of candidates) {
|
|
1408
|
-
if (fs.existsSync(candidate)) {
|
|
1409
|
-
return fs.readFileSync(candidate, 'utf-8');
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
console.warn(`[kuratchi] ui.theme: "${themeValue}" configured but @kuratchi/ui theme.css not found`);
|
|
1413
|
-
return null;
|
|
1414
|
-
}
|
|
1415
|
-
// Custom path �" resolve relative to project root
|
|
1416
|
-
const customPath = path.resolve(projectDir, themeValue);
|
|
1417
|
-
if (fs.existsSync(customPath)) {
|
|
1418
|
-
return fs.readFileSync(customPath, 'utf-8');
|
|
1419
|
-
}
|
|
1420
|
-
console.warn(`[kuratchi] ui.theme: "${themeValue}" not found at ${customPath}`);
|
|
1421
|
-
return null;
|
|
1422
|
-
}
|
|
1423
|
-
/**
|
|
1424
|
-
* Read ui.theme and ui.radius config values from kuratchi.config.ts.
|
|
1425
|
-
* Returns null if no ui block is present.
|
|
1426
|
-
*/
|
|
1427
|
-
function readUiConfigValues(projectDir) {
|
|
1428
|
-
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
1429
|
-
if (!fs.existsSync(configPath))
|
|
1430
|
-
return null;
|
|
1431
|
-
const source = fs.readFileSync(configPath, 'utf-8');
|
|
1432
|
-
const uiBlock = readConfigBlock(source, 'ui');
|
|
1433
|
-
if (!uiBlock)
|
|
1434
|
-
return null;
|
|
1435
|
-
const themeMatch = uiBlock.body.match(/theme\s*:\s*['"]([^'"]+)['"]/);
|
|
1436
|
-
const radiusMatch = uiBlock.body.match(/radius\s*:\s*['"]([^'"]+)['"]/);
|
|
1437
|
-
return {
|
|
1438
|
-
theme: themeMatch?.[1] ?? 'dark',
|
|
1439
|
-
radius: radiusMatch?.[1] ?? 'default',
|
|
1440
|
-
};
|
|
1441
|
-
}
|
|
1442
|
-
/**
|
|
1443
|
-
* Patch the opening <html> tag in a layout source string to reflect ui config.
|
|
1444
|
-
* theme='dark' ? ensures class="dark" is present, removes data-theme.
|
|
1445
|
-
* theme='light' ? ensures class="dark" is absent, removes data-theme.
|
|
1446
|
-
* theme='system' ? removes class="dark", sets data-theme="system".
|
|
1447
|
-
* radius='none'|'full' ? sets data-radius; radius='default' ? removes it.
|
|
1448
|
-
*/
|
|
1449
|
-
function patchHtmlTag(source, theme, radius) {
|
|
1450
|
-
return source.replace(/(<html\b)([^>]*)(>)/i, (_m, open, attrs, close) => {
|
|
1451
|
-
if (theme === 'dark') {
|
|
1452
|
-
if (/\bclass\s*=\s*"([^"]*)"/i.test(attrs)) {
|
|
1453
|
-
attrs = attrs.replace(/class\s*=\s*"([^"]*)"/i, (_mc, cls) => {
|
|
1454
|
-
const classes = cls.split(/\s+/).filter(Boolean);
|
|
1455
|
-
if (!classes.includes('dark'))
|
|
1456
|
-
classes.unshift('dark');
|
|
1457
|
-
return `class="${classes.join(' ')}"`;
|
|
1458
|
-
});
|
|
1459
|
-
}
|
|
1460
|
-
else {
|
|
1461
|
-
attrs += ' class="dark"';
|
|
1462
|
-
}
|
|
1463
|
-
attrs = attrs.replace(/\s*data-theme\s*=\s*"[^"]*"/i, '');
|
|
1464
|
-
}
|
|
1465
|
-
else if (theme === 'light') {
|
|
1466
|
-
attrs = attrs.replace(/class\s*=\s*"([^"]*)"/i, (_mc, cls) => {
|
|
1467
|
-
const classes = cls.split(/\s+/).filter(Boolean).filter((c) => c !== 'dark');
|
|
1468
|
-
return classes.length ? `class="${classes.join(' ')}"` : '';
|
|
1469
|
-
});
|
|
1470
|
-
attrs = attrs.replace(/\s*data-theme\s*=\s*"[^"]*"/i, '');
|
|
1471
|
-
}
|
|
1472
|
-
else if (theme === 'system') {
|
|
1473
|
-
attrs = attrs.replace(/class\s*=\s*"([^"]*)"/i, (_mc, cls) => {
|
|
1474
|
-
const classes = cls.split(/\s+/).filter(Boolean).filter((c) => c !== 'dark');
|
|
1475
|
-
return classes.length ? `class="${classes.join(' ')}"` : '';
|
|
1476
|
-
});
|
|
1477
|
-
if (/data-theme\s*=/i.test(attrs)) {
|
|
1478
|
-
attrs = attrs.replace(/data-theme\s*=\s*"[^"]*"/i, 'data-theme="system"');
|
|
1479
|
-
}
|
|
1480
|
-
else {
|
|
1481
|
-
attrs += ' data-theme="system"';
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
attrs = attrs.replace(/\s*data-radius\s*=\s*"[^"]*"/i, '');
|
|
1485
|
-
if (radius === 'none' || radius === 'full') {
|
|
1486
|
-
attrs += ` data-radius="${radius}"`;
|
|
1487
|
-
}
|
|
1488
|
-
return open + attrs + close;
|
|
1489
|
-
});
|
|
1490
|
-
}
|
|
1491
|
-
/**
|
|
1492
|
-
* Resolve a component .html file from a package (e.g. @kuratchi/ui).
|
|
1493
|
-
* Searches: node_modules, then workspace siblings (../../packages/).
|
|
1494
|
-
*/
|
|
1495
|
-
function resolvePackageComponent(projectDir, pkgName, componentFile) {
|
|
1496
|
-
// 1. Try node_modules (standard resolution)
|
|
1497
|
-
const nmPath = path.join(projectDir, 'node_modules', pkgName, 'src', 'lib', componentFile + '.html');
|
|
1498
|
-
if (fs.existsSync(nmPath))
|
|
1499
|
-
return nmPath;
|
|
1500
|
-
// 2. Try workspace layout: project is in apps/X or packages/X, sibling packages in packages/
|
|
1501
|
-
// @kuratchi/ui �' kuratchi-ui (convention: scope stripped, slash �' dash)
|
|
1502
|
-
const pkgDirName = pkgName.replace(/^@/, '').replace(/\//g, '-');
|
|
1503
|
-
const workspaceRoot = path.resolve(projectDir, '../..');
|
|
1504
|
-
const wsPath = path.join(workspaceRoot, 'packages', pkgDirName, 'src', 'lib', componentFile + '.html');
|
|
1505
|
-
if (fs.existsSync(wsPath))
|
|
1506
|
-
return wsPath;
|
|
1507
|
-
// 3. Try one level up (monorepo root node_modules)
|
|
1508
|
-
const rootNmPath = path.join(workspaceRoot, 'node_modules', pkgName, 'src', 'lib', componentFile + '.html');
|
|
1509
|
-
if (fs.existsSync(rootNmPath))
|
|
1510
|
-
return rootNmPath;
|
|
1511
|
-
return '';
|
|
1512
|
-
}
|
|
1513
|
-
function discoverRoutes(routesDir) {
|
|
1514
|
-
const results = [];
|
|
1515
|
-
const registered = new Set();
|
|
1516
|
-
function getLayoutsForPrefix(prefix) {
|
|
1517
|
-
const layouts = [];
|
|
1518
|
-
if (fs.existsSync(path.join(routesDir, 'layout.html')))
|
|
1519
|
-
layouts.push('layout.html');
|
|
1520
|
-
if (!prefix)
|
|
1521
|
-
return layouts;
|
|
1522
|
-
const parts = prefix.split('/').filter(Boolean);
|
|
1523
|
-
let current = '';
|
|
1524
|
-
for (const part of parts) {
|
|
1525
|
-
current = current ? `${current}/${part}` : part;
|
|
1526
|
-
const rel = `${current}/layout.html`;
|
|
1527
|
-
if (fs.existsSync(path.join(routesDir, rel)))
|
|
1528
|
-
layouts.push(rel);
|
|
1529
|
-
}
|
|
1530
|
-
return layouts;
|
|
1531
|
-
}
|
|
1532
|
-
function walk(dir, prefix) {
|
|
1533
|
-
if (!fs.existsSync(dir))
|
|
1534
|
-
return;
|
|
1535
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1536
|
-
for (const entry of entries) {
|
|
1537
|
-
if (entry.isDirectory()) {
|
|
1538
|
-
const childPrefix = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1539
|
-
// Folder-based page route: routes/db/page.html ? /db
|
|
1540
|
-
const pageFile = path.join(dir, entry.name, 'page.html');
|
|
1541
|
-
if (fs.existsSync(pageFile)) {
|
|
1542
|
-
const routeFile = `${childPrefix}/page.html`;
|
|
1543
|
-
if (!registered.has(routeFile)) {
|
|
1544
|
-
registered.add(routeFile);
|
|
1545
|
-
results.push({ file: routeFile, name: childPrefix, layouts: getLayoutsForPrefix(childPrefix), type: 'page' });
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
// Folder-based API route: routes/api/v1/health/index.ts -> /api/v1/health
|
|
1549
|
-
const apiFile = ['index.ts', 'index.js'].find(f => fs.existsSync(path.join(dir, entry.name, f)));
|
|
1550
|
-
if (apiFile && !fs.existsSync(pageFile)) {
|
|
1551
|
-
const routeFile = `${childPrefix}/${apiFile}`;
|
|
1552
|
-
if (!registered.has(routeFile)) {
|
|
1553
|
-
registered.add(routeFile);
|
|
1554
|
-
results.push({ file: routeFile, name: childPrefix, layouts: [], type: 'api' });
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
// Always recurse into subdirectory (for nested routes like /admin/roles)
|
|
1558
|
-
walk(path.join(dir, entry.name), childPrefix);
|
|
1559
|
-
}
|
|
1560
|
-
else if (entry.name === 'layout.html' || entry.name === '404.html' || entry.name === '500.html') {
|
|
1561
|
-
// Skip � layout.html is the app layout, 404/500 are error pages, not routes
|
|
1562
|
-
continue;
|
|
1563
|
-
}
|
|
1564
|
-
else if (entry.name === 'index.ts' || entry.name === 'index.js') {
|
|
1565
|
-
// API route file in current directory -> index API route for this prefix
|
|
1566
|
-
const routeFile = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1567
|
-
if (!registered.has(routeFile)) {
|
|
1568
|
-
registered.add(routeFile);
|
|
1569
|
-
results.push({ file: routeFile, name: prefix || 'index', layouts: [], type: 'api' });
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
else if (entry.name === 'page.html') {
|
|
1573
|
-
// page.html in current directory ? index route for this prefix
|
|
1574
|
-
const routeFile = prefix ? `${prefix}/page.html` : 'page.html';
|
|
1575
|
-
if (!registered.has(routeFile)) {
|
|
1576
|
-
registered.add(routeFile);
|
|
1577
|
-
results.push({ file: routeFile, name: prefix || 'index', layouts: getLayoutsForPrefix(prefix), type: 'page' });
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
else if (entry.name.endsWith('.html') && entry.name !== 'page.html') {
|
|
1581
|
-
// File-based route: routes/about.html ? /about (fallback)
|
|
1582
|
-
const name = prefix
|
|
1583
|
-
? `${prefix}/${entry.name.replace('.html', '')}`
|
|
1584
|
-
: entry.name.replace('.html', '');
|
|
1585
|
-
results.push({
|
|
1586
|
-
file: prefix ? `${prefix}/${entry.name}` : entry.name,
|
|
1587
|
-
name,
|
|
1588
|
-
layouts: getLayoutsForPrefix(prefix),
|
|
1589
|
-
type: 'page',
|
|
1590
|
-
});
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
walk(routesDir, '');
|
|
1595
|
-
// Sort: static routes first, then dynamic, then catch-all
|
|
1596
|
-
results.sort((a, b) => {
|
|
1597
|
-
const aScore = a.name.includes('[...') ? 2 : a.name.includes('[') ? 1 : 0;
|
|
1598
|
-
const bScore = b.name.includes('[...') ? 2 : b.name.includes('[') ? 1 : 0;
|
|
1599
|
-
return aScore - bScore || a.name.localeCompare(b.name);
|
|
1600
|
-
});
|
|
1601
|
-
return results;
|
|
1602
|
-
}
|
|
1603
|
-
function buildSegmentedScriptBody(opts) {
|
|
1604
|
-
const { segments, fnToModule, importDecls, workerEnvAliases, devAliases, isDev, asyncMode } = opts;
|
|
1605
|
-
const lines = [];
|
|
1606
|
-
const routeDevDecls = buildDevAliasDeclarations(devAliases, isDev);
|
|
1607
|
-
if (routeDevDecls)
|
|
1608
|
-
lines.push(routeDevDecls);
|
|
1609
|
-
if (importDecls)
|
|
1610
|
-
lines.push(importDecls);
|
|
1611
|
-
lines.push('const __segmentData: Record<string, any> = {};');
|
|
1612
|
-
const availableVars = [];
|
|
1613
|
-
let segmentIndex = 0;
|
|
1614
|
-
for (const segment of segments) {
|
|
1615
|
-
if (!segment.script)
|
|
1616
|
-
continue;
|
|
1617
|
-
let segmentBody = stripTopLevelImports(segment.script);
|
|
1618
|
-
segmentBody = rewriteImportedFunctionCalls(segmentBody, fnToModule);
|
|
1619
|
-
segmentBody = rewriteWorkerEnvAliases(segmentBody, workerEnvAliases);
|
|
1620
|
-
if (!segmentBody.trim())
|
|
1621
|
-
continue;
|
|
1622
|
-
const returnVars = segment.dataVars.filter((name) => /^[A-Za-z_$][\w$]*$/.test(name));
|
|
1623
|
-
const segmentVar = '__segment_' + segmentIndex++;
|
|
1624
|
-
const invokePrefix = asyncMode ? 'await ' : '';
|
|
1625
|
-
const factoryPrefix = asyncMode ? 'async ' : '';
|
|
1626
|
-
lines.push('const ' + segmentVar + ' = ' + invokePrefix + '(' + factoryPrefix + '(__ctx: Record<string, any>) => {');
|
|
1627
|
-
lines.push(segmentBody);
|
|
1628
|
-
lines.push(returnVars.length > 0 ? 'return { ' + returnVars.join(', ') + ' };' : 'return {};');
|
|
1629
|
-
lines.push('})(__segmentData);');
|
|
1630
|
-
lines.push('Object.assign(__segmentData, ' + segmentVar + ');');
|
|
1631
|
-
for (const name of returnVars) {
|
|
1632
|
-
if (!availableVars.includes(name))
|
|
1633
|
-
availableVars.push(name);
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
if (!asyncMode && availableVars.length > 0) {
|
|
1637
|
-
lines.push('const { ' + availableVars.join(', ') + ' } = __segmentData;');
|
|
1638
|
-
}
|
|
1639
|
-
return lines.join('\n');
|
|
1640
|
-
}
|
|
1641
|
-
function buildRouteObject(opts) {
|
|
1642
|
-
const { pattern, renderBody, isDev, parsed, fnToModule, rpcNameMap, componentStyles } = opts;
|
|
1643
|
-
const hasFns = Object.keys(fnToModule).length > 0;
|
|
1644
|
-
const parts = [];
|
|
1645
|
-
parts.push(` pattern: '${pattern}'`);
|
|
1646
|
-
const queryVars = parsed.dataGetQueries?.map((q) => q.asName) ?? [];
|
|
1647
|
-
const scriptSegments = (parsed.scriptSegments ?? [])
|
|
1648
|
-
.filter((segment) => !!segment.script);
|
|
1649
|
-
const hasSegmentedScripts = scriptSegments.length > 1;
|
|
1650
|
-
const routeDevDecls = buildDevAliasDeclarations(parsed.devAliases, isDev);
|
|
1651
|
-
const routeImportDecls = (parsed.scriptImportDecls ?? []).join('\n');
|
|
1652
|
-
let scriptBody = '';
|
|
1653
|
-
let scriptUsesAwait = false;
|
|
1654
|
-
if (hasSegmentedScripts) {
|
|
1655
|
-
const combinedScript = scriptSegments.map((segment) => stripTopLevelImports(segment.script)).join('\n');
|
|
1656
|
-
scriptUsesAwait = /\bawait\b/.test(combinedScript);
|
|
1657
|
-
scriptBody = buildSegmentedScriptBody({
|
|
1658
|
-
segments: scriptSegments,
|
|
1659
|
-
fnToModule,
|
|
1660
|
-
importDecls: routeImportDecls,
|
|
1661
|
-
workerEnvAliases: parsed.workerEnvAliases,
|
|
1662
|
-
devAliases: parsed.devAliases,
|
|
1663
|
-
isDev,
|
|
1664
|
-
asyncMode: scriptUsesAwait,
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
else {
|
|
1668
|
-
scriptBody = parsed.script
|
|
1669
|
-
? stripTopLevelImports(parsed.script)
|
|
1670
|
-
: '';
|
|
1671
|
-
scriptBody = [routeDevDecls, routeImportDecls, scriptBody].filter(Boolean).join('\n');
|
|
1672
|
-
scriptBody = rewriteImportedFunctionCalls(scriptBody, fnToModule);
|
|
1673
|
-
scriptBody = rewriteWorkerEnvAliases(scriptBody, parsed.workerEnvAliases);
|
|
1674
|
-
scriptUsesAwait = /\bawait\b/.test(scriptBody);
|
|
1675
|
-
}
|
|
1676
|
-
let explicitLoadFunction = parsed.loadFunction
|
|
1677
|
-
? parsed.loadFunction.replace(/^export\s+/, '').trim()
|
|
1678
|
-
: '';
|
|
1679
|
-
if (explicitLoadFunction) {
|
|
1680
|
-
explicitLoadFunction = [routeDevDecls, explicitLoadFunction].filter(Boolean).join('\n');
|
|
1681
|
-
explicitLoadFunction = rewriteImportedFunctionCalls(explicitLoadFunction, fnToModule);
|
|
1682
|
-
explicitLoadFunction = rewriteWorkerEnvAliases(explicitLoadFunction, parsed.workerEnvAliases);
|
|
1683
|
-
if (routeImportDecls)
|
|
1684
|
-
explicitLoadFunction = explicitLoadFunction.replace('{', `{\n${routeImportDecls}\n`);
|
|
1685
|
-
}
|
|
1686
|
-
if (explicitLoadFunction && scriptUsesAwait) {
|
|
1687
|
-
throw new Error(`[kuratchi compiler] ${pattern}\nTop-level await cannot be mixed with export async function load(). Move async server work into load().`);
|
|
1688
|
-
}
|
|
1689
|
-
if (scriptBody) {
|
|
1690
|
-
scriptBody = transpileTypeScript(scriptBody, `route-script:${pattern}.ts`);
|
|
1691
|
-
}
|
|
1692
|
-
if (explicitLoadFunction) {
|
|
1693
|
-
explicitLoadFunction = transpileTypeScript(explicitLoadFunction, `route-load:${pattern}.ts`);
|
|
1694
|
-
}
|
|
1695
|
-
const scriptReturnVars = parsed.script
|
|
1696
|
-
? parsed.dataVars.filter((v) => !queryVars.includes(v) &&
|
|
1697
|
-
!parsed.actionFunctions.includes(v) &&
|
|
1698
|
-
!parsed.pollFunctions.includes(v))
|
|
1699
|
-
: [];
|
|
1700
|
-
// Load function �" internal server prepass for async route script bodies
|
|
1701
|
-
// and data-get query state hydration.
|
|
1702
|
-
const hasDataGetQueries = Array.isArray(parsed.dataGetQueries) && parsed.dataGetQueries.length > 0;
|
|
1703
|
-
if (explicitLoadFunction) {
|
|
1704
|
-
parts.push(` load: ${explicitLoadFunction}`);
|
|
1705
|
-
}
|
|
1706
|
-
else if ((scriptBody && scriptUsesAwait) || hasDataGetQueries) {
|
|
1707
|
-
let loadBody = '';
|
|
1708
|
-
if (scriptBody && scriptUsesAwait) {
|
|
1709
|
-
loadBody = scriptBody;
|
|
1710
|
-
}
|
|
1711
|
-
// Inject data-get query state blocks into load scope.
|
|
1712
|
-
// Each query exposes:
|
|
1713
|
-
// { state, loading, error, data, empty, success }
|
|
1714
|
-
const queries = parsed.dataGetQueries;
|
|
1715
|
-
if (hasDataGetQueries) {
|
|
1716
|
-
const queryLines = [];
|
|
1717
|
-
for (const q of queries) {
|
|
1718
|
-
const fnName = q.fnName;
|
|
1719
|
-
const rpcId = q.rpcId || rpcNameMap?.get(fnName) || fnName;
|
|
1720
|
-
const argsExpr = (q.argsExpr || '').trim();
|
|
1721
|
-
const asName = q.asName;
|
|
1722
|
-
const defaultArgs = argsExpr ? `[${argsExpr}]` : '[]';
|
|
1723
|
-
const moduleId = fnToModule[fnName];
|
|
1724
|
-
const qualifiedFn = moduleId ? `${moduleId}.${fnName}` : fnName;
|
|
1725
|
-
queryLines.push(`let ${asName} = { state: 'loading', loading: true, error: null, data: null, empty: false, success: false };`);
|
|
1726
|
-
queryLines.push(`const __qOverride_${asName} = __getLocals().__queryOverride;`);
|
|
1727
|
-
queryLines.push(`const __qArgs_${asName} = ${defaultArgs};`);
|
|
1728
|
-
queryLines.push(`const __qShouldRun_${asName} = !!(__qOverride_${asName} && __qOverride_${asName}.fn === '${rpcId}' && Array.isArray(__qOverride_${asName}.args) && JSON.stringify(__qOverride_${asName}.args) === JSON.stringify(__qArgs_${asName}));`);
|
|
1729
|
-
queryLines.push(`if (__qShouldRun_${asName}) {`);
|
|
1730
|
-
queryLines.push(` try {`);
|
|
1731
|
-
queryLines.push(` const __qData_${asName} = await ${qualifiedFn}(...__qArgs_${asName});`);
|
|
1732
|
-
queryLines.push(` const __qEmpty_${asName} = Array.isArray(__qData_${asName}) ? __qData_${asName}.length === 0 : (__qData_${asName} == null);`);
|
|
1733
|
-
queryLines.push(` ${asName} = { state: __qEmpty_${asName} ? 'empty' : 'success', loading: false, error: null, data: __qData_${asName}, empty: __qEmpty_${asName}, success: !__qEmpty_${asName} };`);
|
|
1734
|
-
queryLines.push(` } catch (err) {`);
|
|
1735
|
-
queryLines.push(` const __qErr_${asName} = (err && err.message) ? String(err.message) : String(err);`);
|
|
1736
|
-
queryLines.push(` ${asName} = { state: 'error', loading: false, error: __qErr_${asName}, data: null, empty: false, success: false };`);
|
|
1737
|
-
queryLines.push(` }`);
|
|
1738
|
-
queryLines.push(`}`);
|
|
1739
|
-
}
|
|
1740
|
-
loadBody = [loadBody, queryLines.join('\n')].filter(Boolean).join('\n');
|
|
1741
|
-
}
|
|
1742
|
-
const loadReturnVars = [...scriptReturnVars, ...queryVars];
|
|
1743
|
-
let returnObj = '';
|
|
1744
|
-
if (loadReturnVars.length > 0) {
|
|
1745
|
-
if (hasSegmentedScripts && scriptUsesAwait) {
|
|
1746
|
-
const segmentReturnEntries = scriptReturnVars.map((name) => name + ': __segmentData.' + name);
|
|
1747
|
-
const queryReturnEntries = queryVars
|
|
1748
|
-
.filter((name) => !scriptReturnVars.includes(name))
|
|
1749
|
-
.map((name) => name);
|
|
1750
|
-
returnObj = `
|
|
1751
|
-
return { ${[...segmentReturnEntries, ...queryReturnEntries].join(', ')} };`;
|
|
1752
|
-
}
|
|
1753
|
-
else {
|
|
1754
|
-
returnObj = `
|
|
1755
|
-
return { ${loadReturnVars.join(', ')} };`;
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
parts.push(` async load(__routeParams = {}) {
|
|
1759
|
-
${loadBody}${returnObj}
|
|
1760
|
-
}`);
|
|
1761
|
-
}
|
|
1762
|
-
// Actions �" functions referenced via action={fn} in the template
|
|
1763
|
-
if (hasFns && parsed.actionFunctions.length > 0) {
|
|
1764
|
-
const actionEntries = parsed.actionFunctions
|
|
1765
|
-
.map(fn => {
|
|
1766
|
-
const moduleId = fnToModule[fn];
|
|
1767
|
-
return moduleId ? `'${fn}': ${moduleId}.${fn}` : `'${fn}': ${fn}`;
|
|
1768
|
-
})
|
|
1769
|
-
.join(', ');
|
|
1770
|
-
parts.push(` actions: { ${actionEntries} }`);
|
|
1771
|
-
}
|
|
1772
|
-
// RPC �" functions referenced via data-poll={fn(args)} in the template
|
|
1773
|
-
if (hasFns && parsed.pollFunctions.length > 0) {
|
|
1774
|
-
const rpcEntries = parsed.pollFunctions
|
|
1775
|
-
.map(fn => {
|
|
1776
|
-
const moduleId = fnToModule[fn];
|
|
1777
|
-
const rpcId = rpcNameMap?.get(fn) || fn;
|
|
1778
|
-
return moduleId ? `'${rpcId}': ${moduleId}.${fn}` : `'${rpcId}': ${fn}`;
|
|
1779
|
-
})
|
|
1780
|
-
.join(', ');
|
|
1781
|
-
parts.push(` rpc: { ${rpcEntries} }`);
|
|
1782
|
-
}
|
|
1783
|
-
// Render function �" template compiled to JS with native flow control
|
|
1784
|
-
// Destructure data vars so templates reference them directly (e.g., {todos} not {data.todos})
|
|
1785
|
-
// Auto-inject action state objects so templates can reference signIn.error, signIn.loading, etc.
|
|
1786
|
-
const renderPrelude = (scriptBody && !scriptUsesAwait) ? scriptBody : '';
|
|
1787
|
-
const allVars = [...queryVars];
|
|
1788
|
-
if (scriptUsesAwait) {
|
|
1789
|
-
for (const v of scriptReturnVars) {
|
|
1790
|
-
if (!allVars.includes(v))
|
|
1791
|
-
allVars.push(v);
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
for (const fn of parsed.actionFunctions) {
|
|
1795
|
-
if (!allVars.includes(fn))
|
|
1796
|
-
allVars.push(fn);
|
|
1797
|
-
}
|
|
1798
|
-
if (!allVars.includes('params'))
|
|
1799
|
-
allVars.push('params');
|
|
1800
|
-
if (!allVars.includes('breadcrumbs'))
|
|
1801
|
-
allVars.push('breadcrumbs');
|
|
1802
|
-
const destructure = `const { ${allVars.join(', ')} } = data;\n `;
|
|
1803
|
-
// Inject component CSS at compile time (once per route, no runtime dedup)
|
|
1804
|
-
// Must come after 'let __html = "";' (first line of renderBody)
|
|
1805
|
-
let finalRenderBody = renderBody;
|
|
1806
|
-
if (componentStyles.length > 0) {
|
|
1807
|
-
const lines = renderBody.split('\n');
|
|
1808
|
-
const styleLines = componentStyles.map(css => `__html += \`${css}\\n\`;`);
|
|
1809
|
-
finalRenderBody = [lines[0], ...styleLines, ...lines.slice(1)].join('\n');
|
|
1810
|
-
}
|
|
1811
|
-
parts.push(` render(data) {
|
|
1812
|
-
${destructure}${renderPrelude ? renderPrelude + '\n ' : ''}${finalRenderBody}
|
|
1813
|
-
return __html;
|
|
1814
|
-
}`);
|
|
1815
|
-
return ` {\n${parts.join(',\n')}\n }`;
|
|
1816
|
-
}
|
|
1817
|
-
function readOrmConfig(projectDir) {
|
|
1818
|
-
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
1819
|
-
if (!fs.existsSync(configPath))
|
|
1820
|
-
return [];
|
|
1821
|
-
const source = fs.readFileSync(configPath, 'utf-8');
|
|
1822
|
-
const ormBlock = readConfigBlock(source, 'orm');
|
|
1823
|
-
if (!ormBlock)
|
|
1824
|
-
return [];
|
|
1825
|
-
// Extract schema imports: import { todoSchema } from './src/schemas/todo';
|
|
1826
|
-
const importMap = new Map(); // exportName �' importPath
|
|
1827
|
-
const importRegex = /import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
1828
|
-
let m;
|
|
1829
|
-
while ((m = importRegex.exec(source)) !== null) {
|
|
1830
|
-
const names = m[1].split(',').map(n => n.trim()).filter(Boolean);
|
|
1831
|
-
const importPath = m[2];
|
|
1832
|
-
for (const name of names) {
|
|
1833
|
-
importMap.set(name, importPath);
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
const databasesIdx = ormBlock.body.search(/databases\s*:\s*\{/);
|
|
1837
|
-
if (databasesIdx === -1)
|
|
1838
|
-
return [];
|
|
1839
|
-
const dbBraceStart = ormBlock.body.indexOf('{', databasesIdx);
|
|
1840
|
-
if (dbBraceStart === -1)
|
|
1841
|
-
return [];
|
|
1842
|
-
const databasesBody = extractBalancedBody(ormBlock.body, dbBraceStart, '{', '}');
|
|
1843
|
-
if (databasesBody == null)
|
|
1844
|
-
return [];
|
|
1845
|
-
// Pattern: BINDING: { schema: schemaName, skipMigrations?: true/false }
|
|
1846
|
-
const entries = [];
|
|
1847
|
-
const entryRegex = /(\w+)\s*:\s*\{\s*schema\s*:\s*(\w+)([^}]*)\}/g;
|
|
1848
|
-
while ((m = entryRegex.exec(databasesBody)) !== null) {
|
|
1849
|
-
const binding = m[1];
|
|
1850
|
-
const schemaExportName = m[2];
|
|
1851
|
-
const rest = m[3] || '';
|
|
1852
|
-
const skipMatch = rest.match(/skipMigrations\s*:\s*(true|false)/);
|
|
1853
|
-
const skipMigrations = skipMatch?.[1] === 'true';
|
|
1854
|
-
const typeMatch = rest.match(/type\s*:\s*['"]?(d1|do)['"]?/);
|
|
1855
|
-
const type = typeMatch?.[1] ?? 'd1';
|
|
1856
|
-
// Only include if the schema name maps to a known import (not 'orm', 'databases', etc.)
|
|
1857
|
-
const schemaImportPath = importMap.get(schemaExportName);
|
|
1858
|
-
if (!schemaImportPath)
|
|
1859
|
-
continue;
|
|
1860
|
-
entries.push({ binding, schemaImportPath, schemaExportName, skipMigrations, type });
|
|
1861
|
-
}
|
|
1862
|
-
return entries;
|
|
1863
|
-
}
|
|
1864
|
-
function readAuthConfig(projectDir) {
|
|
1865
|
-
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
1866
|
-
if (!fs.existsSync(configPath))
|
|
1867
|
-
return null;
|
|
1868
|
-
const source = fs.readFileSync(configPath, 'utf-8');
|
|
1869
|
-
const authBlockMatch = readConfigBlock(source, 'auth');
|
|
1870
|
-
if (!authBlockMatch)
|
|
1871
|
-
return null;
|
|
1872
|
-
const authBlock = authBlockMatch.body;
|
|
1873
|
-
const cookieMatch = authBlock.match(/cookieName\s*:\s*['"]([^'"]+)['"]/);
|
|
1874
|
-
const secretMatch = authBlock.match(/secretEnvKey\s*:\s*['"]([^'"]+)['"]/);
|
|
1875
|
-
const sessionMatch = authBlock.match(/sessionEnabled\s*:\s*(true|false)/);
|
|
1876
|
-
// Detect sub-configs by looking for the key followed by a colon
|
|
1877
|
-
const hasCredentials = /credentials\s*:/.test(authBlock);
|
|
1878
|
-
const hasActivity = /activity\s*:/.test(authBlock);
|
|
1879
|
-
const hasRoles = /roles\s*:/.test(authBlock);
|
|
1880
|
-
const hasOAuth = /oauth\s*:/.test(authBlock);
|
|
1881
|
-
const hasGuards = /guards\s*:/.test(authBlock);
|
|
1882
|
-
const hasRateLimit = /rateLimit\s*:/.test(authBlock);
|
|
1883
|
-
const hasTurnstile = /turnstile\s*:/.test(authBlock);
|
|
1884
|
-
const hasOrganization = /organizations\s*:/.test(authBlock);
|
|
1885
|
-
return {
|
|
1886
|
-
cookieName: cookieMatch?.[1] ?? 'kuratchi_session',
|
|
1887
|
-
secretEnvKey: secretMatch?.[1] ?? 'AUTH_SECRET',
|
|
1888
|
-
sessionEnabled: sessionMatch?.[1] !== 'false',
|
|
1889
|
-
hasCredentials,
|
|
1890
|
-
hasActivity,
|
|
1891
|
-
hasRoles,
|
|
1892
|
-
hasOAuth,
|
|
1893
|
-
hasGuards,
|
|
1894
|
-
hasRateLimit,
|
|
1895
|
-
hasTurnstile,
|
|
1896
|
-
hasOrganization,
|
|
1897
|
-
};
|
|
1898
|
-
}
|
|
1899
|
-
function toSafeIdentifier(input) {
|
|
1900
|
-
const normalized = input.replace(/[^A-Za-z0-9_$]/g, '_');
|
|
1901
|
-
return /^[A-Za-z_$]/.test(normalized) ? normalized : `_${normalized}`;
|
|
1902
|
-
}
|
|
1903
|
-
/**
|
|
1904
|
-
* Parse durableObjects config from kuratchi.config.ts.
|
|
1905
|
-
*
|
|
1906
|
-
* Supports both string shorthand and object form:
|
|
1907
|
-
* durableObjects: {
|
|
1908
|
-
* ORG_DB: { className: 'OrganizationDO', stubId: 'user.orgId' },
|
|
1909
|
-
* CACHE_DB: 'CacheDO'
|
|
1910
|
-
* }
|
|
1911
|
-
*/
|
|
1912
|
-
function readDoConfig(projectDir) {
|
|
1913
|
-
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
1914
|
-
if (!fs.existsSync(configPath))
|
|
1915
|
-
return [];
|
|
1916
|
-
const source = fs.readFileSync(configPath, 'utf-8');
|
|
1917
|
-
// Find durableObjects block
|
|
1918
|
-
const doIdx = source.search(/durableObjects\s*:\s*\{/);
|
|
1919
|
-
if (doIdx === -1)
|
|
1920
|
-
return [];
|
|
1921
|
-
const braceStart = source.indexOf('{', doIdx);
|
|
1922
|
-
if (braceStart === -1)
|
|
1923
|
-
return [];
|
|
1924
|
-
// Balance braces
|
|
1925
|
-
let depth = 0, braceEnd = braceStart;
|
|
1926
|
-
for (let i = braceStart; i < source.length; i++) {
|
|
1927
|
-
if (source[i] === '{')
|
|
1928
|
-
depth++;
|
|
1929
|
-
else if (source[i] === '}') {
|
|
1930
|
-
depth--;
|
|
1931
|
-
if (depth === 0) {
|
|
1932
|
-
braceEnd = i;
|
|
1933
|
-
break;
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
const doBlock = source.slice(braceStart + 1, braceEnd);
|
|
1938
|
-
const entries = [];
|
|
1939
|
-
// Match object form: BINDING: { className: '...', stubId: '...' }
|
|
1940
|
-
const objRegex = /(\w+)\s*:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/g;
|
|
1941
|
-
let m;
|
|
1942
|
-
while ((m = objRegex.exec(doBlock)) !== null) {
|
|
1943
|
-
const binding = m[1];
|
|
1944
|
-
const body = m[2];
|
|
1945
|
-
const cnMatch = body.match(/className\s*:\s*['"](\w+)['"]/);
|
|
1946
|
-
if (!cnMatch)
|
|
1947
|
-
continue;
|
|
1948
|
-
const entry = { binding, className: cnMatch[1] };
|
|
1949
|
-
const stubIdMatch = body.match(/stubId\s*:\s*['"]([^'"]+)['"]/);
|
|
1950
|
-
if (stubIdMatch)
|
|
1951
|
-
entry.stubId = stubIdMatch[1];
|
|
1952
|
-
const filesMatch = body.match(/files\s*:\s*\[([\s\S]*?)\]/);
|
|
1953
|
-
if (filesMatch) {
|
|
1954
|
-
const list = [];
|
|
1955
|
-
const itemRegex = /['"]([^'"]+)['"]/g;
|
|
1956
|
-
let fm;
|
|
1957
|
-
while ((fm = itemRegex.exec(filesMatch[1])) !== null) {
|
|
1958
|
-
list.push(fm[1]);
|
|
1959
|
-
}
|
|
1960
|
-
if (list.length > 0)
|
|
1961
|
-
entry.files = list;
|
|
1962
|
-
}
|
|
1963
|
-
// (inject config removed �" DO methods are org-scoped, no auto-injection needed)
|
|
1964
|
-
entries.push(entry);
|
|
1965
|
-
}
|
|
1966
|
-
// Match string shorthand: BINDING: 'ClassName' (skip bindings already found)
|
|
1967
|
-
const foundBindings = new Set(entries.map(e => e.binding));
|
|
1968
|
-
const pairRegex = /(\w+)\s*:\s*['"](\w+)['"]\s*[,}\n]/g;
|
|
1969
|
-
while ((m = pairRegex.exec(doBlock)) !== null) {
|
|
1970
|
-
if (foundBindings.has(m[1]))
|
|
1971
|
-
continue;
|
|
1972
|
-
// Make sure this isn't a nested key like 'className'
|
|
1973
|
-
if (['className', 'stubId'].includes(m[1]))
|
|
1974
|
-
continue;
|
|
1975
|
-
entries.push({ binding: m[1], className: m[2] });
|
|
1976
|
-
}
|
|
1977
|
-
return entries;
|
|
1978
|
-
}
|
|
1979
|
-
function readWorkerClassConfig(projectDir, key) {
|
|
1980
|
-
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
1981
|
-
if (!fs.existsSync(configPath))
|
|
1982
|
-
return [];
|
|
1983
|
-
const source = fs.readFileSync(configPath, 'utf-8');
|
|
1984
|
-
const keyIdx = source.search(new RegExp(`\\b${key}\\s*:\\s*\\{`));
|
|
1985
|
-
if (keyIdx === -1)
|
|
1986
|
-
return [];
|
|
1987
|
-
const braceStart = source.indexOf('{', keyIdx);
|
|
1988
|
-
if (braceStart === -1)
|
|
1989
|
-
return [];
|
|
1990
|
-
const body = extractBalancedBody(source, braceStart, '{', '}');
|
|
1991
|
-
if (body == null)
|
|
1992
|
-
return [];
|
|
1993
|
-
const entries = [];
|
|
1994
|
-
const expectedSuffix = key === 'containers' ? '.container' : '.workflow';
|
|
1995
|
-
const allowedExt = /\.(ts|js|mjs|cjs)$/i;
|
|
1996
|
-
const requiredFilePattern = new RegExp(`\\${expectedSuffix}\\.(ts|js|mjs|cjs)$`, 'i');
|
|
1997
|
-
const resolveClassFromFile = (binding, filePath) => {
|
|
1998
|
-
if (!requiredFilePattern.test(filePath)) {
|
|
1999
|
-
throw new Error(`[kuratchi] ${key}.${binding} must reference a file ending in "${expectedSuffix}.ts|js|mjs|cjs". Received: ${filePath}`);
|
|
2000
|
-
}
|
|
2001
|
-
if (!allowedExt.test(filePath)) {
|
|
2002
|
-
throw new Error(`[kuratchi] ${key}.${binding} file must be a TypeScript or JavaScript module. Received: ${filePath}`);
|
|
2003
|
-
}
|
|
2004
|
-
const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
|
|
2005
|
-
if (!fs.existsSync(absPath)) {
|
|
2006
|
-
throw new Error(`[kuratchi] ${key}.${binding} file not found: ${filePath}`);
|
|
2007
|
-
}
|
|
2008
|
-
const fileSource = fs.readFileSync(absPath, 'utf-8');
|
|
2009
|
-
const defaultClass = fileSource.match(/export\s+default\s+class\s+(\w+)/);
|
|
2010
|
-
if (defaultClass) {
|
|
2011
|
-
return { className: defaultClass[1], exportKind: 'default' };
|
|
2012
|
-
}
|
|
2013
|
-
const namedClass = fileSource.match(/export\s+class\s+(\w+)/);
|
|
2014
|
-
if (namedClass) {
|
|
2015
|
-
return { className: namedClass[1], exportKind: 'named' };
|
|
2016
|
-
}
|
|
2017
|
-
throw new Error(`[kuratchi] ${key}.${binding} must export a class via "export class X" or "export default class X". File: ${filePath}`);
|
|
2018
|
-
};
|
|
2019
|
-
// Object form:
|
|
2020
|
-
// containers: { WORDPRESS_CONTAINER: { file: 'src/server/containers/wordpress.container.ts', className?: 'WordPressContainer' } }
|
|
2021
|
-
// workflows: { NEW_SITE_WORKFLOW: { file: 'src/server/workflows/new-site.workflow.ts', className?: 'NewSiteWorkflow' } }
|
|
2022
|
-
const objRegex = /(\w+)\s*:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/g;
|
|
2023
|
-
let m;
|
|
2024
|
-
while ((m = objRegex.exec(body)) !== null) {
|
|
2025
|
-
const binding = m[1];
|
|
2026
|
-
const entryBody = m[2];
|
|
2027
|
-
const fileMatch = entryBody.match(/file\s*:\s*['"]([^'"]+)['"]/);
|
|
2028
|
-
if (!fileMatch)
|
|
2029
|
-
continue;
|
|
2030
|
-
const inferred = resolveClassFromFile(binding, fileMatch[1]);
|
|
2031
|
-
const classMatch = entryBody.match(/className\s*:\s*['"](\w+)['"]/);
|
|
2032
|
-
const className = classMatch?.[1] ?? inferred.className;
|
|
2033
|
-
entries.push({
|
|
2034
|
-
binding,
|
|
2035
|
-
className,
|
|
2036
|
-
file: fileMatch[1],
|
|
2037
|
-
exportKind: inferred.exportKind,
|
|
2038
|
-
});
|
|
2039
|
-
}
|
|
2040
|
-
// String shorthand:
|
|
2041
|
-
// containers: { WORDPRESS_CONTAINER: 'src/server/containers/wordpress.container.ts' }
|
|
2042
|
-
// workflows: { NEW_SITE_WORKFLOW: 'src/server/workflows/new-site.workflow.ts' }
|
|
2043
|
-
const foundBindings = new Set(entries.map((e) => e.binding));
|
|
2044
|
-
const pairRegex = /(\w+)\s*:\s*['"]([^'"]+)['"]\s*[,}\n]/g;
|
|
2045
|
-
while ((m = pairRegex.exec(body)) !== null) {
|
|
2046
|
-
const binding = m[1];
|
|
2047
|
-
const file = m[2];
|
|
2048
|
-
if (foundBindings.has(binding))
|
|
2049
|
-
continue;
|
|
2050
|
-
if (binding === 'file' || binding === 'className')
|
|
2051
|
-
continue;
|
|
2052
|
-
const inferred = resolveClassFromFile(binding, file);
|
|
2053
|
-
entries.push({
|
|
2054
|
-
binding,
|
|
2055
|
-
className: inferred.className,
|
|
2056
|
-
file,
|
|
2057
|
-
exportKind: inferred.exportKind,
|
|
2058
|
-
});
|
|
2059
|
-
}
|
|
2060
|
-
return entries;
|
|
2061
|
-
}
|
|
2062
|
-
function resolveClassExportFromFile(absPath, errorLabel) {
|
|
2063
|
-
if (!fs.existsSync(absPath)) {
|
|
2064
|
-
throw new Error(`[kuratchi] ${errorLabel} file not found: ${absPath}`);
|
|
2065
|
-
}
|
|
2066
|
-
const fileSource = fs.readFileSync(absPath, 'utf-8');
|
|
2067
|
-
const defaultClass = fileSource.match(/export\s+default\s+class\s+(\w+)/);
|
|
2068
|
-
if (defaultClass) {
|
|
2069
|
-
return { className: defaultClass[1], exportKind: 'default' };
|
|
2070
|
-
}
|
|
2071
|
-
const namedClass = fileSource.match(/export\s+class\s+(\w+)/);
|
|
2072
|
-
if (namedClass) {
|
|
2073
|
-
return { className: namedClass[1], exportKind: 'named' };
|
|
2074
|
-
}
|
|
2075
|
-
throw new Error(`[kuratchi] ${errorLabel} must export a class via "export class X" or "export default class X". File: ${absPath}`);
|
|
2076
|
-
}
|
|
2077
|
-
function readAssetsPrefix(projectDir) {
|
|
2078
|
-
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
2079
|
-
if (!fs.existsSync(configPath))
|
|
2080
|
-
return '/assets/';
|
|
2081
|
-
const source = fs.readFileSync(configPath, 'utf-8');
|
|
2082
|
-
const match = source.match(/assetsPrefix\s*:\s*['"]([^'"]+)['"]/);
|
|
2083
|
-
if (!match)
|
|
2084
|
-
return '/assets/';
|
|
2085
|
-
let prefix = match[1];
|
|
2086
|
-
if (!prefix.startsWith('/'))
|
|
2087
|
-
prefix = '/' + prefix;
|
|
2088
|
-
if (!prefix.endsWith('/'))
|
|
2089
|
-
prefix += '/';
|
|
2090
|
-
return prefix;
|
|
2091
|
-
}
|
|
2092
|
-
function discoverConventionClassFiles(projectDir, dir, suffix, errorLabel) {
|
|
2093
|
-
const absDir = path.join(projectDir, dir);
|
|
2094
|
-
const files = discoverFilesWithSuffix(absDir, suffix);
|
|
2095
|
-
if (files.length === 0)
|
|
2096
|
-
return [];
|
|
2097
|
-
return files.map((absPath) => {
|
|
2098
|
-
const resolved = resolveClassExportFromFile(absPath, errorLabel);
|
|
2099
|
-
return {
|
|
2100
|
-
className: resolved.className,
|
|
2101
|
-
file: path.relative(projectDir, absPath).replace(/\\/g, '/'),
|
|
2102
|
-
exportKind: resolved.exportKind,
|
|
2103
|
-
};
|
|
2104
|
-
});
|
|
2105
|
-
}
|
|
2106
|
-
function discoverFilesWithSuffix(dir, suffix) {
|
|
2107
|
-
if (!fs.existsSync(dir))
|
|
2108
|
-
return [];
|
|
2109
|
-
const out = [];
|
|
2110
|
-
const walk = (absDir) => {
|
|
2111
|
-
for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) {
|
|
2112
|
-
const abs = path.join(absDir, entry.name);
|
|
2113
|
-
if (entry.isDirectory()) {
|
|
2114
|
-
walk(abs);
|
|
2115
|
-
}
|
|
2116
|
-
else if (entry.isFile() && abs.endsWith(suffix)) {
|
|
2117
|
-
out.push(abs);
|
|
2118
|
-
}
|
|
2119
|
-
}
|
|
2120
|
-
};
|
|
2121
|
-
walk(dir);
|
|
2122
|
-
return out;
|
|
2123
|
-
}
|
|
2124
|
-
/**
|
|
2125
|
-
* Auto-discover .workflow.ts files in src/server/.
|
|
2126
|
-
* Derives binding name from filename: migration.workflow.ts → MIGRATION_WORKFLOW
|
|
2127
|
-
* Returns entries compatible with WorkerClassConfigEntry for worker.js export generation.
|
|
2128
|
-
*/
|
|
2129
|
-
function discoverWorkflowFiles(projectDir) {
|
|
2130
|
-
const serverDir = path.join(projectDir, 'src', 'server');
|
|
2131
|
-
const files = discoverFilesWithSuffix(serverDir, '.workflow.ts');
|
|
2132
|
-
if (files.length === 0)
|
|
2133
|
-
return [];
|
|
2134
|
-
return files.map((absPath) => {
|
|
2135
|
-
const fileName = path.basename(absPath, '.workflow.ts');
|
|
2136
|
-
// Derive binding: migration.workflow.ts → MIGRATION_WORKFLOW
|
|
2137
|
-
const binding = fileName.toUpperCase().replace(/-/g, '_') + '_WORKFLOW';
|
|
2138
|
-
const resolved = resolveClassExportFromFile(absPath, `.workflow`);
|
|
2139
|
-
return {
|
|
2140
|
-
binding,
|
|
2141
|
-
className: resolved.className,
|
|
2142
|
-
file: path.relative(projectDir, absPath).replace(/\\/g, '/'),
|
|
2143
|
-
exportKind: resolved.exportKind,
|
|
2144
|
-
};
|
|
2145
|
-
});
|
|
2146
|
-
}
|
|
2147
|
-
/**
|
|
2148
|
-
* Auto-discover .container.ts files in src/server/.
|
|
2149
|
-
* Derives binding name from filename: wordpress.container.ts → WORDPRESS_CONTAINER
|
|
2150
|
-
* Returns entries compatible with WorkerClassConfigEntry for worker.js export generation.
|
|
2151
|
-
*/
|
|
2152
|
-
function discoverContainerFiles(projectDir) {
|
|
2153
|
-
const serverDir = path.join(projectDir, 'src', 'server');
|
|
2154
|
-
const files = discoverFilesWithSuffix(serverDir, '.container.ts');
|
|
2155
|
-
if (files.length === 0)
|
|
2156
|
-
return [];
|
|
2157
|
-
return files.map((absPath) => {
|
|
2158
|
-
const fileName = path.basename(absPath, '.container.ts');
|
|
2159
|
-
// Derive binding: wordpress.container.ts → WORDPRESS_CONTAINER
|
|
2160
|
-
const binding = fileName.toUpperCase().replace(/-/g, '_') + '_CONTAINER';
|
|
2161
|
-
const resolved = resolveClassExportFromFile(absPath, `.container`);
|
|
2162
|
-
return {
|
|
2163
|
-
binding,
|
|
2164
|
-
className: resolved.className,
|
|
2165
|
-
file: path.relative(projectDir, absPath).replace(/\\/g, '/'),
|
|
2166
|
-
exportKind: resolved.exportKind,
|
|
2167
|
-
};
|
|
2168
|
-
});
|
|
2169
|
-
}
|
|
2170
|
-
/**
|
|
2171
|
-
* Auto-discover Durable Objects from .do.ts files.
|
|
2172
|
-
* Returns both DoConfigEntry (for wrangler sync) and DoHandlerEntry (for code gen).
|
|
2173
|
-
*
|
|
2174
|
-
* Convention:
|
|
2175
|
-
* - File: user.do.ts → Binding: USER_DO
|
|
2176
|
-
* - Class: export default class UserDO extends DurableObject
|
|
2177
|
-
* - Optional: static binding = 'CUSTOM_BINDING' to override
|
|
2178
|
-
*/
|
|
2179
|
-
function discoverDurableObjects(srcDir, configDoEntries, ormDatabases) {
|
|
2180
|
-
const serverDir = path.join(srcDir, 'server');
|
|
2181
|
-
const legacyDir = path.join(srcDir, 'durable-objects');
|
|
2182
|
-
const serverDoFiles = discoverFilesWithSuffix(serverDir, '.do.ts');
|
|
2183
|
-
const legacyDoFiles = discoverFilesWithSuffix(legacyDir, '.ts');
|
|
2184
|
-
const discoveredFiles = Array.from(new Set([...serverDoFiles, ...legacyDoFiles]));
|
|
2185
|
-
if (discoveredFiles.length === 0) {
|
|
2186
|
-
return { config: configDoEntries, handlers: [] };
|
|
2187
|
-
}
|
|
2188
|
-
// Build lookup from config for stubId (still needed for auth integration)
|
|
2189
|
-
const configByBinding = new Map();
|
|
2190
|
-
for (const entry of configDoEntries) {
|
|
2191
|
-
configByBinding.set(entry.binding, entry);
|
|
2192
|
-
}
|
|
2193
|
-
const handlers = [];
|
|
2194
|
-
const discoveredConfig = [];
|
|
2195
|
-
const fileNameToAbsPath = new Map();
|
|
2196
|
-
const seenBindings = new Set();
|
|
2197
|
-
for (const absPath of discoveredFiles) {
|
|
2198
|
-
const file = path.basename(absPath);
|
|
2199
|
-
const source = fs.readFileSync(absPath, 'utf-8');
|
|
2200
|
-
// Must extend DurableObject
|
|
2201
|
-
const hasClass = /extends\s+DurableObject\b/.test(source);
|
|
2202
|
-
if (!hasClass)
|
|
2203
|
-
continue;
|
|
2204
|
-
// Extract class name
|
|
2205
|
-
const classMatch = source.match(/export\s+default\s+class\s+(\w+)\s+extends\s+DurableObject/);
|
|
2206
|
-
const className = classMatch?.[1] ?? null;
|
|
2207
|
-
if (!className)
|
|
2208
|
-
continue;
|
|
2209
|
-
// Derive binding from filename or static binding property
|
|
2210
|
-
// user.do.ts → USER_DO
|
|
2211
|
-
const bindingMatch = source.match(/static\s+binding\s*=\s*['"](\w+)['"]/);
|
|
2212
|
-
const baseName = file.replace(/\.do\.ts$/, '').replace(/\.ts$/, '');
|
|
2213
|
-
const derivedBinding = baseName.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase() + '_DO';
|
|
2214
|
-
const binding = bindingMatch?.[1] ?? derivedBinding;
|
|
2215
|
-
if (seenBindings.has(binding)) {
|
|
2216
|
-
throw new Error(`[KuratchiJS] Duplicate DO binding '${binding}' detected. Use 'static binding = "UNIQUE_NAME"' in one of the classes.`);
|
|
2217
|
-
}
|
|
2218
|
-
seenBindings.add(binding);
|
|
2219
|
-
// Extract public class methods for RPC
|
|
2220
|
-
const classMethods = extractClassMethods(source, className);
|
|
2221
|
-
const fileName = file.replace(/\.ts$/, '');
|
|
2222
|
-
const existing = fileNameToAbsPath.get(fileName);
|
|
2223
|
-
if (existing && existing !== absPath) {
|
|
2224
|
-
throw new Error(`[KuratchiJS] Duplicate DO handler file name '${fileName}.ts' detected:\n- ${existing}\n- ${absPath}\nRename one file or move it to avoid proxy name collision.`);
|
|
2225
|
-
}
|
|
2226
|
-
fileNameToAbsPath.set(fileName, absPath);
|
|
2227
|
-
// Merge with config entry if exists (for stubId)
|
|
2228
|
-
const configEntry = configByBinding.get(binding);
|
|
2229
|
-
discoveredConfig.push({
|
|
2230
|
-
binding,
|
|
2231
|
-
className,
|
|
2232
|
-
stubId: configEntry?.stubId,
|
|
2233
|
-
files: [file],
|
|
2234
|
-
});
|
|
2235
|
-
handlers.push({
|
|
2236
|
-
fileName,
|
|
2237
|
-
absPath,
|
|
2238
|
-
binding,
|
|
2239
|
-
mode: 'class',
|
|
2240
|
-
className,
|
|
2241
|
-
classMethods,
|
|
2242
|
-
exportedFunctions: [],
|
|
2243
|
-
});
|
|
2244
|
-
}
|
|
2245
|
-
return { config: discoveredConfig, handlers };
|
|
2246
|
-
}
|
|
2247
|
-
/**
|
|
2248
|
-
* Extract method names from a class body using brace-balanced parsing.
|
|
2249
|
-
* Only public methods (no private/protected/underscore prefix) are RPC-accessible.
|
|
2250
|
-
*/
|
|
2251
|
-
function extractClassMethods(source, className) {
|
|
2252
|
-
// Find: class ClassName extends DurableObject {
|
|
2253
|
-
const classIdx = source.search(new RegExp(`class\\s+${className}\\s+extends\\s+DurableObject`));
|
|
2254
|
-
if (classIdx === -1)
|
|
2255
|
-
return [];
|
|
2256
|
-
const braceStart = source.indexOf('{', classIdx);
|
|
2257
|
-
if (braceStart === -1)
|
|
2258
|
-
return [];
|
|
2259
|
-
// Balance braces to find end of class
|
|
2260
|
-
let depth = 0, braceEnd = braceStart;
|
|
2261
|
-
for (let i = braceStart; i < source.length; i++) {
|
|
2262
|
-
if (source[i] === '{')
|
|
2263
|
-
depth++;
|
|
2264
|
-
else if (source[i] === '}') {
|
|
2265
|
-
depth--;
|
|
2266
|
-
if (depth === 0) {
|
|
2267
|
-
braceEnd = i;
|
|
2268
|
-
break;
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
}
|
|
2272
|
-
const classBody = source.slice(braceStart + 1, braceEnd);
|
|
2273
|
-
// Match method declarations with optional visibility/static/async modifiers.
|
|
2274
|
-
const methods = [];
|
|
2275
|
-
const methodRegex = /^\s+(?:(public|private|protected)\s+)?(?:(static)\s+)?(?:(async)\s+)?([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*(?::[^{]+)?\{/gm;
|
|
2276
|
-
const reserved = new Set([
|
|
2277
|
-
'constructor', 'static', 'get', 'set',
|
|
2278
|
-
'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case',
|
|
2279
|
-
'throw', 'try', 'catch', 'finally', 'new', 'delete', 'typeof',
|
|
2280
|
-
'void', 'instanceof', 'in', 'of', 'await', 'yield', 'const',
|
|
2281
|
-
'let', 'var', 'function', 'class', 'import', 'export', 'default',
|
|
2282
|
-
'break', 'continue', 'with', 'super', 'this',
|
|
2283
|
-
]);
|
|
2284
|
-
let m;
|
|
2285
|
-
while ((m = methodRegex.exec(classBody)) !== null) {
|
|
2286
|
-
const visibility = m[1] ?? 'public';
|
|
2287
|
-
const isStatic = !!m[2];
|
|
2288
|
-
const isAsync = !!m[3];
|
|
2289
|
-
const name = m[4];
|
|
2290
|
-
if (isStatic)
|
|
2291
|
-
continue;
|
|
2292
|
-
if (reserved.has(name))
|
|
2293
|
-
continue;
|
|
2294
|
-
const matchText = m[0] ?? '';
|
|
2295
|
-
const openRel = matchText.lastIndexOf('{');
|
|
2296
|
-
const openAbs = openRel >= 0 ? m.index + openRel : -1;
|
|
2297
|
-
let hasWorkerContextCalls = false;
|
|
2298
|
-
const callsThisMethods = [];
|
|
2299
|
-
if (openAbs >= 0) {
|
|
2300
|
-
let depth = 0;
|
|
2301
|
-
let endAbs = openAbs;
|
|
2302
|
-
for (let i = openAbs; i < classBody.length; i++) {
|
|
2303
|
-
const ch = classBody[i];
|
|
2304
|
-
if (ch === '{')
|
|
2305
|
-
depth++;
|
|
2306
|
-
else if (ch === '}') {
|
|
2307
|
-
depth--;
|
|
2308
|
-
if (depth === 0) {
|
|
2309
|
-
endAbs = i;
|
|
2310
|
-
break;
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
}
|
|
2314
|
-
const bodySource = classBody.slice(openAbs + 1, endAbs);
|
|
2315
|
-
hasWorkerContextCalls = /\b(getCurrentUser|redirect|goto|getRequest|getLocals)\s*\(/.test(bodySource);
|
|
2316
|
-
const called = new Set();
|
|
2317
|
-
const callRegex = /\bthis\.([A-Za-z_$][\w$]*)\s*\(/g;
|
|
2318
|
-
let cm;
|
|
2319
|
-
while ((cm = callRegex.exec(bodySource)) !== null) {
|
|
2320
|
-
called.add(cm[1]);
|
|
2321
|
-
}
|
|
2322
|
-
callsThisMethods.push(...called);
|
|
2323
|
-
}
|
|
2324
|
-
methods.push({
|
|
2325
|
-
name,
|
|
2326
|
-
visibility: visibility,
|
|
2327
|
-
isStatic,
|
|
2328
|
-
isAsync,
|
|
2329
|
-
hasWorkerContextCalls,
|
|
2330
|
-
callsThisMethods,
|
|
2331
|
-
});
|
|
2332
|
-
}
|
|
2333
|
-
return methods;
|
|
2334
|
-
}
|
|
2335
|
-
function extractExportedFunctions(source) {
|
|
2336
|
-
const out = [];
|
|
2337
|
-
const fnRegex = /export\s+(?:async\s+)?function\s+(\w+)/g;
|
|
2338
|
-
let m;
|
|
2339
|
-
while ((m = fnRegex.exec(source)) !== null)
|
|
2340
|
-
out.push(m[1]);
|
|
2341
|
-
return out;
|
|
2342
|
-
}
|
|
2343
|
-
/**
|
|
2344
|
-
* Generate a proxy module for a DO handler file.
|
|
2345
|
-
*
|
|
2346
|
-
* The proxy provides auto-RPC function exports.
|
|
2347
|
-
* Class mode only: public class methods become RPC exports.
|
|
2348
|
-
* Methods starting with underscore or marked private/protected are excluded.
|
|
2349
|
-
*/
|
|
2350
|
-
function generateHandlerProxy(handler, projectDir) {
|
|
2351
|
-
const doDir = path.join(projectDir, '.kuratchi', 'do');
|
|
2352
|
-
const origRelPath = path.relative(doDir, handler.absPath).replace(/\\/g, '/').replace(/\.ts$/, '.js');
|
|
2353
|
-
const handlerLocal = `__handler_${toSafeIdentifier(handler.fileName)}`;
|
|
2354
|
-
// Lifecycle methods excluded from RPC
|
|
2355
|
-
const lifecycle = new Set(['constructor', 'fetch', 'alarm', 'webSocketMessage', 'webSocketClose', 'webSocketError']);
|
|
2356
|
-
// Only public methods (not starting with _) are RPC-accessible
|
|
2357
|
-
const rpcFunctions = handler.classMethods
|
|
2358
|
-
.filter((m) => m.visibility === 'public' && !m.name.startsWith('_') && !lifecycle.has(m.name))
|
|
2359
|
-
.map((m) => m.name);
|
|
2360
|
-
const methods = handler.classMethods.map((m) => ({ ...m }));
|
|
2361
|
-
const methodMap = new Map(methods.map((m) => [m.name, m]));
|
|
2362
|
-
let changed = true;
|
|
2363
|
-
while (changed) {
|
|
2364
|
-
changed = false;
|
|
2365
|
-
for (const m of methods) {
|
|
2366
|
-
if (m.hasWorkerContextCalls)
|
|
2367
|
-
continue;
|
|
2368
|
-
for (const called of m.callsThisMethods) {
|
|
2369
|
-
const target = methodMap.get(called);
|
|
2370
|
-
if (target?.hasWorkerContextCalls) {
|
|
2371
|
-
m.hasWorkerContextCalls = true;
|
|
2372
|
-
changed = true;
|
|
2373
|
-
break;
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
}
|
|
2378
|
-
const workerContextMethods = methods
|
|
2379
|
-
.filter((m) => m.visibility === 'public' && m.hasWorkerContextCalls)
|
|
2380
|
-
.map((m) => m.name);
|
|
2381
|
-
const asyncMethods = methods.filter((m) => m.isAsync).map((m) => m.name);
|
|
2382
|
-
const lines = [
|
|
2383
|
-
`// Auto-generated by KuratchiJS compiler �" do not edit.`,
|
|
2384
|
-
`import { __getDoStub } from '${RUNTIME_DO_IMPORT}';`,
|
|
2385
|
-
`import ${handlerLocal} from '${origRelPath}';`,
|
|
2386
|
-
``,
|
|
2387
|
-
`const __FD_TAG = '__kuratchi_form_data__';`,
|
|
2388
|
-
`function __isPlainObject(__v) {`,
|
|
2389
|
-
` if (!__v || typeof __v !== 'object') return false;`,
|
|
2390
|
-
` const __proto = Object.getPrototypeOf(__v);`,
|
|
2391
|
-
` return __proto === Object.prototype || __proto === null;`,
|
|
2392
|
-
`}`,
|
|
2393
|
-
`function __encodeArg(__v, __seen = new WeakSet()) {`,
|
|
2394
|
-
` if (typeof FormData !== 'undefined' && __v instanceof FormData) {`,
|
|
2395
|
-
` return { [__FD_TAG]: Array.from(__v.entries()) };`,
|
|
2396
|
-
` }`,
|
|
2397
|
-
` if (Array.isArray(__v)) return __v.map((__x) => __encodeArg(__x, __seen));`,
|
|
2398
|
-
` if (__isPlainObject(__v)) {`,
|
|
2399
|
-
` if (__seen.has(__v)) throw new Error('[KuratchiJS] Circular object passed to DO RPC');`,
|
|
2400
|
-
` __seen.add(__v);`,
|
|
2401
|
-
` const __out = {};`,
|
|
2402
|
-
` for (const [__k, __val] of Object.entries(__v)) __out[__k] = __encodeArg(__val, __seen);`,
|
|
2403
|
-
` __seen.delete(__v);`,
|
|
2404
|
-
` return __out;`,
|
|
2405
|
-
` }`,
|
|
2406
|
-
` return __v;`,
|
|
2407
|
-
`}`,
|
|
2408
|
-
`function __decodeArg(__v) {`,
|
|
2409
|
-
` if (Array.isArray(__v)) return __v.map(__decodeArg);`,
|
|
2410
|
-
` if (__isPlainObject(__v)) {`,
|
|
2411
|
-
` const __obj = __v;`,
|
|
2412
|
-
` if (__FD_TAG in __obj) {`,
|
|
2413
|
-
` const __fd = new FormData();`,
|
|
2414
|
-
` const __entries = Array.isArray(__obj[__FD_TAG]) ? __obj[__FD_TAG] : [];`,
|
|
2415
|
-
` for (const __pair of __entries) {`,
|
|
2416
|
-
` if (Array.isArray(__pair) && __pair.length >= 2) __fd.append(String(__pair[0]), __pair[1]);`,
|
|
2417
|
-
` }`,
|
|
2418
|
-
` return __fd;`,
|
|
2419
|
-
` }`,
|
|
2420
|
-
` const __out = {};`,
|
|
2421
|
-
` for (const [__k, __val] of Object.entries(__obj)) __out[__k] = __decodeArg(__val);`,
|
|
2422
|
-
` return __out;`,
|
|
2423
|
-
` }`,
|
|
2424
|
-
` return __v;`,
|
|
2425
|
-
`}`,
|
|
2426
|
-
``,
|
|
2427
|
-
];
|
|
2428
|
-
if (workerContextMethods.length > 0) {
|
|
2429
|
-
lines.push(`const __workerMethods = new Set(${JSON.stringify(workerContextMethods)});`);
|
|
2430
|
-
lines.push(`const __asyncMethods = new Set(${JSON.stringify(asyncMethods)});`);
|
|
2431
|
-
lines.push(`function __callWorkerMethod(__name, __args) {`);
|
|
2432
|
-
lines.push(` const __self = new Proxy({}, {`);
|
|
2433
|
-
lines.push(` get(_, __k) {`);
|
|
2434
|
-
lines.push(` if (typeof __k !== 'string') return undefined;`);
|
|
2435
|
-
lines.push(` if (__k === 'db') {`);
|
|
2436
|
-
lines.push(` throw new Error("[KuratchiJS] Worker-executed DO method cannot use this.db directly. Move DB access into a non-public method and call it via this.<method>().");`);
|
|
2437
|
-
lines.push(` }`);
|
|
2438
|
-
lines.push(` if (__workerMethods.has(__k)) {`);
|
|
2439
|
-
lines.push(` return (...__a) => ${handlerLocal}.prototype[__k].apply(__self, __a);`);
|
|
2440
|
-
lines.push(` }`);
|
|
2441
|
-
lines.push(` const __local = ${handlerLocal}.prototype[__k];`);
|
|
2442
|
-
lines.push(` if (typeof __local === 'function' && !__asyncMethods.has(__k)) {`);
|
|
2443
|
-
lines.push(` return (...__a) => __local.apply(__self, __a);`);
|
|
2444
|
-
lines.push(` }`);
|
|
2445
|
-
lines.push(` return async (...__a) => { const __s = await __getDoStub('${handler.binding}'); if (!__s) throw new Error('Not authenticated'); return __s[__k](...__a.map((__x) => __encodeArg(__x))); };`);
|
|
2446
|
-
lines.push(` },`);
|
|
2447
|
-
lines.push(` });`);
|
|
2448
|
-
lines.push(` return ${handlerLocal}.prototype[__name].apply(__self, __args.map(__decodeArg));`);
|
|
2449
|
-
lines.push(`}`);
|
|
2450
|
-
lines.push(``);
|
|
2451
|
-
}
|
|
2452
|
-
// Export RPC methods
|
|
2453
|
-
for (const method of rpcFunctions) {
|
|
2454
|
-
if (workerContextMethods.includes(method)) {
|
|
2455
|
-
lines.push(`export async function ${method}(...a) { return __callWorkerMethod('${method}', a); }`);
|
|
2456
|
-
}
|
|
2457
|
-
else {
|
|
2458
|
-
lines.push(`export async function ${method}(...a) { const s = await __getDoStub('${handler.binding}'); if (!s) throw new Error('Not authenticated'); return s.${method}(...a.map((__x) => __encodeArg(__x))); }`);
|
|
2459
|
-
}
|
|
2460
|
-
}
|
|
2461
|
-
return lines.join('\n') + '\n';
|
|
2462
|
-
}
|
|
2463
|
-
function generateRoutesModule(opts) {
|
|
2464
|
-
const layoutBlock = opts.compiledLayout ?? 'function __layout(content) { return content; }';
|
|
2465
|
-
const layoutActionsBlock = opts.compiledLayoutActions
|
|
2466
|
-
? `const __layoutActions = ${opts.compiledLayoutActions};`
|
|
2467
|
-
: 'const __layoutActions = {};';
|
|
2468
|
-
// Custom error page overrides (user-created NNN.html files)
|
|
2469
|
-
const customErrorFunctions = Array.from(opts.compiledErrorPages.entries())
|
|
2470
|
-
.map(([status, fn]) => fn)
|
|
2471
|
-
.join('\n\n');
|
|
2472
|
-
// Resolve path to the framework's context module from the output directory
|
|
2473
|
-
const contextImport = `import { __setRequestContext, __esc, __rawHtml, __sanitizeHtml, __setLocal, __getLocals, buildDefaultBreadcrumbs as __buildDefaultBreadcrumbs } from '${RUNTIME_CONTEXT_IMPORT}';`;
|
|
2474
|
-
const runtimeImport = opts.hasRuntime && opts.runtimeImportPath
|
|
2475
|
-
? `import __kuratchiRuntime from '${opts.runtimeImportPath}';`
|
|
2476
|
-
: '';
|
|
2477
|
-
// Auth session init �" thin cookie parsing injected into Worker entry
|
|
2478
|
-
let authInit = '';
|
|
2479
|
-
if (opts.authConfig && opts.authConfig.sessionEnabled) {
|
|
2480
|
-
const cookieName = opts.authConfig.cookieName;
|
|
2481
|
-
authInit = `
|
|
2482
|
-
// �"��"� Auth Session Init �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
|
|
2483
|
-
|
|
2484
|
-
function __parseCookies(header) {
|
|
2485
|
-
const map = {};
|
|
2486
|
-
if (!header) return map;
|
|
2487
|
-
for (const pair of header.split(';')) {
|
|
2488
|
-
const eq = pair.indexOf('=');
|
|
2489
|
-
if (eq === -1) continue;
|
|
2490
|
-
map[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
|
|
2491
|
-
}
|
|
2492
|
-
return map;
|
|
2493
|
-
}
|
|
2494
|
-
|
|
2495
|
-
function __initAuth(request) {
|
|
2496
|
-
const cookies = __parseCookies(request.headers.get('cookie'));
|
|
2497
|
-
__setLocal('session', null);
|
|
2498
|
-
__setLocal('user', null);
|
|
2499
|
-
__setLocal('auth', {
|
|
2500
|
-
cookies,
|
|
2501
|
-
sessionCookie: cookies['${cookieName}'] || null,
|
|
2502
|
-
cookieName: '${cookieName}',
|
|
2503
|
-
});
|
|
2504
|
-
}
|
|
2505
|
-
`;
|
|
2506
|
-
}
|
|
2507
|
-
const workerImport = `import { WorkerEntrypoint, env as __env } from 'cloudflare:workers';`;
|
|
2508
|
-
// ORM migration imports + init code
|
|
2509
|
-
let migrationImports = '';
|
|
2510
|
-
let migrationInit = '';
|
|
2511
|
-
if (opts.ormDatabases.length > 0) {
|
|
2512
|
-
const schemaImports = [];
|
|
2513
|
-
const migrateEntries = [];
|
|
2514
|
-
// Resolve schema import paths relative to .kuratchi output dir
|
|
2515
|
-
// Config imports are relative to project root (e.g., './src/schemas/todo')
|
|
2516
|
-
// Generated code lives in .kuratchi/routes.js, so we prefix ../ to reach project root
|
|
2517
|
-
for (const db of opts.ormDatabases) {
|
|
2518
|
-
const resolvedPath = db.schemaImportPath.replace(/^\.\//, '../');
|
|
2519
|
-
// Only D1 databases get runtime migration in the Worker fetch handler
|
|
2520
|
-
// DO databases are migrated via initDO() in the DO constructor
|
|
2521
|
-
if (!db.skipMigrations && db.type === 'd1') {
|
|
2522
|
-
schemaImports.push(`import { ${db.schemaExportName} } from '${resolvedPath}';`);
|
|
2523
|
-
migrateEntries.push(` { binding: '${db.binding}', schema: ${db.schemaExportName} }`);
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
if (migrateEntries.length > 0) {
|
|
2527
|
-
migrationImports = [
|
|
2528
|
-
`import { runMigrations } from '@kuratchi/orm/migrations';`,
|
|
2529
|
-
`import { kuratchiORM } from '@kuratchi/orm';`,
|
|
2530
|
-
...schemaImports,
|
|
2531
|
-
].join('\n');
|
|
2532
|
-
migrationInit = `
|
|
2533
|
-
// �"��"� ORM Auto-Migration �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
|
|
2534
|
-
|
|
2535
|
-
let __migrated = false;
|
|
2536
|
-
const __ormDatabases = [
|
|
2537
|
-
${migrateEntries.join(',\n')}
|
|
2538
|
-
];
|
|
2539
|
-
|
|
2540
|
-
async function __runMigrations() {
|
|
2541
|
-
if (__migrated) return;
|
|
2542
|
-
__migrated = true;
|
|
2543
|
-
for (const db of __ormDatabases) {
|
|
2544
|
-
const binding = __env[db.binding];
|
|
2545
|
-
if (!binding) continue;
|
|
2546
|
-
try {
|
|
2547
|
-
const executor = (sql, params) => {
|
|
2548
|
-
let stmt = binding.prepare(sql);
|
|
2549
|
-
if (params?.length) stmt = stmt.bind(...params);
|
|
2550
|
-
return stmt.all().then(r => ({ success: r.success ?? true, data: r.results, results: r.results }));
|
|
2551
|
-
};
|
|
2552
|
-
const result = await runMigrations({ execute: executor, schema: db.schema });
|
|
2553
|
-
if (result.applied) {
|
|
2554
|
-
console.log('[kuratchi] ' + db.binding + ': migrated (' + result.statementsRun + ' statements)');
|
|
2555
|
-
}
|
|
2556
|
-
if (result.warnings.length) {
|
|
2557
|
-
result.warnings.forEach(w => console.warn('[kuratchi] ' + db.binding + ': ' + w));
|
|
2558
|
-
}
|
|
2559
|
-
} catch (err) {
|
|
2560
|
-
console.error('[kuratchi] ' + db.binding + ' migration failed:', err.message);
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
2564
|
-
`;
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
// Auth plugin init �" import config + call @kuratchi/auth setup functions
|
|
2568
|
-
let authPluginImports = '';
|
|
2569
|
-
let authPluginInit = '';
|
|
2570
|
-
const ac = opts.authConfig;
|
|
2571
|
-
if (ac && (ac.hasCredentials || ac.hasActivity || ac.hasRoles || ac.hasOAuth || ac.hasGuards || ac.hasRateLimit || ac.hasTurnstile || ac.hasOrganization)) {
|
|
2572
|
-
const imports = [];
|
|
2573
|
-
const initLines = [];
|
|
2574
|
-
// Import the config file to read auth sub-configs at runtime
|
|
2575
|
-
imports.push(`import __kuratchiConfig from '../kuratchi.config';`);
|
|
2576
|
-
if (ac.hasCredentials) {
|
|
2577
|
-
imports.push(`import { configureCredentials as __configCreds } from '@kuratchi/auth';`);
|
|
2578
|
-
initLines.push(` if (__kuratchiConfig.auth?.credentials) __configCreds(__kuratchiConfig.auth.credentials);`);
|
|
2579
|
-
}
|
|
2580
|
-
if (ac.hasActivity) {
|
|
2581
|
-
imports.push(`import { defineActivities as __defActivities } from '@kuratchi/auth';`);
|
|
2582
|
-
initLines.push(` if (__kuratchiConfig.auth?.activity) __defActivities(__kuratchiConfig.auth.activity);`);
|
|
2583
|
-
}
|
|
2584
|
-
if (ac.hasRoles) {
|
|
2585
|
-
imports.push(`import { defineRoles as __defRoles } from '@kuratchi/auth';`);
|
|
2586
|
-
initLines.push(` if (__kuratchiConfig.auth?.roles) __defRoles(__kuratchiConfig.auth.roles);`);
|
|
2587
|
-
}
|
|
2588
|
-
if (ac.hasOAuth) {
|
|
2589
|
-
imports.push(`import { configureOAuth as __configOAuth } from '@kuratchi/auth';`);
|
|
2590
|
-
initLines.push(` if (__kuratchiConfig.auth?.oauth) {`);
|
|
2591
|
-
initLines.push(` const oc = __kuratchiConfig.auth.oauth;`);
|
|
2592
|
-
initLines.push(` const providers = {};`);
|
|
2593
|
-
initLines.push(` if (oc.providers) {`);
|
|
2594
|
-
initLines.push(` for (const [name, cfg] of Object.entries(oc.providers)) {`);
|
|
2595
|
-
initLines.push(` providers[name] = { clientId: __env[cfg.clientIdEnv] || '', clientSecret: __env[cfg.clientSecretEnv] || '', scopes: cfg.scopes };`);
|
|
2596
|
-
initLines.push(` }`);
|
|
2597
|
-
initLines.push(` }`);
|
|
2598
|
-
initLines.push(` __configOAuth({ providers, loginRedirect: oc.loginRedirect });`);
|
|
2599
|
-
initLines.push(` }`);
|
|
2600
|
-
}
|
|
2601
|
-
if (ac.hasGuards) {
|
|
2602
|
-
imports.push(`import { configureGuards as __configGuards, checkGuard as __checkGuard } from '@kuratchi/auth';`);
|
|
2603
|
-
initLines.push(` if (__kuratchiConfig.auth?.guards) __configGuards(__kuratchiConfig.auth.guards);`);
|
|
2604
|
-
}
|
|
2605
|
-
if (ac.hasRateLimit) {
|
|
2606
|
-
imports.push(`import { configureRateLimit as __configRL, checkRateLimit as __checkRL } from '@kuratchi/auth';`);
|
|
2607
|
-
initLines.push(` if (__kuratchiConfig.auth?.rateLimit) __configRL(__kuratchiConfig.auth.rateLimit);`);
|
|
2608
|
-
}
|
|
2609
|
-
if (ac.hasTurnstile) {
|
|
2610
|
-
imports.push(`import { configureTurnstile as __configTS, checkTurnstile as __checkTS } from '@kuratchi/auth';`);
|
|
2611
|
-
initLines.push(` if (__kuratchiConfig.auth?.turnstile) __configTS(__kuratchiConfig.auth.turnstile);`);
|
|
2612
|
-
}
|
|
2613
|
-
if (ac.hasOrganization) {
|
|
2614
|
-
imports.push(`import { configureOrganization as __configOrg } from '@kuratchi/auth';`);
|
|
2615
|
-
initLines.push(` if (__kuratchiConfig.auth?.organizations) __configOrg(__kuratchiConfig.auth.organizations);`);
|
|
2616
|
-
}
|
|
2617
|
-
authPluginImports = imports.join('\n');
|
|
2618
|
-
authPluginInit = `
|
|
2619
|
-
// �"��"� Auth Plugin Init �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
|
|
2620
|
-
|
|
2621
|
-
function __initAuthPlugins() {
|
|
2622
|
-
${initLines.join('\n')}
|
|
2623
|
-
}
|
|
2624
|
-
`;
|
|
2625
|
-
}
|
|
2626
|
-
// �"��"� Durable Object class generation �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
|
|
2627
|
-
let doImports = '';
|
|
2628
|
-
let doClassCode = '';
|
|
2629
|
-
let doResolverInit = '';
|
|
2630
|
-
if (opts.doConfig.length > 0 && opts.doHandlers.length > 0) {
|
|
2631
|
-
const doImportLines = [];
|
|
2632
|
-
const doClassLines = [];
|
|
2633
|
-
const doResolverLines = [];
|
|
2634
|
-
doImportLines.push(`import { DurableObject as __DO } from 'cloudflare:workers';`);
|
|
2635
|
-
doImportLines.push(`import { initDO as __initDO } from '@kuratchi/orm';`);
|
|
2636
|
-
doImportLines.push(`import { __registerDoResolver, __registerDoClassBinding, __setDoContext } from '${RUNTIME_DO_IMPORT}';`);
|
|
2637
|
-
doImportLines.push(`const __DO_FD_TAG = '__kuratchi_form_data__';`);
|
|
2638
|
-
doImportLines.push(`function __isDoPlainObject(__v) {`);
|
|
2639
|
-
doImportLines.push(` if (!__v || typeof __v !== 'object') return false;`);
|
|
2640
|
-
doImportLines.push(` const __proto = Object.getPrototypeOf(__v);`);
|
|
2641
|
-
doImportLines.push(` return __proto === Object.prototype || __proto === null;`);
|
|
2642
|
-
doImportLines.push(`}`);
|
|
2643
|
-
doImportLines.push(`function __decodeDoArg(__v) {`);
|
|
2644
|
-
doImportLines.push(` if (Array.isArray(__v)) return __v.map(__decodeDoArg);`);
|
|
2645
|
-
doImportLines.push(` if (__isDoPlainObject(__v)) {`);
|
|
2646
|
-
doImportLines.push(` if (__DO_FD_TAG in __v) {`);
|
|
2647
|
-
doImportLines.push(` const __fd = new FormData();`);
|
|
2648
|
-
doImportLines.push(` const __entries = Array.isArray(__v[__DO_FD_TAG]) ? __v[__DO_FD_TAG] : [];`);
|
|
2649
|
-
doImportLines.push(` for (const __pair of __entries) { if (Array.isArray(__pair) && __pair.length >= 2) __fd.append(String(__pair[0]), __pair[1]); }`);
|
|
2650
|
-
doImportLines.push(` return __fd;`);
|
|
2651
|
-
doImportLines.push(` }`);
|
|
2652
|
-
doImportLines.push(` const __out = {};`);
|
|
2653
|
-
doImportLines.push(` for (const [__k, __val] of Object.entries(__v)) __out[__k] = __decodeDoArg(__val);`);
|
|
2654
|
-
doImportLines.push(` return __out;`);
|
|
2655
|
-
doImportLines.push(` }`);
|
|
2656
|
-
doImportLines.push(` return __v;`);
|
|
2657
|
-
doImportLines.push(`}`);
|
|
2658
|
-
// We need getCurrentUser and getOrgStubByName for stub resolvers
|
|
2659
|
-
doImportLines.push(`import { getCurrentUser as __getCU, getOrgStubByName as __getOSBN } from '@kuratchi/auth';`);
|
|
2660
|
-
// Group handlers by binding
|
|
2661
|
-
const handlersByBinding = new Map();
|
|
2662
|
-
for (const h of opts.doHandlers) {
|
|
2663
|
-
const list = handlersByBinding.get(h.binding) ?? [];
|
|
2664
|
-
list.push(h);
|
|
2665
|
-
handlersByBinding.set(h.binding, list);
|
|
2666
|
-
}
|
|
2667
|
-
// Import handler files + schema for each DO (class mode only)
|
|
2668
|
-
for (const doEntry of opts.doConfig) {
|
|
2669
|
-
const handlers = handlersByBinding.get(doEntry.binding) ?? [];
|
|
2670
|
-
const ormDb = opts.ormDatabases.find(d => d.binding === doEntry.binding);
|
|
2671
|
-
// Import schema (paths are relative to project root; prefix ../ since we're in .kuratchi/)
|
|
2672
|
-
if (ormDb) {
|
|
2673
|
-
const schemaPath = ormDb.schemaImportPath.replace(/^\.\//, '../');
|
|
2674
|
-
doImportLines.push(`import { ${ormDb.schemaExportName} as __doSchema_${doEntry.binding} } from '${schemaPath}';`);
|
|
2675
|
-
}
|
|
2676
|
-
// Import handler classes (class mode only - extends DurableObject)
|
|
2677
|
-
for (const h of handlers) {
|
|
2678
|
-
let handlerImportPath = path
|
|
2679
|
-
.relative(path.join(opts.projectDir, '.kuratchi'), h.absPath)
|
|
2680
|
-
.replace(/\\/g, '/')
|
|
2681
|
-
.replace(/\.ts$/, '.js');
|
|
2682
|
-
if (!handlerImportPath.startsWith('.'))
|
|
2683
|
-
handlerImportPath = './' + handlerImportPath;
|
|
2684
|
-
const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
|
|
2685
|
-
doImportLines.push(`import ${handlerVar} from '${handlerImportPath}';`);
|
|
2686
|
-
}
|
|
2687
|
-
// Generate DO class that extends the user's class (for ORM integration)
|
|
2688
|
-
// If no ORM, we just re-export the user's class directly
|
|
2689
|
-
if (ormDb) {
|
|
2690
|
-
const handler = handlers[0];
|
|
2691
|
-
const handlerVar = handler ? `__handler_${toSafeIdentifier(handler.fileName)}` : '__DO';
|
|
2692
|
-
const baseClass = handler ? handlerVar : '__DO';
|
|
2693
|
-
doClassLines.push(`export class ${doEntry.className} extends ${baseClass} {`);
|
|
2694
|
-
doClassLines.push(` constructor(ctx, env) {`);
|
|
2695
|
-
doClassLines.push(` super(ctx, env);`);
|
|
2696
|
-
doClassLines.push(` this.db = __initDO(ctx.storage.sql, __doSchema_${doEntry.binding});`);
|
|
2697
|
-
doClassLines.push(` }`);
|
|
2698
|
-
doClassLines.push(` async __kuratchiLogActivity(payload) {`);
|
|
2699
|
-
doClassLines.push(` const now = new Date().toISOString();`);
|
|
2700
|
-
doClassLines.push(` try {`);
|
|
2701
|
-
doClassLines.push(` await this.db.activityLog.insert({`);
|
|
2702
|
-
doClassLines.push(` userId: payload?.userId ?? null,`);
|
|
2703
|
-
doClassLines.push(` action: payload?.action,`);
|
|
2704
|
-
doClassLines.push(` detail: payload?.detail ?? null,`);
|
|
2705
|
-
doClassLines.push(` ip: payload?.ip ?? null,`);
|
|
2706
|
-
doClassLines.push(` userAgent: payload?.userAgent ?? null,`);
|
|
2707
|
-
doClassLines.push(` createdAt: now,`);
|
|
2708
|
-
doClassLines.push(` updatedAt: now,`);
|
|
2709
|
-
doClassLines.push(` });`);
|
|
2710
|
-
doClassLines.push(` } catch (err) {`);
|
|
2711
|
-
doClassLines.push(` const msg = String((err && err.message) || err || '');`);
|
|
2712
|
-
doClassLines.push(` if (!msg.includes('userId')) throw err;`);
|
|
2713
|
-
doClassLines.push(` // Backward-compat fallback for org DBs not yet migrated with userId column.`);
|
|
2714
|
-
doClassLines.push(` await this.db.activityLog.insert({`);
|
|
2715
|
-
doClassLines.push(` action: payload?.action,`);
|
|
2716
|
-
doClassLines.push(` detail: payload?.detail ?? null,`);
|
|
2717
|
-
doClassLines.push(` ip: payload?.ip ?? null,`);
|
|
2718
|
-
doClassLines.push(` userAgent: payload?.userAgent ?? null,`);
|
|
2719
|
-
doClassLines.push(` createdAt: now,`);
|
|
2720
|
-
doClassLines.push(` updatedAt: now,`);
|
|
2721
|
-
doClassLines.push(` });`);
|
|
2722
|
-
doClassLines.push(` }`);
|
|
2723
|
-
doClassLines.push(` }`);
|
|
2724
|
-
doClassLines.push(` async __kuratchiGetActivity(options = {}) {`);
|
|
2725
|
-
doClassLines.push(` let query = this.db.activityLog;`);
|
|
2726
|
-
doClassLines.push(` if (options?.action) query = query.where({ action: options.action });`);
|
|
2727
|
-
doClassLines.push(` const result = await query.orderBy({ createdAt: 'desc' }).many();`);
|
|
2728
|
-
doClassLines.push(` const rows = Array.isArray(result?.data) ? result.data : [];`);
|
|
2729
|
-
doClassLines.push(` const limit = Number(options?.limit);`);
|
|
2730
|
-
doClassLines.push(` if (Number.isFinite(limit) && limit > 0) return rows.slice(0, Math.floor(limit));`);
|
|
2731
|
-
doClassLines.push(` return rows;`);
|
|
2732
|
-
doClassLines.push(` }`);
|
|
2733
|
-
doClassLines.push(`}`);
|
|
2734
|
-
}
|
|
2735
|
-
else if (handlers.length > 0) {
|
|
2736
|
-
// No ORM - just re-export the user's class directly
|
|
2737
|
-
const handler = handlers[0];
|
|
2738
|
-
const handlerVar = `__handler_${toSafeIdentifier(handler.fileName)}`;
|
|
2739
|
-
doClassLines.push(`export { ${handlerVar} as ${doEntry.className} };`);
|
|
2740
|
-
}
|
|
2741
|
-
// Register class binding for RPC
|
|
2742
|
-
for (const h of handlers) {
|
|
2743
|
-
const handlerVar = `__handler_${toSafeIdentifier(h.fileName)}`;
|
|
2744
|
-
doResolverLines.push(` __registerDoClassBinding(${handlerVar}, '${doEntry.binding}');`);
|
|
2745
|
-
}
|
|
2746
|
-
// Register stub resolver
|
|
2747
|
-
if (doEntry.stubId) {
|
|
2748
|
-
// Config-driven: e.g. stubId: 'user.orgId' �' __u.orgId
|
|
2749
|
-
const fieldPath = doEntry.stubId.startsWith('user.') ? `__u.${doEntry.stubId.slice(5)}` : doEntry.stubId;
|
|
2750
|
-
const checkField = doEntry.stubId.startsWith('user.') ? doEntry.stubId.slice(5) : doEntry.stubId;
|
|
2751
|
-
doResolverLines.push(` __registerDoResolver('${doEntry.binding}', async () => {`);
|
|
2752
|
-
doResolverLines.push(` const __u = await __getCU();`);
|
|
2753
|
-
doResolverLines.push(` if (!__u?.${checkField}) return null;`);
|
|
2754
|
-
doResolverLines.push(` return __getOSBN(${fieldPath});`);
|
|
2755
|
-
doResolverLines.push(` });`);
|
|
2756
|
-
}
|
|
2757
|
-
else {
|
|
2758
|
-
// No stubId config �" stub must be obtained manually
|
|
2759
|
-
doResolverLines.push(` // No 'stubId' config for ${doEntry.binding} �" stub must be obtained manually`);
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
doImports = doImportLines.join('\n');
|
|
2763
|
-
doClassCode = `\n// ── Durable Object Classes (generated) ─────────────────────────\n\n` + doClassLines.join('\n') + '\n';
|
|
2764
|
-
doResolverInit = `\nfunction __initDoResolvers() {\n${doResolverLines.join('\n')}\n}\n`;
|
|
2765
|
-
}
|
|
2766
|
-
// Generate workflow status RPC handlers for auto-discovered workflows
|
|
2767
|
-
// Naming: migration.workflow.ts -> migrationWorkflowStatus(instanceId)
|
|
2768
|
-
let workflowStatusRpc = '';
|
|
2769
|
-
if (opts.workflowConfig.length > 0) {
|
|
2770
|
-
const rpcLines = [];
|
|
2771
|
-
rpcLines.push(`\n// ── Workflow Status RPCs (auto-generated) ─────────────────────`);
|
|
2772
|
-
rpcLines.push(`const __workflowStatusRpc = {`);
|
|
2773
|
-
for (const wf of opts.workflowConfig) {
|
|
2774
|
-
// file: src/server/migration.workflow.ts -> camelCase RPC name: migrationWorkflowStatus
|
|
2775
|
-
const baseName = wf.file.split('/').pop()?.replace(/\.workflow\.ts$/, '') || '';
|
|
2776
|
-
const camelName = baseName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
2777
|
-
const rpcName = `${camelName}WorkflowStatus`;
|
|
2778
|
-
rpcLines.push(` '${rpcName}': async (instanceId) => {`);
|
|
2779
|
-
rpcLines.push(` if (!instanceId) return { status: 'unknown', error: { name: 'Error', message: 'Missing instanceId' } };`);
|
|
2780
|
-
rpcLines.push(` try {`);
|
|
2781
|
-
rpcLines.push(` const instance = await __env.${wf.binding}.get(instanceId);`);
|
|
2782
|
-
rpcLines.push(` return await instance.status();`);
|
|
2783
|
-
rpcLines.push(` } catch (err) {`);
|
|
2784
|
-
rpcLines.push(` return { status: 'errored', error: { name: err?.name || 'Error', message: err?.message || 'Unknown error' } };`);
|
|
2785
|
-
rpcLines.push(` }`);
|
|
2786
|
-
rpcLines.push(` },`);
|
|
2787
|
-
}
|
|
2788
|
-
rpcLines.push(`};`);
|
|
2789
|
-
workflowStatusRpc = rpcLines.join('\n');
|
|
2790
|
-
}
|
|
2791
|
-
return `// Generated by KuratchiJS compiler �" do not edit.
|
|
2792
|
-
${opts.isDev ? '\nglobalThis.__kuratchi_DEV__ = true;\n' : ''}
|
|
2793
|
-
${workerImport}
|
|
2794
|
-
${contextImport}
|
|
2795
|
-
${runtimeImport ? runtimeImport + '\n' : ''}${migrationImports ? migrationImports + '\n' : ''}${authPluginImports ? authPluginImports + '\n' : ''}${doImports ? doImports + '\n' : ''}${opts.serverImports.join('\n')}
|
|
2796
|
-
${workflowStatusRpc}
|
|
2797
|
-
|
|
2798
|
-
// ── Assets ─────────────────────────────────────────────────────
|
|
2799
|
-
|
|
2800
|
-
const __assets = {
|
|
2801
|
-
${opts.compiledAssets.map(a => ` ${JSON.stringify(a.name)}: { content: ${JSON.stringify(a.content)}, mime: ${JSON.stringify(a.mime)}, etag: ${JSON.stringify(a.etag)} }`).join(',\n')}
|
|
2802
|
-
};
|
|
2803
|
-
|
|
2804
|
-
// �"��"� Router �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
|
|
2805
|
-
|
|
2806
|
-
const __staticRoutes = new Map(); // exact path �' index (O(1) lookup)
|
|
2807
|
-
const __dynamicRoutes = []; // regex-based routes (params/wildcards)
|
|
2808
|
-
|
|
2809
|
-
function __addRoute(pattern, index) {
|
|
2810
|
-
if (!pattern.includes(':') && !pattern.includes('*')) {
|
|
2811
|
-
// Static route �" direct Map lookup, no regex needed
|
|
2812
|
-
__staticRoutes.set(pattern, index);
|
|
2813
|
-
} else {
|
|
2814
|
-
// Dynamic route �" build regex for param extraction
|
|
2815
|
-
const paramNames = [];
|
|
2816
|
-
let regexStr = pattern
|
|
2817
|
-
.replace(/\\*(\\w+)/g, (_, name) => { paramNames.push(name); return '(?<' + name + '>.+)'; })
|
|
2818
|
-
.replace(/:(\\w+)/g, (_, name) => { paramNames.push(name); return '(?<' + name + '>[^/]+)'; });
|
|
2819
|
-
__dynamicRoutes.push({ regex: new RegExp('^' + regexStr + '$'), paramNames, index });
|
|
2820
|
-
}
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
|
-
function __match(pathname) {
|
|
2824
|
-
const normalized = pathname === '/' ? '/' : pathname.replace(/\\/$/, '');
|
|
2825
|
-
// Fast path: static routes (most common)
|
|
2826
|
-
const staticIdx = __staticRoutes.get(normalized);
|
|
2827
|
-
if (staticIdx !== undefined) return { params: {}, index: staticIdx };
|
|
2828
|
-
// Slow path: dynamic routes with params
|
|
2829
|
-
for (const route of __dynamicRoutes) {
|
|
2830
|
-
const m = normalized.match(route.regex);
|
|
2831
|
-
if (m) {
|
|
2832
|
-
const params = {};
|
|
2833
|
-
for (const name of route.paramNames) params[name] = m.groups?.[name] ?? '';
|
|
2834
|
-
return { params, index: route.index };
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
return null;
|
|
2838
|
-
}
|
|
2839
|
-
|
|
2840
|
-
// �"��"� Layout �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
|
|
2841
|
-
|
|
2842
|
-
${layoutBlock}
|
|
2843
|
-
|
|
2844
|
-
${layoutActionsBlock}
|
|
2845
|
-
|
|
2846
|
-
// �"��"� Error pages �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
|
|
2847
|
-
|
|
2848
|
-
const __errorMessages = {
|
|
2849
|
-
400: 'Bad Request',
|
|
2850
|
-
401: 'Unauthorized',
|
|
2851
|
-
403: 'Forbidden',
|
|
2852
|
-
404: 'Not Found',
|
|
2853
|
-
405: 'Method Not Allowed',
|
|
2854
|
-
408: 'Request Timeout',
|
|
2855
|
-
429: 'Too Many Requests',
|
|
2856
|
-
500: 'Internal Server Error',
|
|
2857
|
-
502: 'Bad Gateway',
|
|
2858
|
-
503: 'Service Unavailable',
|
|
2859
|
-
};
|
|
2860
|
-
|
|
2861
|
-
// Built-in default error page �" clean, dark, minimal, centered
|
|
2862
|
-
function __errorPage(status, detail) {
|
|
2863
|
-
const title = __errorMessages[status] || 'Error';
|
|
2864
|
-
const detailHtml = detail ? '<p style="font-family:ui-monospace,monospace;font-size:0.8rem;color:#555;background:#111;padding:0.5rem 1rem;border-radius:6px;max-width:480px;margin:1rem auto 0;word-break:break-word">' + __esc(detail) + '</p>' : '';
|
|
2865
|
-
return '<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;text-align:center;padding:2rem">'
|
|
2866
|
-
+ '<div>'
|
|
2867
|
-
+ '<p style="font-size:5rem;font-weight:700;margin:0;color:#333;line-height:1">' + status + '</p>'
|
|
2868
|
-
+ '<p style="font-size:1rem;color:#555;margin:0.5rem 0 0;letter-spacing:0.05em">' + __esc(title) + '</p>'
|
|
2869
|
-
+ detailHtml
|
|
2870
|
-
+ '</div>'
|
|
2871
|
-
+ '</div>';
|
|
2872
|
-
}
|
|
2873
|
-
|
|
2874
|
-
${customErrorFunctions ? '// Custom error page overrides (user-created NNN.html)\n' + customErrorFunctions + '\n' : ''}
|
|
2875
|
-
// Dispatch: use custom override if it exists, otherwise built-in default
|
|
2876
|
-
const __customErrors = {${Array.from(opts.compiledErrorPages.keys()).map(s => ` ${s}: __error_${s}`).join(',')} };
|
|
2877
|
-
|
|
2878
|
-
function __error(status, detail) {
|
|
2879
|
-
if (__customErrors[status]) return __customErrors[status](detail);
|
|
2880
|
-
return __errorPage(status, detail);
|
|
2881
|
-
}
|
|
2882
|
-
|
|
2883
|
-
${opts.compiledComponents.length > 0 ? '// �"��"� Components �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�\n\n' + opts.compiledComponents.join('\n\n') + '\n' : ''}${migrationInit}${authInit}${authPluginInit}${doResolverInit}${doClassCode}
|
|
2884
|
-
// �"��"� Route definitions �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
|
|
2885
|
-
|
|
2886
|
-
const routes = [
|
|
2887
|
-
${opts.compiledRoutes.join(',\n')}
|
|
2888
|
-
];
|
|
2889
|
-
|
|
2890
|
-
for (let i = 0; i < routes.length; i++) __addRoute(routes[i].pattern, i);
|
|
2891
|
-
|
|
2892
|
-
// �"��"� Response helpers �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
|
|
2893
|
-
|
|
2894
|
-
const __defaultSecHeaders = {
|
|
2895
|
-
'X-Content-Type-Options': 'nosniff',
|
|
2896
|
-
'X-Frame-Options': 'DENY',
|
|
2897
|
-
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
2898
|
-
};
|
|
2899
|
-
|
|
2900
|
-
function __secHeaders(response) {
|
|
2901
|
-
for (const [k, v] of Object.entries(__defaultSecHeaders)) {
|
|
2902
|
-
if (!response.headers.has(k)) response.headers.set(k, v);
|
|
2903
|
-
}
|
|
2904
|
-
return response;
|
|
2905
|
-
}
|
|
2906
|
-
|
|
2907
|
-
function __attachCookies(response) {
|
|
2908
|
-
const cookies = __getLocals().__setCookieHeaders;
|
|
2909
|
-
if (cookies && cookies.length > 0) {
|
|
2910
|
-
const newResponse = new Response(response.body, response);
|
|
2911
|
-
for (const h of cookies) newResponse.headers.append('Set-Cookie', h);
|
|
2912
|
-
return __secHeaders(newResponse);
|
|
2913
|
-
}
|
|
2914
|
-
return __secHeaders(response);
|
|
2915
|
-
}
|
|
2916
|
-
|
|
2917
|
-
function __isSameOrigin(request, url) {
|
|
2918
|
-
const fetchSite = request.headers.get('sec-fetch-site');
|
|
2919
|
-
if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'same-site' && fetchSite !== 'none') {
|
|
2920
|
-
return false;
|
|
2921
|
-
}
|
|
2922
|
-
const origin = request.headers.get('origin');
|
|
2923
|
-
if (!origin) return true;
|
|
2924
|
-
try { return new URL(origin).origin === url.origin; } catch { return false; }
|
|
2925
|
-
}
|
|
2926
|
-
|
|
2927
|
-
// Extract fragment content by ID from rendered HTML
|
|
2928
|
-
function __extractFragment(html, fragmentId) {
|
|
2929
|
-
// Find the element with data-poll-id="fragmentId" and extract its innerHTML
|
|
2930
|
-
const escaped = fragmentId.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&');
|
|
2931
|
-
const openTagRegex = new RegExp('<([a-z][a-z0-9]*)\\\\s[^>]*data-poll-id="' + escaped + '"[^>]*>', 'i');
|
|
2932
|
-
const match = html.match(openTagRegex);
|
|
2933
|
-
if (!match) return null;
|
|
2934
|
-
const tagName = match[1];
|
|
2935
|
-
const startIdx = match.index + match[0].length;
|
|
2936
|
-
// Find matching closing tag (handle nesting)
|
|
2937
|
-
let depth = 1;
|
|
2938
|
-
let i = startIdx;
|
|
2939
|
-
const closeTag = '</' + tagName + '>';
|
|
2940
|
-
const openTag = '<' + tagName;
|
|
2941
|
-
while (i < html.length && depth > 0) {
|
|
2942
|
-
const nextClose = html.indexOf(closeTag, i);
|
|
2943
|
-
const nextOpen = html.indexOf(openTag, i);
|
|
2944
|
-
if (nextClose === -1) break;
|
|
2945
|
-
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
2946
|
-
depth++;
|
|
2947
|
-
i = nextOpen + openTag.length;
|
|
2948
|
-
} else {
|
|
2949
|
-
depth--;
|
|
2950
|
-
if (depth === 0) return html.slice(startIdx, nextClose);
|
|
2951
|
-
i = nextClose + closeTag.length;
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
|
-
return null;
|
|
2955
|
-
}
|
|
2956
|
-
|
|
2957
|
-
${opts.isLayoutAsync ? 'async ' : ''}function __render(route, data, fragmentId) {
|
|
2958
|
-
let html = route.render(data);
|
|
2959
|
-
|
|
2960
|
-
// Fragment request: return only the fragment's innerHTML
|
|
2961
|
-
if (fragmentId) {
|
|
2962
|
-
const fragment = __extractFragment(html, fragmentId);
|
|
2963
|
-
if (fragment !== null) {
|
|
2964
|
-
return new Response(fragment, { headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' } });
|
|
2965
|
-
}
|
|
2966
|
-
return new Response('Fragment not found', { status: 404 });
|
|
2967
|
-
}
|
|
2968
|
-
|
|
2969
|
-
// Full page render
|
|
2970
|
-
const headMatch = html.match(/<head>([\\s\\S]*?)<\\/head>/);
|
|
2971
|
-
if (headMatch) {
|
|
2972
|
-
html = html.replace(headMatch[0], '');
|
|
2973
|
-
const layoutHtml = ${opts.isLayoutAsync ? 'await ' : ''}__layout(html);
|
|
2974
|
-
return __attachCookies(new Response(layoutHtml.replace('</head>', headMatch[1] + '</head>'), {
|
|
2975
|
-
headers: { 'content-type': 'text/html; charset=utf-8' }
|
|
2976
|
-
}));
|
|
2977
|
-
}
|
|
2978
|
-
return __attachCookies(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(html), { headers: { 'content-type': 'text/html; charset=utf-8' } }));
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
|
-
const __runtimeDef = (typeof __kuratchiRuntime !== 'undefined' && __kuratchiRuntime && typeof __kuratchiRuntime === 'object') ? __kuratchiRuntime : {};
|
|
2982
|
-
const __runtimeEntries = Object.entries(__runtimeDef).filter(([, step]) => step && typeof step === 'object');
|
|
2983
|
-
|
|
2984
|
-
async function __runRuntimeRequest(ctx, next) {
|
|
2985
|
-
let idx = -1;
|
|
2986
|
-
async function __dispatch(i) {
|
|
2987
|
-
if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in request phase');
|
|
2988
|
-
idx = i;
|
|
2989
|
-
const entry = __runtimeEntries[i];
|
|
2990
|
-
if (!entry) return next();
|
|
2991
|
-
const [, step] = entry;
|
|
2992
|
-
if (typeof step.request !== 'function') return __dispatch(i + 1);
|
|
2993
|
-
return await step.request(ctx, () => __dispatch(i + 1));
|
|
2994
|
-
}
|
|
2995
|
-
return __dispatch(0);
|
|
2996
|
-
}
|
|
2997
|
-
|
|
2998
|
-
async function __runRuntimeRoute(ctx, next) {
|
|
2999
|
-
let idx = -1;
|
|
3000
|
-
async function __dispatch(i) {
|
|
3001
|
-
if (i <= idx) throw new Error('[kuratchi runtime] next() called multiple times in route phase');
|
|
3002
|
-
idx = i;
|
|
3003
|
-
const entry = __runtimeEntries[i];
|
|
3004
|
-
if (!entry) return next();
|
|
3005
|
-
const [, step] = entry;
|
|
3006
|
-
if (typeof step.route !== 'function') return __dispatch(i + 1);
|
|
3007
|
-
return await step.route(ctx, () => __dispatch(i + 1));
|
|
3008
|
-
}
|
|
3009
|
-
return __dispatch(0);
|
|
3010
|
-
}
|
|
3011
|
-
|
|
3012
|
-
async function __runRuntimeResponse(ctx, response) {
|
|
3013
|
-
let out = response;
|
|
3014
|
-
for (const [, step] of __runtimeEntries) {
|
|
3015
|
-
if (typeof step.response !== 'function') continue;
|
|
3016
|
-
out = await step.response(ctx, out);
|
|
3017
|
-
if (!(out instanceof Response)) {
|
|
3018
|
-
throw new Error('[kuratchi runtime] response handlers must return a Response');
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
return out;
|
|
3022
|
-
}
|
|
3023
|
-
|
|
3024
|
-
async function __runRuntimeError(ctx, error) {
|
|
3025
|
-
for (const [name, step] of __runtimeEntries) {
|
|
3026
|
-
if (typeof step.error !== 'function') continue;
|
|
3027
|
-
try {
|
|
3028
|
-
const handled = await step.error(ctx, error);
|
|
3029
|
-
if (handled instanceof Response) return handled;
|
|
3030
|
-
} catch (hookErr) {
|
|
3031
|
-
console.error('[kuratchi runtime] error handler failed in step', name, hookErr);
|
|
3032
|
-
}
|
|
3033
|
-
}
|
|
3034
|
-
return null;
|
|
3035
|
-
}
|
|
3036
|
-
|
|
3037
|
-
// �"��"� Exported Worker entrypoint �"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"��"�
|
|
3038
|
-
|
|
3039
|
-
export default class extends WorkerEntrypoint {
|
|
3040
|
-
async fetch(request) {
|
|
3041
|
-
__setRequestContext(this.ctx, request, __env);
|
|
3042
|
-
${migrationInit ? ' await __runMigrations();\n' : ''}${authInit ? ' __initAuth(request);\n' : ''}${authPluginInit ? ' __initAuthPlugins();\n' : ''}${doResolverInit ? ' __initDoResolvers();\n' : ''}
|
|
3043
|
-
const __runtimeCtx = {
|
|
3044
|
-
request,
|
|
3045
|
-
env: __env,
|
|
3046
|
-
ctx: this.ctx,
|
|
3047
|
-
url: new URL(request.url),
|
|
3048
|
-
params: {},
|
|
3049
|
-
locals: __getLocals(),
|
|
3050
|
-
};
|
|
3051
|
-
|
|
3052
|
-
const __coreFetch = async () => {
|
|
3053
|
-
const request = __runtimeCtx.request;
|
|
3054
|
-
const url = __runtimeCtx.url;
|
|
3055
|
-
const __fragmentId = request.headers.get('x-kuratchi-fragment');
|
|
3056
|
-
${ac?.hasRateLimit ? '\n // Rate limiting - check before route handlers\n { const __rlRes = await __checkRL(); if (__rlRes) return __secHeaders(__rlRes); }\n' : ''}${ac?.hasTurnstile ? ' // Turnstile bot protection\n { const __tsRes = await __checkTS(); if (__tsRes) return __secHeaders(__tsRes); }\n' : ''}${ac?.hasGuards ? ' // Route guards - redirect if not authenticated\n { const __gRes = __checkGuard(); if (__gRes) return __secHeaders(__gRes); }\n' : ''}
|
|
3057
|
-
|
|
3058
|
-
// Serve static assets from src/assets/
|
|
3059
|
-
if (url.pathname.startsWith('${opts.assetsPrefix}')) {
|
|
3060
|
-
const name = url.pathname.slice('${opts.assetsPrefix}'.length);
|
|
3061
|
-
const asset = __assets[name];
|
|
3062
|
-
if (asset) {
|
|
3063
|
-
if (request.headers.get('if-none-match') === asset.etag) {
|
|
3064
|
-
return new Response(null, { status: 304 });
|
|
3065
|
-
}
|
|
3066
|
-
return new Response(asset.content, {
|
|
3067
|
-
headers: { 'content-type': asset.mime, 'cache-control': 'public, max-age=31536000, immutable', 'etag': asset.etag }
|
|
3068
|
-
});
|
|
3069
|
-
}
|
|
3070
|
-
return __secHeaders(new Response('Not Found', { status: 404 }));
|
|
3071
|
-
}
|
|
3072
|
-
|
|
3073
|
-
const match = __match(url.pathname);
|
|
3074
|
-
|
|
3075
|
-
if (!match) {
|
|
3076
|
-
return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(404)), { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } }));
|
|
3077
|
-
}
|
|
3078
|
-
|
|
3079
|
-
__runtimeCtx.params = match.params;
|
|
3080
|
-
const route = routes[match.index];
|
|
3081
|
-
__setLocal('params', match.params);
|
|
3082
|
-
|
|
3083
|
-
// API route: dispatch to method handler
|
|
3084
|
-
if (route.__api) {
|
|
3085
|
-
const method = request.method;
|
|
3086
|
-
if (method === 'OPTIONS') {
|
|
3087
|
-
const handler = route['OPTIONS'];
|
|
3088
|
-
if (typeof handler === 'function') return __secHeaders(await handler(__runtimeCtx));
|
|
3089
|
-
const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
|
|
3090
|
-
return __secHeaders(new Response(null, { status: 204, headers: { 'Allow': allowed, 'Access-Control-Allow-Methods': allowed } }));
|
|
3091
|
-
}
|
|
3092
|
-
const handler = route[method];
|
|
3093
|
-
if (typeof handler !== 'function') {
|
|
3094
|
-
const allowed = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'].filter(m => typeof route[m] === 'function').join(', ');
|
|
3095
|
-
return __secHeaders(new Response(JSON.stringify({ error: 'Method Not Allowed' }), { status: 405, headers: { 'content-type': 'application/json', 'Allow': allowed } }));
|
|
3096
|
-
}
|
|
3097
|
-
return __secHeaders(await handler(__runtimeCtx));
|
|
3098
|
-
}
|
|
3099
|
-
|
|
3100
|
-
const __qFn = request.headers.get('x-kuratchi-query-fn') || '';
|
|
3101
|
-
const __qArgsRaw = request.headers.get('x-kuratchi-query-args') || '[]';
|
|
3102
|
-
let __qArgs = [];
|
|
3103
|
-
try {
|
|
3104
|
-
const __parsed = JSON.parse(__qArgsRaw);
|
|
3105
|
-
__qArgs = Array.isArray(__parsed) ? __parsed : [];
|
|
3106
|
-
} catch {}
|
|
3107
|
-
__setLocal('__queryOverride', __qFn ? { fn: __qFn, args: __qArgs } : null);
|
|
3108
|
-
if (!__getLocals().__breadcrumbs) {
|
|
3109
|
-
__setLocal('breadcrumbs', __buildDefaultBreadcrumbs(url.pathname, match.params));
|
|
3110
|
-
}
|
|
3111
|
-
|
|
3112
|
-
// RPC call: GET ?_rpc=fnName&_args=[...] -> JSON response
|
|
3113
|
-
const __rpcName = url.searchParams.get('_rpc');
|
|
3114
|
-
const __hasRouteRpc = __rpcName && route.rpc && Object.hasOwn(route.rpc, __rpcName);
|
|
3115
|
-
const __hasWorkflowRpc = __rpcName && typeof __workflowStatusRpc !== 'undefined' && Object.hasOwn(__workflowStatusRpc, __rpcName);
|
|
3116
|
-
if (request.method === 'GET' && __rpcName && (__hasRouteRpc || __hasWorkflowRpc)) {
|
|
3117
|
-
if (request.headers.get('x-kuratchi-rpc') !== '1') {
|
|
3118
|
-
return __secHeaders(new Response(JSON.stringify({ ok: false, error: 'Forbidden' }), {
|
|
3119
|
-
status: 403, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
|
|
3120
|
-
}));
|
|
3121
|
-
}
|
|
3122
|
-
try {
|
|
3123
|
-
const __rpcArgsStr = url.searchParams.get('_args');
|
|
3124
|
-
let __rpcArgs = [];
|
|
3125
|
-
if (__rpcArgsStr) {
|
|
3126
|
-
const __parsed = JSON.parse(__rpcArgsStr);
|
|
3127
|
-
__rpcArgs = Array.isArray(__parsed) ? __parsed : [];
|
|
3128
|
-
}
|
|
3129
|
-
const __rpcFn = __hasRouteRpc ? route.rpc[__rpcName] : __workflowStatusRpc[__rpcName];
|
|
3130
|
-
const __rpcResult = await __rpcFn(...__rpcArgs);
|
|
3131
|
-
return __secHeaders(new Response(JSON.stringify({ ok: true, data: __rpcResult }), {
|
|
3132
|
-
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
|
|
3133
|
-
}));
|
|
3134
|
-
} catch (err) {
|
|
3135
|
-
console.error('[kuratchi] RPC error:', err);
|
|
3136
|
-
const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' ? err.message : 'Internal Server Error';
|
|
3137
|
-
return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
|
|
3138
|
-
status: 500, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
|
|
3139
|
-
}));
|
|
3140
|
-
}
|
|
3141
|
-
}
|
|
3142
|
-
|
|
3143
|
-
// Form action: POST with hidden _action field in form body
|
|
3144
|
-
if (request.method === 'POST') {
|
|
3145
|
-
if (!__isSameOrigin(request, url)) {
|
|
3146
|
-
return __secHeaders(new Response('Forbidden', { status: 403 }));
|
|
3147
|
-
}
|
|
3148
|
-
const formData = await request.formData();
|
|
3149
|
-
const actionName = formData.get('_action');
|
|
3150
|
-
const __actionFn = (actionName && route.actions && Object.hasOwn(route.actions, actionName) ? route.actions[actionName] : null)
|
|
3151
|
-
|| (actionName && __layoutActions && Object.hasOwn(__layoutActions, actionName) ? __layoutActions[actionName] : null);
|
|
3152
|
-
if (actionName && __actionFn) {
|
|
3153
|
-
// Check if this is a fetch-based action call (onclick) with JSON args
|
|
3154
|
-
const argsStr = formData.get('_args');
|
|
3155
|
-
const isFetchAction = argsStr !== null;
|
|
3156
|
-
try {
|
|
3157
|
-
if (isFetchAction) {
|
|
3158
|
-
const __parsed = JSON.parse(argsStr);
|
|
3159
|
-
const args = Array.isArray(__parsed) ? __parsed : [];
|
|
3160
|
-
await __actionFn(...args);
|
|
3161
|
-
} else {
|
|
3162
|
-
await __actionFn(formData);
|
|
3163
|
-
}
|
|
3164
|
-
} catch (err) {
|
|
3165
|
-
if (err && err.isRedirectError) {
|
|
3166
|
-
const __redirectTo = err.location || url.pathname;
|
|
3167
|
-
const __redirectStatus = Number(err.status) || 303;
|
|
3168
|
-
if (isFetchAction) {
|
|
3169
|
-
return __attachCookies(__secHeaders(new Response(JSON.stringify({ ok: true, redirectTo: __redirectTo, redirectStatus: __redirectStatus }), {
|
|
3170
|
-
headers: { 'content-type': 'application/json' }
|
|
3171
|
-
})));
|
|
3172
|
-
}
|
|
3173
|
-
return __attachCookies(new Response(null, { status: __redirectStatus, headers: { 'location': __redirectTo } }));
|
|
3174
|
-
}
|
|
3175
|
-
console.error('[kuratchi] Action error:', err);
|
|
3176
|
-
if (isFetchAction) {
|
|
3177
|
-
const __errMsg = typeof __kuratchi_DEV__ !== 'undefined' && err && err.message ? err.message : 'Internal Server Error';
|
|
3178
|
-
return __secHeaders(new Response(JSON.stringify({ ok: false, error: __errMsg }), {
|
|
3179
|
-
status: 500, headers: { 'content-type': 'application/json' }
|
|
3180
|
-
}));
|
|
3181
|
-
}
|
|
3182
|
-
const __loaded = route.load ? await route.load(match.params) : {};
|
|
3183
|
-
const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
|
|
3184
|
-
data.params = match.params;
|
|
3185
|
-
data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
|
|
3186
|
-
const __allActions = Object.assign({}, route.actions, __layoutActions || {});
|
|
3187
|
-
Object.keys(__allActions).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
|
|
3188
|
-
const __errMsg = (err && err.isActionError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : 'Action failed';
|
|
3189
|
-
data[actionName] = { error: __errMsg, loading: false, success: false };
|
|
3190
|
-
return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data, __fragmentId);
|
|
3191
|
-
}
|
|
3192
|
-
// Fetch-based actions return lightweight JSON (no page re-render)
|
|
3193
|
-
if (isFetchAction) {
|
|
3194
|
-
return __attachCookies(new Response(JSON.stringify({ ok: true }), {
|
|
3195
|
-
headers: { 'content-type': 'application/json' }
|
|
3196
|
-
}));
|
|
3197
|
-
}
|
|
3198
|
-
// POST-Redirect-GET: redirect to custom target or back to same URL
|
|
3199
|
-
const __locals = __getLocals();
|
|
3200
|
-
const redirectTo = __locals.__redirectTo || url.pathname;
|
|
3201
|
-
const redirectStatus = Number(__locals.__redirectStatus) || 303;
|
|
3202
|
-
return __attachCookies(new Response(null, { status: redirectStatus, headers: { 'location': redirectTo } }));
|
|
3203
|
-
}
|
|
3204
|
-
}
|
|
3205
|
-
|
|
3206
|
-
// GET (or unmatched POST): load + render
|
|
3207
|
-
try {
|
|
3208
|
-
const __loaded = route.load ? await route.load(match.params) : {};
|
|
3209
|
-
const data = (__loaded && typeof __loaded === 'object') ? __loaded : { value: __loaded };
|
|
3210
|
-
data.params = match.params;
|
|
3211
|
-
data.breadcrumbs = __getLocals().__breadcrumbs ?? [];
|
|
3212
|
-
const __allActionsGet = Object.assign({}, route.actions, __layoutActions || {});
|
|
3213
|
-
Object.keys(__allActionsGet).forEach(function(k) { if (!(k in data)) data[k] = { error: undefined, loading: false, success: false }; });
|
|
3214
|
-
return ${opts.isLayoutAsync ? 'await ' : ''}__render(route, data, __fragmentId);
|
|
3215
|
-
} catch (err) {
|
|
3216
|
-
if (err && err.isRedirectError) {
|
|
3217
|
-
const __redirectTo = err.location || url.pathname;
|
|
3218
|
-
const __redirectStatus = Number(err.status) || 303;
|
|
3219
|
-
return __attachCookies(new Response(null, { status: __redirectStatus, headers: { 'location': __redirectTo } }));
|
|
3220
|
-
}
|
|
3221
|
-
console.error('[kuratchi] Route load/render error:', err);
|
|
3222
|
-
const __pageErrStatus = (err && err.isPageError && err.status) ? err.status : 500;
|
|
3223
|
-
const __errDetail = (err && err.isPageError) ? err.message : (typeof __kuratchi_DEV__ !== 'undefined' && err && err.message) ? err.message : undefined;
|
|
3224
|
-
return __secHeaders(new Response(${opts.isLayoutAsync ? 'await ' : ''}__layout(__error(__pageErrStatus, __errDetail)), { status: __pageErrStatus, headers: { 'content-type': 'text/html; charset=utf-8' } }));
|
|
3225
|
-
}
|
|
3226
|
-
};
|
|
3227
|
-
|
|
3228
|
-
try {
|
|
3229
|
-
const __requestResponse = await __runRuntimeRequest(__runtimeCtx, async () => {
|
|
3230
|
-
return __runRuntimeRoute(__runtimeCtx, __coreFetch);
|
|
3231
|
-
});
|
|
3232
|
-
return await __runRuntimeResponse(__runtimeCtx, __requestResponse);
|
|
3233
|
-
} catch (err) {
|
|
3234
|
-
const __handled = await __runRuntimeError(__runtimeCtx, err);
|
|
3235
|
-
if (__handled) return __secHeaders(__handled);
|
|
3236
|
-
throw err;
|
|
3237
|
-
}
|
|
3238
|
-
}
|
|
3239
|
-
}
|
|
3240
|
-
`;
|
|
3241
|
-
}
|
|
3242
|
-
function resolveRuntimeImportPath(projectDir) {
|
|
3243
|
-
const candidates = [
|
|
3244
|
-
{ file: 'src/server/runtime.hook.ts', importPath: '../src/server/runtime.hook' },
|
|
3245
|
-
];
|
|
3246
|
-
for (const candidate of candidates) {
|
|
3247
|
-
if (fs.existsSync(path.join(projectDir, candidate.file))) {
|
|
3248
|
-
return candidate.importPath;
|
|
3249
|
-
}
|
|
3250
|
-
}
|
|
3251
|
-
return null;
|
|
3252
|
-
}
|
|
3253
|
-
function toWorkerImportPath(projectDir, outDir, filePath) {
|
|
3254
|
-
const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
|
|
3255
|
-
let rel = path.relative(outDir, absPath).replace(/\\/g, '/');
|
|
3256
|
-
if (!rel.startsWith('.'))
|
|
3257
|
-
rel = `./${rel}`;
|
|
3258
|
-
return rel.replace(/\.(ts|js|mjs|cjs)$/, '');
|
|
3259
|
-
}
|
|
3260
|
-
/**
|
|
3261
|
-
* Auto-sync wrangler.jsonc with workflow/container/DO config from kuratchi.config.ts.
|
|
3262
|
-
* This eliminates the need to manually duplicate config between kuratchi.config.ts and wrangler.jsonc.
|
|
3263
|
-
*
|
|
3264
|
-
* The function:
|
|
3265
|
-
* 1. Reads existing wrangler.jsonc (or wrangler.json)
|
|
3266
|
-
* 2. Updates/adds workflow entries based on kuratchi.config.ts
|
|
3267
|
-
* 3. Preserves all other wrangler config (bindings, vars, etc.)
|
|
3268
|
-
* 4. Writes back only if changed
|
|
3269
|
-
*/
|
|
3270
|
-
function syncWranglerConfig(projectDir, config) {
|
|
3271
|
-
// Find wrangler config file (prefer .jsonc, fall back to .json)
|
|
3272
|
-
const jsoncPath = path.join(projectDir, 'wrangler.jsonc');
|
|
3273
|
-
const jsonPath = path.join(projectDir, 'wrangler.json');
|
|
3274
|
-
const tomlPath = path.join(projectDir, 'wrangler.toml');
|
|
3275
|
-
let configPath;
|
|
3276
|
-
let isJsonc = false;
|
|
3277
|
-
if (fs.existsSync(jsoncPath)) {
|
|
3278
|
-
configPath = jsoncPath;
|
|
3279
|
-
isJsonc = true;
|
|
3280
|
-
}
|
|
3281
|
-
else if (fs.existsSync(jsonPath)) {
|
|
3282
|
-
configPath = jsonPath;
|
|
3283
|
-
}
|
|
3284
|
-
else if (fs.existsSync(tomlPath)) {
|
|
3285
|
-
// TOML is not supported for auto-sync — user must migrate to JSON/JSONC
|
|
3286
|
-
console.log('[kuratchi] wrangler.toml detected. Auto-sync requires wrangler.jsonc. Skipping wrangler sync.');
|
|
3287
|
-
return;
|
|
3288
|
-
}
|
|
3289
|
-
else {
|
|
3290
|
-
// No wrangler config exists — create a minimal wrangler.jsonc
|
|
3291
|
-
console.log('[kuratchi] Creating wrangler.jsonc with workflow config...');
|
|
3292
|
-
configPath = jsoncPath;
|
|
3293
|
-
isJsonc = true;
|
|
3294
|
-
}
|
|
3295
|
-
// Read existing config (or start fresh)
|
|
3296
|
-
let rawContent = '';
|
|
3297
|
-
let wranglerConfig = {};
|
|
3298
|
-
if (fs.existsSync(configPath)) {
|
|
3299
|
-
rawContent = fs.readFileSync(configPath, 'utf-8');
|
|
3300
|
-
try {
|
|
3301
|
-
// Strip JSONC comments for parsing
|
|
3302
|
-
const jsonContent = stripJsonComments(rawContent);
|
|
3303
|
-
wranglerConfig = JSON.parse(jsonContent);
|
|
3304
|
-
}
|
|
3305
|
-
catch (err) {
|
|
3306
|
-
console.error(`[kuratchi] Failed to parse ${path.basename(configPath)}: ${err.message}`);
|
|
3307
|
-
console.error('[kuratchi] Skipping wrangler sync. Please fix the JSON syntax.');
|
|
3308
|
-
return;
|
|
3309
|
-
}
|
|
3310
|
-
}
|
|
3311
|
-
let changed = false;
|
|
3312
|
-
// Sync workflows
|
|
3313
|
-
if (config.workflows.length > 0) {
|
|
3314
|
-
const existingWorkflows = wranglerConfig.workflows || [];
|
|
3315
|
-
const existingByBinding = new Map(existingWorkflows.map(w => [w.binding, w]));
|
|
3316
|
-
for (const wf of config.workflows) {
|
|
3317
|
-
// Convert SCREAMING_SNAKE binding to kebab-case name
|
|
3318
|
-
const name = wf.binding.toLowerCase().replace(/_/g, '-');
|
|
3319
|
-
const entry = {
|
|
3320
|
-
name,
|
|
3321
|
-
binding: wf.binding,
|
|
3322
|
-
class_name: wf.className,
|
|
3323
|
-
};
|
|
3324
|
-
const existing = existingByBinding.get(wf.binding);
|
|
3325
|
-
if (!existing) {
|
|
3326
|
-
existingWorkflows.push(entry);
|
|
3327
|
-
changed = true;
|
|
3328
|
-
console.log(`[kuratchi] Added workflow "${wf.binding}" to wrangler config`);
|
|
3329
|
-
}
|
|
3330
|
-
else if (existing.class_name !== wf.className) {
|
|
3331
|
-
existing.class_name = wf.className;
|
|
3332
|
-
changed = true;
|
|
3333
|
-
console.log(`[kuratchi] Updated workflow "${wf.binding}" class_name to "${wf.className}"`);
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
// Remove workflows that are no longer in kuratchi.config.ts
|
|
3337
|
-
const configBindings = new Set(config.workflows.map(w => w.binding));
|
|
3338
|
-
const filtered = existingWorkflows.filter(w => {
|
|
3339
|
-
if (!configBindings.has(w.binding)) {
|
|
3340
|
-
// Check if this was a kuratchi-managed workflow (has matching naming convention)
|
|
3341
|
-
const expectedName = w.binding.toLowerCase().replace(/_/g, '-');
|
|
3342
|
-
if (w.name === expectedName) {
|
|
3343
|
-
console.log(`[kuratchi] Removed workflow "${w.binding}" from wrangler config`);
|
|
3344
|
-
changed = true;
|
|
3345
|
-
return false;
|
|
3346
|
-
}
|
|
3347
|
-
}
|
|
3348
|
-
return true;
|
|
3349
|
-
});
|
|
3350
|
-
if (filtered.length !== existingWorkflows.length) {
|
|
3351
|
-
wranglerConfig.workflows = filtered;
|
|
3352
|
-
}
|
|
3353
|
-
else {
|
|
3354
|
-
wranglerConfig.workflows = existingWorkflows;
|
|
3355
|
-
}
|
|
3356
|
-
if (wranglerConfig.workflows.length === 0) {
|
|
3357
|
-
delete wranglerConfig.workflows;
|
|
3358
|
-
}
|
|
3359
|
-
}
|
|
3360
|
-
// Sync containers (similar pattern)
|
|
3361
|
-
if (config.containers.length > 0) {
|
|
3362
|
-
const existingContainers = wranglerConfig.containers || [];
|
|
3363
|
-
const existingByBinding = new Map(existingContainers.map(c => [c.binding, c]));
|
|
3364
|
-
for (const ct of config.containers) {
|
|
3365
|
-
const name = ct.binding.toLowerCase().replace(/_/g, '-');
|
|
3366
|
-
const entry = {
|
|
3367
|
-
name,
|
|
3368
|
-
binding: ct.binding,
|
|
3369
|
-
class_name: ct.className,
|
|
3370
|
-
};
|
|
3371
|
-
const existing = existingByBinding.get(ct.binding);
|
|
3372
|
-
if (!existing) {
|
|
3373
|
-
existingContainers.push(entry);
|
|
3374
|
-
changed = true;
|
|
3375
|
-
console.log(`[kuratchi] Added container "${ct.binding}" to wrangler config`);
|
|
3376
|
-
}
|
|
3377
|
-
else if (existing.class_name !== ct.className) {
|
|
3378
|
-
existing.class_name = ct.className;
|
|
3379
|
-
changed = true;
|
|
3380
|
-
console.log(`[kuratchi] Updated container "${ct.binding}" class_name to "${ct.className}"`);
|
|
3381
|
-
}
|
|
3382
|
-
}
|
|
3383
|
-
wranglerConfig.containers = existingContainers;
|
|
3384
|
-
if (wranglerConfig.containers.length === 0) {
|
|
3385
|
-
delete wranglerConfig.containers;
|
|
3386
|
-
}
|
|
3387
|
-
}
|
|
3388
|
-
// Sync durable_objects
|
|
3389
|
-
if (config.durableObjects.length > 0) {
|
|
3390
|
-
if (!wranglerConfig.durable_objects) {
|
|
3391
|
-
wranglerConfig.durable_objects = { bindings: [] };
|
|
3392
|
-
}
|
|
3393
|
-
const existingBindings = wranglerConfig.durable_objects.bindings || [];
|
|
3394
|
-
const existingByName = new Map(existingBindings.map(b => [b.name, b]));
|
|
3395
|
-
for (const doEntry of config.durableObjects) {
|
|
3396
|
-
const entry = {
|
|
3397
|
-
name: doEntry.binding,
|
|
3398
|
-
class_name: doEntry.className,
|
|
3399
|
-
};
|
|
3400
|
-
const existing = existingByName.get(doEntry.binding);
|
|
3401
|
-
if (!existing) {
|
|
3402
|
-
existingBindings.push(entry);
|
|
3403
|
-
changed = true;
|
|
3404
|
-
console.log(`[kuratchi] Added durable_object "${doEntry.binding}" to wrangler config`);
|
|
3405
|
-
}
|
|
3406
|
-
else if (existing.class_name !== doEntry.className) {
|
|
3407
|
-
existing.class_name = doEntry.className;
|
|
3408
|
-
changed = true;
|
|
3409
|
-
console.log(`[kuratchi] Updated durable_object "${doEntry.binding}" class_name to "${doEntry.className}"`);
|
|
3410
|
-
}
|
|
3411
|
-
}
|
|
3412
|
-
wranglerConfig.durable_objects.bindings = existingBindings;
|
|
3413
|
-
}
|
|
3414
|
-
if (!changed)
|
|
3415
|
-
return;
|
|
3416
|
-
// Write back with pretty formatting
|
|
3417
|
-
const newContent = JSON.stringify(wranglerConfig, null, '\t');
|
|
3418
|
-
writeIfChanged(configPath, newContent + '\n');
|
|
3419
|
-
}
|
|
3420
|
-
/**
|
|
3421
|
-
* Strip JSON comments (// and /* *\/) for parsing JSONC files.
|
|
3422
|
-
*/
|
|
3423
|
-
function stripJsonComments(content) {
|
|
3424
|
-
let result = '';
|
|
3425
|
-
let i = 0;
|
|
3426
|
-
let inString = false;
|
|
3427
|
-
let stringChar = '';
|
|
3428
|
-
while (i < content.length) {
|
|
3429
|
-
const ch = content[i];
|
|
3430
|
-
const next = content[i + 1];
|
|
3431
|
-
// Handle string literals
|
|
3432
|
-
if (inString) {
|
|
3433
|
-
result += ch;
|
|
3434
|
-
if (ch === '\\' && i + 1 < content.length) {
|
|
3435
|
-
result += next;
|
|
3436
|
-
i += 2;
|
|
3437
|
-
continue;
|
|
3438
|
-
}
|
|
3439
|
-
if (ch === stringChar) {
|
|
3440
|
-
inString = false;
|
|
3441
|
-
}
|
|
3442
|
-
i++;
|
|
3443
|
-
continue;
|
|
3444
|
-
}
|
|
3445
|
-
// Start of string
|
|
3446
|
-
if (ch === '"' || ch === "'") {
|
|
3447
|
-
inString = true;
|
|
3448
|
-
stringChar = ch;
|
|
3449
|
-
result += ch;
|
|
3450
|
-
i++;
|
|
3451
|
-
continue;
|
|
3452
|
-
}
|
|
3453
|
-
// Line comment
|
|
3454
|
-
if (ch === '/' && next === '/') {
|
|
3455
|
-
// Skip until end of line
|
|
3456
|
-
while (i < content.length && content[i] !== '\n')
|
|
3457
|
-
i++;
|
|
3458
|
-
continue;
|
|
3459
|
-
}
|
|
3460
|
-
// Block comment
|
|
3461
|
-
if (ch === '/' && next === '*') {
|
|
3462
|
-
i += 2;
|
|
3463
|
-
while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/'))
|
|
3464
|
-
i++;
|
|
3465
|
-
i += 2; // Skip */
|
|
3466
|
-
continue;
|
|
3467
|
-
}
|
|
3468
|
-
result += ch;
|
|
3469
|
-
i++;
|
|
3470
|
-
}
|
|
3471
|
-
return result;
|
|
3472
|
-
}
|