@kuratchi/js 0.0.21 → 0.0.23
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 +65 -29
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +90 -39
- package/dist/compiler/compiler-shared.d.ts +1 -0
- package/dist/compiler/config-reading.d.ts +6 -0
- package/dist/compiler/config-reading.js +92 -16
- package/dist/compiler/desktop-manifest.d.ts +48 -0
- package/dist/compiler/desktop-manifest.js +175 -0
- package/dist/compiler/durable-object-pipeline.d.ts +7 -3
- package/dist/compiler/durable-object-pipeline.js +30 -34
- package/dist/compiler/index.js +15 -5
- package/dist/compiler/parser.js +6 -0
- package/dist/compiler/routes-module-feature-blocks.js +5 -1
- package/dist/compiler/template.js +37 -3
- package/dist/compiler/worker-output-pipeline.d.ts +1 -0
- package/dist/compiler/worker-output-pipeline.js +11 -6
- package/dist/compiler/wrangler-sync.js +47 -0
- package/dist/create.js +19 -19
- package/dist/index.d.ts +1 -1
- package/dist/runtime/desktop.d.ts +19 -0
- package/dist/runtime/desktop.js +82 -0
- package/dist/runtime/generated-worker.js +11 -3
- package/dist/runtime/types.d.ts +37 -0
- package/package.json +1 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export function buildDesktopManifest(opts) {
|
|
4
|
+
const { projectDir, workerFile, desktopConfig, ormDatabases } = opts;
|
|
5
|
+
if (!desktopConfig)
|
|
6
|
+
return null;
|
|
7
|
+
const wranglerConfig = readDesktopWranglerConfig(projectDir);
|
|
8
|
+
const projectName = path.basename(projectDir);
|
|
9
|
+
const appName = desktopConfig.appName ?? projectName;
|
|
10
|
+
const appId = desktopConfig.appId ?? `dev.kuratchi.${projectName.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}`;
|
|
11
|
+
const assetsRoot = resolveAssetsRoot(projectDir);
|
|
12
|
+
const requestedRemoteBindings = desktopConfig.remoteBindings.length > 0
|
|
13
|
+
? desktopConfig.remoteBindings
|
|
14
|
+
: ormDatabases
|
|
15
|
+
.filter((entry) => entry.type === 'd1' && entry.remote)
|
|
16
|
+
.map((entry) => ({ binding: entry.binding, type: 'd1', remote: true }));
|
|
17
|
+
const remoteBindings = requestedRemoteBindings.map((binding) => {
|
|
18
|
+
if (binding.type === 'd1') {
|
|
19
|
+
const wranglerBinding = wranglerConfig.d1Databases.find((entry) => entry.binding === binding.binding && entry.remote);
|
|
20
|
+
if (!wranglerBinding) {
|
|
21
|
+
throw new Error(`Desktop manifest could not resolve remote D1 binding "${binding.binding}" from wrangler.jsonc.`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
binding: binding.binding,
|
|
25
|
+
type: 'd1',
|
|
26
|
+
remote: true,
|
|
27
|
+
databaseId: wranglerBinding.databaseId,
|
|
28
|
+
databaseName: wranglerBinding.databaseName,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const wranglerBinding = wranglerConfig.r2Buckets.find((entry) => entry.binding === binding.binding && entry.remote);
|
|
32
|
+
if (!wranglerBinding) {
|
|
33
|
+
throw new Error(`Desktop manifest could not resolve remote R2 binding "${binding.binding}" from wrangler.jsonc.`);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
binding: binding.binding,
|
|
37
|
+
type: 'r2',
|
|
38
|
+
remote: true,
|
|
39
|
+
bucketName: wranglerBinding.bucketName,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
formatVersion: 1,
|
|
44
|
+
generatedAt: new Date().toISOString(),
|
|
45
|
+
projectDir,
|
|
46
|
+
app: {
|
|
47
|
+
name: appName,
|
|
48
|
+
id: appId,
|
|
49
|
+
initialPath: desktopConfig.initialPath || '/',
|
|
50
|
+
window: {
|
|
51
|
+
title: desktopConfig.windowTitle ?? appName,
|
|
52
|
+
width: desktopConfig.windowWidth,
|
|
53
|
+
height: desktopConfig.windowHeight,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
runtime: {
|
|
57
|
+
workerEntrypoint: workerFile,
|
|
58
|
+
assetsRoot,
|
|
59
|
+
compatibilityDate: wranglerConfig.compatibilityDate,
|
|
60
|
+
compatibilityFlags: wranglerConfig.compatibilityFlags,
|
|
61
|
+
cloudflareAccountId: wranglerConfig.cloudflareAccountId,
|
|
62
|
+
},
|
|
63
|
+
bindings: {
|
|
64
|
+
desktop: {
|
|
65
|
+
notifications: desktopConfig.bindings.notifications,
|
|
66
|
+
files: desktopConfig.bindings.files,
|
|
67
|
+
},
|
|
68
|
+
remote: remoteBindings,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function writeDesktopManifest(projectDir, manifest) {
|
|
73
|
+
const outFile = path.join(projectDir, '.kuratchi', 'desktop.manifest.json');
|
|
74
|
+
const next = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
75
|
+
if (fs.existsSync(outFile)) {
|
|
76
|
+
const current = fs.readFileSync(outFile, 'utf-8');
|
|
77
|
+
if (current === next)
|
|
78
|
+
return outFile;
|
|
79
|
+
}
|
|
80
|
+
fs.writeFileSync(outFile, next, 'utf-8');
|
|
81
|
+
return outFile;
|
|
82
|
+
}
|
|
83
|
+
function resolveAssetsRoot(projectDir) {
|
|
84
|
+
const publicDir = path.join(projectDir, '.kuratchi', 'public');
|
|
85
|
+
if (fs.existsSync(publicDir))
|
|
86
|
+
return publicDir;
|
|
87
|
+
const srcAssets = path.join(projectDir, 'src', 'assets');
|
|
88
|
+
if (fs.existsSync(srcAssets))
|
|
89
|
+
return srcAssets;
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
function readDesktopWranglerConfig(projectDir) {
|
|
93
|
+
const configPath = ['wrangler.jsonc', 'wrangler.json']
|
|
94
|
+
.map((name) => path.join(projectDir, name))
|
|
95
|
+
.find((candidate) => fs.existsSync(candidate));
|
|
96
|
+
if (!configPath) {
|
|
97
|
+
throw new Error('Desktop runtime requires wrangler.jsonc or wrangler.json for compatibility settings and remote bindings.');
|
|
98
|
+
}
|
|
99
|
+
const rawConfig = fs.readFileSync(configPath, 'utf-8');
|
|
100
|
+
const parsed = JSON.parse(stripJsonComments(rawConfig));
|
|
101
|
+
if (!parsed.compatibility_date) {
|
|
102
|
+
throw new Error(`Missing compatibility_date in ${path.basename(configPath)}.`);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
compatibilityDate: parsed.compatibility_date,
|
|
106
|
+
compatibilityFlags: Array.isArray(parsed.compatibility_flags) ? parsed.compatibility_flags : [],
|
|
107
|
+
cloudflareAccountId: typeof parsed.account_id === 'string' && parsed.account_id.trim().length > 0
|
|
108
|
+
? parsed.account_id.trim()
|
|
109
|
+
: null,
|
|
110
|
+
d1Databases: Array.isArray(parsed.d1_databases)
|
|
111
|
+
? parsed.d1_databases
|
|
112
|
+
.filter((entry) => entry.binding && entry.database_id)
|
|
113
|
+
.map((entry) => ({
|
|
114
|
+
binding: entry.binding,
|
|
115
|
+
databaseId: entry.database_id,
|
|
116
|
+
databaseName: entry.database_name ?? null,
|
|
117
|
+
remote: entry.remote !== false,
|
|
118
|
+
}))
|
|
119
|
+
: [],
|
|
120
|
+
r2Buckets: Array.isArray(parsed.r2_buckets)
|
|
121
|
+
? parsed.r2_buckets
|
|
122
|
+
.filter((entry) => entry.binding && entry.bucket_name)
|
|
123
|
+
.map((entry) => ({
|
|
124
|
+
binding: entry.binding,
|
|
125
|
+
bucketName: entry.bucket_name,
|
|
126
|
+
remote: entry.remote !== false,
|
|
127
|
+
}))
|
|
128
|
+
: [],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function stripJsonComments(content) {
|
|
132
|
+
let result = '';
|
|
133
|
+
let i = 0;
|
|
134
|
+
let inString = false;
|
|
135
|
+
let stringChar = '';
|
|
136
|
+
while (i < content.length) {
|
|
137
|
+
const ch = content[i];
|
|
138
|
+
const next = content[i + 1];
|
|
139
|
+
if (inString) {
|
|
140
|
+
result += ch;
|
|
141
|
+
if (ch === '\\' && i + 1 < content.length) {
|
|
142
|
+
result += next;
|
|
143
|
+
i += 2;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (ch === stringChar) {
|
|
147
|
+
inString = false;
|
|
148
|
+
}
|
|
149
|
+
i++;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (ch === '"' || ch === '\'') {
|
|
153
|
+
inString = true;
|
|
154
|
+
stringChar = ch;
|
|
155
|
+
result += ch;
|
|
156
|
+
i++;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (ch === '/' && next === '/') {
|
|
160
|
+
while (i < content.length && content[i] !== '\n')
|
|
161
|
+
i++;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (ch === '/' && next === '*') {
|
|
165
|
+
i += 2;
|
|
166
|
+
while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/'))
|
|
167
|
+
i++;
|
|
168
|
+
i += 2;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
result += ch;
|
|
172
|
+
i++;
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
export declare function discoverDurableObjects(srcDir: string
|
|
3
|
-
config:
|
|
1
|
+
import { type DoHandlerEntry } from './compiler-shared.js';
|
|
2
|
+
export declare function discoverDurableObjects(srcDir: string): {
|
|
3
|
+
config: {
|
|
4
|
+
binding: string;
|
|
5
|
+
className: string;
|
|
6
|
+
files?: string[];
|
|
7
|
+
}[];
|
|
4
8
|
handlers: DoHandlerEntry[];
|
|
5
9
|
};
|
|
6
10
|
export declare function generateHandlerProxy(handler: DoHandlerEntry, opts: {
|
|
@@ -3,27 +3,14 @@ import * as path from 'node:path';
|
|
|
3
3
|
import * as ts from 'typescript';
|
|
4
4
|
import { toSafeIdentifier, } from './compiler-shared.js';
|
|
5
5
|
import { discoverFilesWithExtensions, discoverFilesWithSuffix } from './convention-discovery.js';
|
|
6
|
-
export function discoverDurableObjects(srcDir
|
|
6
|
+
export function discoverDurableObjects(srcDir) {
|
|
7
7
|
const serverDir = path.join(srcDir, 'server');
|
|
8
8
|
const legacyDir = path.join(srcDir, 'durable-objects');
|
|
9
9
|
const serverDoFiles = discoverFilesWithSuffix(serverDir, '.do.ts');
|
|
10
10
|
const legacyDoFiles = discoverFilesWithSuffix(legacyDir, '.ts');
|
|
11
11
|
const discoveredFiles = Array.from(new Set([...serverDoFiles, ...legacyDoFiles]));
|
|
12
12
|
if (discoveredFiles.length === 0) {
|
|
13
|
-
return { config:
|
|
14
|
-
}
|
|
15
|
-
const bindings = new Set(configDoEntries.map((d) => d.binding));
|
|
16
|
-
const fileToBinding = new Map();
|
|
17
|
-
for (const entry of configDoEntries) {
|
|
18
|
-
for (const rawFile of entry.files ?? []) {
|
|
19
|
-
const normalized = rawFile.trim().replace(/^\.?[\\/]/, '').replace(/\\/g, '/').toLowerCase();
|
|
20
|
-
if (!normalized)
|
|
21
|
-
continue;
|
|
22
|
-
fileToBinding.set(normalized, entry.binding);
|
|
23
|
-
const base = path.basename(normalized);
|
|
24
|
-
if (!fileToBinding.has(base))
|
|
25
|
-
fileToBinding.set(base, entry.binding);
|
|
26
|
-
}
|
|
13
|
+
return { config: [], handlers: [] };
|
|
27
14
|
}
|
|
28
15
|
const handlers = [];
|
|
29
16
|
const handlerIdToAbsPath = new Map();
|
|
@@ -48,28 +35,17 @@ export function discoverDurableObjects(srcDir, configDoEntries, ormDatabases) {
|
|
|
48
35
|
continue;
|
|
49
36
|
// Binding resolution:
|
|
50
37
|
// 1) explicit static binding declared in the class
|
|
51
|
-
// 2)
|
|
52
|
-
// 3) if exactly one binding exists, infer it
|
|
38
|
+
// 2) derive from the handler file name
|
|
53
39
|
let binding = null;
|
|
54
40
|
const bindingMatch = source.match(/static\s+binding\s*=\s*['"](\w+)['"]/);
|
|
55
41
|
if (bindingMatch) {
|
|
56
42
|
binding = bindingMatch[1];
|
|
57
43
|
}
|
|
58
44
|
else {
|
|
59
|
-
|
|
60
|
-
const normalizedRelFromSrc = path.relative(srcDir, absPath).replace(/\\/g, '/').toLowerCase();
|
|
61
|
-
binding = className ? (configDoEntries.find((entry) => entry.className === className)?.binding ?? null) : null;
|
|
62
|
-
if (!binding) {
|
|
63
|
-
binding = fileToBinding.get(normalizedRelFromSrc) ?? fileToBinding.get(normalizedFile) ?? null;
|
|
64
|
-
}
|
|
65
|
-
if (!binding && configDoEntries.length === 1) {
|
|
66
|
-
binding = configDoEntries[0].binding;
|
|
67
|
-
}
|
|
45
|
+
binding = deriveBindingFromFile(file);
|
|
68
46
|
}
|
|
69
47
|
if (!binding)
|
|
70
48
|
continue;
|
|
71
|
-
if (!bindings.has(binding))
|
|
72
|
-
continue;
|
|
73
49
|
const classMethods = className ? extractClassMethods(absPath, source, className) : [];
|
|
74
50
|
const fileName = path
|
|
75
51
|
.relative(absPath.startsWith(serverDir) ? serverDir : legacyDir, absPath)
|
|
@@ -103,17 +79,14 @@ export function discoverDurableObjects(srcDir, configDoEntries, ormDatabases) {
|
|
|
103
79
|
}
|
|
104
80
|
}
|
|
105
81
|
// Build config entries from discovered handlers (de-duped by binding).
|
|
106
|
-
//
|
|
82
|
+
// The handler source is authoritative; wrangler.jsonc is then synced from this.
|
|
107
83
|
const discoveredConfigByBinding = new Map();
|
|
108
84
|
for (const handler of handlers) {
|
|
109
|
-
const configEntry = configDoEntries.find((e) => e.binding === handler.binding);
|
|
110
85
|
const existing = discoveredConfigByBinding.get(handler.binding);
|
|
111
86
|
if (!existing) {
|
|
112
87
|
discoveredConfigByBinding.set(handler.binding, {
|
|
113
88
|
binding: handler.binding,
|
|
114
|
-
|
|
115
|
-
className: configEntry?.className ?? handler.className ?? handler.binding,
|
|
116
|
-
stubId: configEntry?.stubId,
|
|
89
|
+
className: handler.className ?? deriveClassNameFromFile(path.basename(handler.absPath)),
|
|
117
90
|
files: [path.basename(handler.absPath)],
|
|
118
91
|
});
|
|
119
92
|
}
|
|
@@ -121,9 +94,32 @@ export function discoverDurableObjects(srcDir, configDoEntries, ormDatabases) {
|
|
|
121
94
|
existing.files?.push(path.basename(handler.absPath));
|
|
122
95
|
}
|
|
123
96
|
}
|
|
124
|
-
void ormDatabases;
|
|
125
97
|
return { config: [...discoveredConfigByBinding.values()], handlers };
|
|
126
98
|
}
|
|
99
|
+
function deriveBindingFromFile(fileName) {
|
|
100
|
+
const normalized = fileName
|
|
101
|
+
.replace(/\.(ts|tsx|js|mjs|cjs)$/i, '')
|
|
102
|
+
.replace(/\.do$/i, '')
|
|
103
|
+
.replace(/[^A-Za-z0-9]+/g, '_')
|
|
104
|
+
.replace(/^_+|_+$/g, '')
|
|
105
|
+
.toUpperCase();
|
|
106
|
+
if (!normalized)
|
|
107
|
+
return 'DURABLE_OBJECT';
|
|
108
|
+
return normalized.endsWith('_DO') ? normalized : `${normalized}_DO`;
|
|
109
|
+
}
|
|
110
|
+
function deriveClassNameFromFile(fileName) {
|
|
111
|
+
const normalized = fileName
|
|
112
|
+
.replace(/\.(ts|tsx|js|mjs|cjs)$/i, '')
|
|
113
|
+
.replace(/\.do$/i, '');
|
|
114
|
+
const base = normalized
|
|
115
|
+
.split(/[^A-Za-z0-9]+/)
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
118
|
+
.join('');
|
|
119
|
+
if (!base)
|
|
120
|
+
return 'DurableObjectHandler';
|
|
121
|
+
return base.endsWith('DO') ? base : `${base}DO`;
|
|
122
|
+
}
|
|
127
123
|
// ---------------------------------------------------------------------------
|
|
128
124
|
// TypeScript AST helpers
|
|
129
125
|
// ---------------------------------------------------------------------------
|
package/dist/compiler/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { compileAssets } from './asset-pipeline.js';
|
|
|
7
7
|
import { compileApiRoute } from './api-route-pipeline.js';
|
|
8
8
|
import { createClientModuleCompiler } from './client-module-pipeline.js';
|
|
9
9
|
import { createComponentCompiler } from './component-pipeline.js';
|
|
10
|
-
import { readAssetsPrefix, readAuthConfig,
|
|
10
|
+
import { readAssetsPrefix, readAuthConfig, readOrmConfig, readSecurityConfig, readUiConfigValues, readUiTheme, } from './config-reading.js';
|
|
11
11
|
import { discoverContainerFiles, discoverConventionClassFiles, discoverWorkflowFiles, } from './convention-discovery.js';
|
|
12
12
|
import { discoverDurableObjects, generateHandlerProxy } from './durable-object-pipeline.js';
|
|
13
13
|
import { compileErrorPages } from './error-page-pipeline.js';
|
|
@@ -132,12 +132,11 @@ export async function compile(options) {
|
|
|
132
132
|
// Read security config from kuratchi.config.ts
|
|
133
133
|
const securityConfig = readSecurityConfig(projectDir);
|
|
134
134
|
// Auto-discover Durable Objects from .do.ts files (config optional, only needed for stubId)
|
|
135
|
-
const
|
|
136
|
-
const { config: doConfig, handlers: doHandlers } = discoverDurableObjects(srcDir, configDoEntries, ormDatabases);
|
|
135
|
+
const { config: doConfig, handlers: doHandlers } = discoverDurableObjects(srcDir);
|
|
137
136
|
// Auto-discover convention-based worker class files (no config needed)
|
|
138
137
|
const containerConfig = discoverContainerFiles(projectDir);
|
|
139
138
|
const workflowConfig = discoverWorkflowFiles(projectDir);
|
|
140
|
-
const agentConfig = discoverConventionClassFiles(projectDir, path.join('src', 'server'), '.
|
|
139
|
+
const agentConfig = discoverConventionClassFiles(projectDir, path.join('src', 'server'), '.agents.ts', '.agents');
|
|
141
140
|
// Generate handler proxy modules in .kuratchi/do/ (must happen BEFORE route processing
|
|
142
141
|
// so that $durable-objects/X imports can be redirected to the generated proxies)
|
|
143
142
|
const doProxyDir = path.join(projectDir, '.kuratchi', 'do');
|
|
@@ -302,12 +301,18 @@ export async function compile(options) {
|
|
|
302
301
|
// routes.ts already exports the default fetch handler and all named DO classes;
|
|
303
302
|
// worker.ts explicitly re-exports them so wrangler.jsonc can reference a
|
|
304
303
|
// stable filename while routes.ts is freely regenerated.
|
|
304
|
+
// If user has src/index.ts with a default export, merge it with the generated worker
|
|
305
|
+
// to support scheduled, queue, and other Cloudflare Worker handlers.
|
|
306
|
+
const userIndexFile = path.join(srcDir, 'index.ts');
|
|
307
|
+
const hasUserIndex = fs.existsSync(userIndexFile) &&
|
|
308
|
+
fs.readFileSync(userIndexFile, 'utf-8').includes('export default');
|
|
305
309
|
const workerFile = path.join(outDir, 'worker.ts');
|
|
306
310
|
writeIfChanged(workerFile, buildWorkerEntrypointSource({
|
|
307
311
|
projectDir,
|
|
308
312
|
outDir,
|
|
309
313
|
doClassNames: doConfig.map((entry) => entry.className),
|
|
310
314
|
workerClassEntries: [...agentConfig, ...containerConfig, ...workflowConfig],
|
|
315
|
+
hasUserIndex,
|
|
311
316
|
}));
|
|
312
317
|
writeIfChanged(path.join(outDir, 'worker.js'), buildCompatEntrypointSource('./worker.ts'));
|
|
313
318
|
// Auto-sync wrangler.jsonc with workflow/container/DO config from kuratchi.config.ts
|
|
@@ -324,12 +329,17 @@ export async function compile(options) {
|
|
|
324
329
|
copyDirIfChanged(srcAssetsDir, publicAssetsDir);
|
|
325
330
|
syncedAssetsDirectory = path.relative(projectDir, publicDir).replace(/\\/g, '/');
|
|
326
331
|
}
|
|
332
|
+
// Convert agent config to DO config format (agents are Durable Objects)
|
|
333
|
+
const agentDoConfig = agentConfig.map((entry) => {
|
|
334
|
+
const binding = entry.className.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase();
|
|
335
|
+
return { binding, className: entry.className };
|
|
336
|
+
});
|
|
327
337
|
syncWranglerConfigPipeline({
|
|
328
338
|
projectDir,
|
|
329
339
|
config: {
|
|
330
340
|
workflows: workflowConfig,
|
|
331
341
|
containers: containerConfig,
|
|
332
|
-
durableObjects: doConfig,
|
|
342
|
+
durableObjects: [...doConfig, ...agentDoConfig],
|
|
333
343
|
assetsDirectory: syncedAssetsDirectory,
|
|
334
344
|
},
|
|
335
345
|
writeFile: writeIfChanged,
|
package/dist/compiler/parser.js
CHANGED
|
@@ -1135,6 +1135,7 @@ export function parseFile(source, options = {}) {
|
|
|
1135
1135
|
const actionFunctions = [];
|
|
1136
1136
|
const pollFunctions = [];
|
|
1137
1137
|
const dataGetQueries = [];
|
|
1138
|
+
let warnedLegacyActionAttrs = false;
|
|
1138
1139
|
for (const tag of templateTags) {
|
|
1139
1140
|
if (tag.closing)
|
|
1140
1141
|
continue;
|
|
@@ -1143,6 +1144,11 @@ export function parseFile(source, options = {}) {
|
|
|
1143
1144
|
actionFunctions.push(actionExpr);
|
|
1144
1145
|
}
|
|
1145
1146
|
for (const [attrName, attrValue] of tag.attrs.entries()) {
|
|
1147
|
+
if (!warnedLegacyActionAttrs && (attrName === 'data-action' || attrName === 'data-args')) {
|
|
1148
|
+
warnedLegacyActionAttrs = true;
|
|
1149
|
+
console.warn(`[kuratchi] ${options.filePath || kind}: authored data-action/data-args are deprecated. ` +
|
|
1150
|
+
`Use data-post={fn(...)} or action={fn} instead.`);
|
|
1151
|
+
}
|
|
1146
1152
|
if (/^on[A-Za-z]+$/i.test(attrName)) {
|
|
1147
1153
|
const actionCall = extractCallExpression(attrValue);
|
|
1148
1154
|
if (actionCall && !actionFunctions.includes(actionCall.fnName))
|
|
@@ -383,7 +383,11 @@ function buildDurableObjectBlock(opts) {
|
|
|
383
383
|
doResolverLines.push(` });`);
|
|
384
384
|
}
|
|
385
385
|
else {
|
|
386
|
-
doResolverLines.push(`
|
|
386
|
+
doResolverLines.push(` __registerDoResolver('${doEntry.binding}', async () => {`);
|
|
387
|
+
doResolverLines.push(` const __ns = __env['${doEntry.binding}'];`);
|
|
388
|
+
doResolverLines.push(` if (!__ns?.idFromName || !__ns?.get) return null;`);
|
|
389
|
+
doResolverLines.push(` return __ns.get(__ns.idFromName('global'));`);
|
|
390
|
+
doResolverLines.push(` });`);
|
|
387
391
|
}
|
|
388
392
|
}
|
|
389
393
|
return {
|
|
@@ -37,6 +37,36 @@ const JS_CONTROL_PATTERNS = [
|
|
|
37
37
|
function isJsControlLine(line) {
|
|
38
38
|
return JS_CONTROL_PATTERNS.some(p => p.test(line));
|
|
39
39
|
}
|
|
40
|
+
/** HTML boolean attributes that should be present or absent, never have a value */
|
|
41
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
42
|
+
'disabled',
|
|
43
|
+
'checked',
|
|
44
|
+
'selected',
|
|
45
|
+
'readonly',
|
|
46
|
+
'required',
|
|
47
|
+
'hidden',
|
|
48
|
+
'open',
|
|
49
|
+
'autofocus',
|
|
50
|
+
'autoplay',
|
|
51
|
+
'controls',
|
|
52
|
+
'default',
|
|
53
|
+
'defer',
|
|
54
|
+
'formnovalidate',
|
|
55
|
+
'inert',
|
|
56
|
+
'ismap',
|
|
57
|
+
'itemscope',
|
|
58
|
+
'loop',
|
|
59
|
+
'multiple',
|
|
60
|
+
'muted',
|
|
61
|
+
'nomodule',
|
|
62
|
+
'novalidate',
|
|
63
|
+
'playsinline',
|
|
64
|
+
'reversed',
|
|
65
|
+
'async',
|
|
66
|
+
]);
|
|
67
|
+
function isBooleanAttribute(name) {
|
|
68
|
+
return BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
|
|
69
|
+
}
|
|
40
70
|
const FRAGMENT_OPEN_MARKER = '<!--__KURATCHI_FRAGMENT_OPEN:';
|
|
41
71
|
const FRAGMENT_CLOSE_MARKER = '<!--__KURATCHI_FRAGMENT_CLOSE-->';
|
|
42
72
|
export function splitTemplateRenderSections(template) {
|
|
@@ -1130,9 +1160,13 @@ function compileHtmlSegment(line, actionNames, rpcNameMap, options = {}) {
|
|
|
1130
1160
|
pos = closeIdx + 1;
|
|
1131
1161
|
continue;
|
|
1132
1162
|
}
|
|
1133
|
-
else if (attrName
|
|
1134
|
-
// Boolean attributes: disabled={expr}
|
|
1135
|
-
|
|
1163
|
+
else if (isBooleanAttribute(attrName)) {
|
|
1164
|
+
// Boolean attributes: disabled={expr} → conditionally include the attribute or omit entirely
|
|
1165
|
+
// Remove the trailing "attrName=" we already appended, we'll handle it with a ternary
|
|
1166
|
+
result = result.replace(new RegExp(`\\s*${attrName}=$`), '');
|
|
1167
|
+
result += `\${${inner} ? ' ${attrName}' : ''}`;
|
|
1168
|
+
pos = closeIdx + 1;
|
|
1169
|
+
continue;
|
|
1136
1170
|
}
|
|
1137
1171
|
else {
|
|
1138
1172
|
// Regular attribute: value={expr} → value="escaped"
|
|
@@ -10,5 +10,6 @@ export declare function buildWorkerEntrypointSource(opts: {
|
|
|
10
10
|
outDir: string;
|
|
11
11
|
doClassNames: string[];
|
|
12
12
|
workerClassEntries: WorkerClassExportEntry[];
|
|
13
|
+
hasUserIndex?: boolean;
|
|
13
14
|
}): string;
|
|
14
15
|
export declare function buildCompatEntrypointSource(targetFile: string): string;
|
|
@@ -29,13 +29,18 @@ export function buildWorkerEntrypointSource(opts) {
|
|
|
29
29
|
}
|
|
30
30
|
return `export { ${entry.className} } from '${importPath}';`;
|
|
31
31
|
});
|
|
32
|
-
|
|
32
|
+
const lines = [
|
|
33
33
|
'// Auto-generated by kuratchi — do not edit.',
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'',
|
|
38
|
-
|
|
34
|
+
];
|
|
35
|
+
// If user has src/index.ts with a default export, merge it with the generated fetch handler
|
|
36
|
+
if (opts.hasUserIndex) {
|
|
37
|
+
lines.push("import __generatedWorker from './routes.ts';", "import __userWorker from '../src/index.ts';", '', '// Merge user worker exports (scheduled, queue, etc.) with generated fetch handler', 'export default {', ' ...__userWorker,', ' fetch: __generatedWorker.fetch,', '};');
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
lines.push("export { default } from './routes.ts';");
|
|
41
|
+
}
|
|
42
|
+
lines.push(...opts.doClassNames.map((className) => `export { ${className} } from './routes.ts';`), ...workerClassExports, '');
|
|
43
|
+
return lines.join('\n');
|
|
39
44
|
}
|
|
40
45
|
export function buildCompatEntrypointSource(targetFile) {
|
|
41
46
|
return [
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
function nextMigrationTag(existing) {
|
|
4
|
+
const used = new Set(existing
|
|
5
|
+
.map((entry) => (entry && typeof entry.tag === 'string' ? entry.tag : null))
|
|
6
|
+
.filter(Boolean));
|
|
7
|
+
let maxNumeric = 0;
|
|
8
|
+
for (const tag of used) {
|
|
9
|
+
const match = /^v(\d+)$/i.exec(String(tag));
|
|
10
|
+
if (match) {
|
|
11
|
+
maxNumeric = Math.max(maxNumeric, Number(match[1]));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
let candidate = `v${maxNumeric > 0 ? maxNumeric + 1 : 1}`;
|
|
15
|
+
if (!used.has(candidate))
|
|
16
|
+
return candidate;
|
|
17
|
+
let index = maxNumeric + 1;
|
|
18
|
+
while (used.has(candidate)) {
|
|
19
|
+
index += 1;
|
|
20
|
+
candidate = `v${index}`;
|
|
21
|
+
}
|
|
22
|
+
return candidate;
|
|
23
|
+
}
|
|
3
24
|
function stripJsonComments(content) {
|
|
4
25
|
let result = '';
|
|
5
26
|
let i = 0;
|
|
@@ -169,8 +190,34 @@ export function syncWranglerConfig(opts) {
|
|
|
169
190
|
changed = true;
|
|
170
191
|
console.log(`[kuratchi] Added durable_object "${durableObject.binding}" to wrangler config`);
|
|
171
192
|
}
|
|
193
|
+
else if (existing.class_name !== durableObject.className) {
|
|
194
|
+
existing.class_name = durableObject.className;
|
|
195
|
+
changed = true;
|
|
196
|
+
console.log(`[kuratchi] Updated durable_object "${durableObject.binding}" class_name to "${durableObject.className}"`);
|
|
197
|
+
}
|
|
172
198
|
}
|
|
173
199
|
wranglerConfig.durable_objects.bindings = existingBindings;
|
|
200
|
+
const existingMigrations = Array.isArray(wranglerConfig.migrations) ? wranglerConfig.migrations : [];
|
|
201
|
+
const knownClasses = new Set();
|
|
202
|
+
for (const migration of existingMigrations) {
|
|
203
|
+
const newClasses = Array.isArray(migration?.new_sqlite_classes) ? migration.new_sqlite_classes : [];
|
|
204
|
+
for (const className of newClasses) {
|
|
205
|
+
if (typeof className === 'string' && className)
|
|
206
|
+
knownClasses.add(className);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const missingClasses = opts.config.durableObjects
|
|
210
|
+
.map((entry) => entry.className)
|
|
211
|
+
.filter((className) => !knownClasses.has(className));
|
|
212
|
+
if (missingClasses.length > 0) {
|
|
213
|
+
existingMigrations.push({
|
|
214
|
+
tag: nextMigrationTag(existingMigrations),
|
|
215
|
+
new_sqlite_classes: missingClasses,
|
|
216
|
+
});
|
|
217
|
+
wranglerConfig.migrations = existingMigrations;
|
|
218
|
+
changed = true;
|
|
219
|
+
console.log(`[kuratchi] Added durable object migration for ${missingClasses.join(', ')}`);
|
|
220
|
+
}
|
|
174
221
|
}
|
|
175
222
|
if (opts.config.assetsDirectory !== undefined) {
|
|
176
223
|
const existing = wranglerConfig.assets;
|