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