@kuratchi/js 0.0.21 → 0.0.22
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 +2 -3
- 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/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/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,8 +132,7 @@ 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);
|
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"
|
|
@@ -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;
|
package/dist/create.js
CHANGED
|
@@ -192,11 +192,11 @@ function scaffold(dir, opts) {
|
|
|
192
192
|
if (orm || enableDO) {
|
|
193
193
|
dirs.push('src/schemas');
|
|
194
194
|
}
|
|
195
|
-
if (orm) {
|
|
196
|
-
dirs.push('src/
|
|
195
|
+
if (orm || enableDO || auth) {
|
|
196
|
+
dirs.push('src/server');
|
|
197
197
|
}
|
|
198
198
|
if (enableDO) {
|
|
199
|
-
dirs.push('src/
|
|
199
|
+
dirs.push('src/routes/notes');
|
|
200
200
|
}
|
|
201
201
|
if (auth) {
|
|
202
202
|
dirs.push('src/routes/auth', 'src/routes/auth/login', 'src/routes/auth/signup', 'src/routes/admin');
|
|
@@ -214,19 +214,19 @@ function scaffold(dir, opts) {
|
|
|
214
214
|
write(dir, 'src/routes/index.html', genLandingPage(opts));
|
|
215
215
|
if (orm) {
|
|
216
216
|
write(dir, 'src/schemas/app.ts', genSchema(opts));
|
|
217
|
-
write(dir, 'src/
|
|
217
|
+
write(dir, 'src/server/items.ts', genItemsCrud());
|
|
218
218
|
write(dir, 'src/routes/items/index.html', genItemsPage());
|
|
219
219
|
}
|
|
220
220
|
if (enableDO) {
|
|
221
221
|
write(dir, 'src/schemas/notes.ts', genNotesSchema());
|
|
222
222
|
write(dir, 'src/server/notes.do.ts', genNotesDoHandler());
|
|
223
|
-
write(dir, 'src/
|
|
223
|
+
write(dir, 'src/server/notes.ts', genNotesDb());
|
|
224
224
|
write(dir, 'src/routes/notes/index.html', genNotesPage());
|
|
225
225
|
}
|
|
226
226
|
if (auth) {
|
|
227
227
|
write(dir, '.dev.vars', genDevVars());
|
|
228
|
-
write(dir, 'src/
|
|
229
|
-
write(dir, 'src/
|
|
228
|
+
write(dir, 'src/server/auth.ts', genAuthFunctions());
|
|
229
|
+
write(dir, 'src/server/admin.ts', genAdminLoader());
|
|
230
230
|
write(dir, 'src/routes/auth/login/index.html', genLoginPage());
|
|
231
231
|
write(dir, 'src/routes/auth/signup/index.html', genSignupPage());
|
|
232
232
|
write(dir, 'src/routes/admin/index.html', genAdminPage());
|
|
@@ -449,7 +449,7 @@ export async function deleteNote(id: number): Promise<void> {
|
|
|
449
449
|
}
|
|
450
450
|
function genNotesPage() {
|
|
451
451
|
return `<script>
|
|
452
|
-
import { getNotes, addNote, deleteNote } from '$
|
|
452
|
+
import { getNotes, addNote, deleteNote } from '$server/notes';
|
|
453
453
|
|
|
454
454
|
const notes = await getNotes();
|
|
455
455
|
</script>
|
|
@@ -473,7 +473,7 @@ if (notes.length === 0) {
|
|
|
473
473
|
for (const note of notes) {
|
|
474
474
|
<article>
|
|
475
475
|
<span>{note.title}</span>
|
|
476
|
-
<button data-
|
|
476
|
+
<button data-post={deleteNote(note.id)} data-refresh="" type="button">Remove</button>
|
|
477
477
|
</article>
|
|
478
478
|
}
|
|
479
479
|
</section>
|
|
@@ -631,10 +631,10 @@ ${types}
|
|
|
631
631
|
`;
|
|
632
632
|
}
|
|
633
633
|
function genItemsCrud() {
|
|
634
|
-
return `import { env } from 'cloudflare:workers';
|
|
635
|
-
import { kuratchiORM } from '@kuratchi/orm';
|
|
636
|
-
import { redirect } from '${FRAMEWORK_PACKAGE_NAME}';
|
|
637
|
-
import type { Item } from '
|
|
634
|
+
return `import { env } from 'cloudflare:workers';
|
|
635
|
+
import { kuratchiORM } from '@kuratchi/orm';
|
|
636
|
+
import { redirect } from '${FRAMEWORK_PACKAGE_NAME}';
|
|
637
|
+
import type { Item } from '../schemas/app';
|
|
638
638
|
|
|
639
639
|
const db = kuratchiORM(() => (env as any).DB);
|
|
640
640
|
|
|
@@ -664,7 +664,7 @@ export async function toggleItem(id: number): Promise<void> {
|
|
|
664
664
|
}
|
|
665
665
|
function genItemsPage() {
|
|
666
666
|
return `<script>
|
|
667
|
-
import { getItems, addItem, deleteItem, toggleItem } from '$
|
|
667
|
+
import { getItems, addItem, deleteItem, toggleItem } from '$server/items';
|
|
668
668
|
import EmptyState from '@kuratchi/ui/empty-state.html';
|
|
669
669
|
|
|
670
670
|
const items = await getItems();
|
|
@@ -690,10 +690,10 @@ if (items.length === 0) {
|
|
|
690
690
|
<article>
|
|
691
691
|
<span style={item.done ? 'text-decoration: line-through; opacity: 0.5' : ''}>{item.title}</span>
|
|
692
692
|
<div>
|
|
693
|
-
<button data-
|
|
693
|
+
<button data-post={toggleItem(item.id)} data-refresh="" type="button">
|
|
694
694
|
{item.done ? '↩' : '✓'}
|
|
695
695
|
</button>
|
|
696
|
-
<button data-
|
|
696
|
+
<button data-post={deleteItem(item.id)} data-refresh="" type="button">✕</button>
|
|
697
697
|
</div>
|
|
698
698
|
</article>
|
|
699
699
|
}
|
|
@@ -905,7 +905,7 @@ export async function getAdminData() {
|
|
|
905
905
|
}
|
|
906
906
|
function genLoginPage() {
|
|
907
907
|
return `<script>
|
|
908
|
-
import { signIn } from '$
|
|
908
|
+
import { signIn } from '$server/auth';
|
|
909
909
|
import AuthCard from '@kuratchi/ui/auth-card.html';
|
|
910
910
|
</script>
|
|
911
911
|
|
|
@@ -933,7 +933,7 @@ function genLoginPage() {
|
|
|
933
933
|
}
|
|
934
934
|
function genSignupPage() {
|
|
935
935
|
return `<script>
|
|
936
|
-
import { signUp } from '$
|
|
936
|
+
import { signUp } from '$server/auth';
|
|
937
937
|
import AuthCard from '@kuratchi/ui/auth-card.html';
|
|
938
938
|
</script>
|
|
939
939
|
|
|
@@ -965,7 +965,7 @@ function genSignupPage() {
|
|
|
965
965
|
}
|
|
966
966
|
function genAdminPage() {
|
|
967
967
|
return `<script>
|
|
968
|
-
import { getAdminData, signOut } from '$
|
|
968
|
+
import { getAdminData, signOut } from '$server/admin';
|
|
969
969
|
import Badge from '@kuratchi/ui/badge.html';
|
|
970
970
|
import Card from '@kuratchi/ui/card.html';
|
|
971
971
|
import DataList from '@kuratchi/ui/data-list.html';
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { SchemaValidationError, schema, } from './runtime/schema.js';
|
|
|
12
12
|
export { ActionError } from './runtime/action.js';
|
|
13
13
|
export { PageError } from './runtime/page-error.js';
|
|
14
14
|
export { extractSubdomainSlug, extractSlugFromPrefix, matchContainerViewPath, rewriteProxyLocationHeader, buildContainerRequest, createContainerEnvVars, startContainer, proxyToContainer, handleContainerRouting, forwardJsonPostToContainerDO, matchSiteViewPath, buildSiteContainerRequest, createWpContainerEnvVars, startSiteContainer, proxyToSiteContainer, } from './runtime/containers.js';
|
|
15
|
-
export type { AppConfig, kuratchiConfig, DatabaseConfig, AuthConfig, RouteContext, RouteModule, RuntimeContext, RuntimeDefinition, RuntimeStep, RuntimeNext, RuntimeErrorResult, } from './runtime/types.js';
|
|
15
|
+
export type { AppConfig, kuratchiConfig, DatabaseConfig, DesktopConfig, DesktopRemoteBindingConfig, DesktopWindowConfig, AuthConfig, RouteContext, RouteModule, RuntimeContext, RuntimeDefinition, RuntimeStep, RuntimeNext, RuntimeErrorResult, } from './runtime/types.js';
|
|
16
16
|
export type { RpcOf } from './runtime/do.js';
|
|
17
17
|
export type { SchemaType, InferSchema } from './runtime/schema.js';
|
|
18
18
|
export { url, pathname, searchParams, headers, method, params, slug } from './runtime/request.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface DesktopNotificationPayload {
|
|
2
|
+
title: string;
|
|
3
|
+
body?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface DesktopCommandRequest {
|
|
6
|
+
command: string;
|
|
7
|
+
workingDirectory?: string;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface DesktopCommandResult {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
error?: string | null;
|
|
13
|
+
exitCode: number;
|
|
14
|
+
durationMs: number;
|
|
15
|
+
stdout: string;
|
|
16
|
+
stderr: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function showDesktopNotification(payload: DesktopNotificationPayload): Promise<boolean>;
|
|
19
|
+
export declare function runDesktopCommand(request: DesktopCommandRequest): Promise<DesktopCommandResult | null>;
|