@kuratchi/js 0.0.15 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -8
- package/dist/cli.js +80 -47
- package/dist/compiler/api-route-pipeline.d.ts +8 -0
- package/dist/compiler/api-route-pipeline.js +23 -0
- package/dist/compiler/asset-pipeline.d.ts +7 -0
- package/dist/compiler/asset-pipeline.js +33 -0
- package/dist/compiler/client-module-pipeline.d.ts +25 -0
- package/dist/compiler/client-module-pipeline.js +257 -0
- package/dist/compiler/compiler-shared.d.ts +55 -0
- package/dist/compiler/compiler-shared.js +4 -0
- package/dist/compiler/component-pipeline.d.ts +15 -0
- package/dist/compiler/component-pipeline.js +163 -0
- package/dist/compiler/config-reading.d.ts +11 -0
- package/dist/compiler/config-reading.js +323 -0
- package/dist/compiler/convention-discovery.d.ts +9 -0
- package/dist/compiler/convention-discovery.js +83 -0
- package/dist/compiler/durable-object-pipeline.d.ts +9 -0
- package/dist/compiler/durable-object-pipeline.js +255 -0
- package/dist/compiler/error-page-pipeline.d.ts +1 -0
- package/dist/compiler/error-page-pipeline.js +16 -0
- package/dist/compiler/import-linking.d.ts +36 -0
- package/dist/compiler/import-linking.js +139 -0
- package/dist/compiler/index.d.ts +3 -3
- package/dist/compiler/index.js +133 -3305
- package/dist/compiler/layout-pipeline.d.ts +31 -0
- package/dist/compiler/layout-pipeline.js +155 -0
- package/dist/compiler/page-route-pipeline.d.ts +16 -0
- package/dist/compiler/page-route-pipeline.js +62 -0
- package/dist/compiler/parser.d.ts +4 -0
- package/dist/compiler/parser.js +433 -51
- package/dist/compiler/root-layout-pipeline.d.ts +10 -0
- package/dist/compiler/root-layout-pipeline.js +517 -0
- package/dist/compiler/route-discovery.d.ts +7 -0
- package/dist/compiler/route-discovery.js +87 -0
- package/dist/compiler/route-pipeline.d.ts +57 -0
- package/dist/compiler/route-pipeline.js +296 -0
- package/dist/compiler/route-state-pipeline.d.ts +25 -0
- package/dist/compiler/route-state-pipeline.js +139 -0
- package/dist/compiler/routes-module-feature-blocks.d.ts +2 -0
- package/dist/compiler/routes-module-feature-blocks.js +330 -0
- package/dist/compiler/routes-module-pipeline.d.ts +2 -0
- package/dist/compiler/routes-module-pipeline.js +6 -0
- package/dist/compiler/routes-module-runtime-shell.d.ts +2 -0
- package/dist/compiler/routes-module-runtime-shell.js +81 -0
- package/dist/compiler/routes-module-types.d.ts +44 -0
- package/dist/compiler/routes-module-types.js +1 -0
- package/dist/compiler/script-transform.d.ts +16 -0
- package/dist/compiler/script-transform.js +218 -0
- package/dist/compiler/server-module-pipeline.d.ts +13 -0
- package/dist/compiler/server-module-pipeline.js +124 -0
- package/dist/compiler/template.d.ts +13 -1
- package/dist/compiler/template.js +315 -58
- package/dist/compiler/worker-output-pipeline.d.ts +13 -0
- package/dist/compiler/worker-output-pipeline.js +37 -0
- package/dist/compiler/wrangler-sync.d.ts +14 -0
- package/dist/compiler/wrangler-sync.js +185 -0
- package/dist/runtime/app.js +15 -3
- package/dist/runtime/generated-worker.d.ts +33 -0
- package/dist/runtime/generated-worker.js +412 -0
- package/dist/runtime/index.d.ts +2 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/router.d.ts +2 -1
- package/dist/runtime/router.js +12 -3
- package/dist/runtime/types.d.ts +8 -2
- package/package.json +5 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { parseFile, stripTopLevelImports } from './parser.js';
|
|
5
|
+
import { compileTemplate } from './template.js';
|
|
6
|
+
import { transpileTypeScript } from './transpile.js';
|
|
7
|
+
import { buildDevAliasDeclarations } from './script-transform.js';
|
|
8
|
+
function resolvePackageComponent(projectDir, pkgName, componentFile) {
|
|
9
|
+
const nmPath = path.join(projectDir, 'node_modules', pkgName, 'src', 'lib', componentFile + '.html');
|
|
10
|
+
if (fs.existsSync(nmPath))
|
|
11
|
+
return nmPath;
|
|
12
|
+
const pkgDirName = pkgName.replace(/^@/, '').replace(/\//g, '-');
|
|
13
|
+
const workspaceRoot = path.resolve(projectDir, '../..');
|
|
14
|
+
const wsPath = path.join(workspaceRoot, 'packages', pkgDirName, 'src', 'lib', componentFile + '.html');
|
|
15
|
+
if (fs.existsSync(wsPath))
|
|
16
|
+
return wsPath;
|
|
17
|
+
const rootNmPath = path.join(workspaceRoot, 'node_modules', pkgName, 'src', 'lib', componentFile + '.html');
|
|
18
|
+
if (fs.existsSync(rootNmPath))
|
|
19
|
+
return rootNmPath;
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
function escapeTemplateLiteral(source) {
|
|
23
|
+
return source.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
24
|
+
}
|
|
25
|
+
function scopeComponentCss(source, scopeHash) {
|
|
26
|
+
return source.replace(/([^{}]+)\{/g, (_match, selectors) => {
|
|
27
|
+
const scoped = selectors
|
|
28
|
+
.split(',')
|
|
29
|
+
.map((selector) => `.${scopeHash} ${selector.trim()}`)
|
|
30
|
+
.join(', ');
|
|
31
|
+
return scoped + ' {';
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function createComponentCompiler(options) {
|
|
35
|
+
const { projectDir, srcDir, isDev } = options;
|
|
36
|
+
const libDir = path.join(srcDir, 'lib');
|
|
37
|
+
const compiledComponentCache = new Map();
|
|
38
|
+
const componentStyleCache = new Map();
|
|
39
|
+
const componentActionCache = new Map();
|
|
40
|
+
function ensureCompiled(fileName) {
|
|
41
|
+
if (compiledComponentCache.has(fileName))
|
|
42
|
+
return compiledComponentCache.get(fileName);
|
|
43
|
+
let filePath;
|
|
44
|
+
let funcName;
|
|
45
|
+
const pkgMatch = fileName.match(/^(@[^:]+):(.+)$/);
|
|
46
|
+
if (pkgMatch) {
|
|
47
|
+
const pkgName = pkgMatch[1];
|
|
48
|
+
const componentFile = pkgMatch[2];
|
|
49
|
+
funcName = '__c_' + componentFile.replace(/[\/\-]/g, '_');
|
|
50
|
+
filePath = resolvePackageComponent(projectDir, pkgName, componentFile);
|
|
51
|
+
if (!filePath || !fs.existsSync(filePath))
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
funcName = '__c_' + fileName.replace(/[\/\-]/g, '_');
|
|
56
|
+
filePath = path.join(libDir, fileName + '.html');
|
|
57
|
+
if (!fs.existsSync(filePath))
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const scopeHash = 'dz-' + crypto.createHash('md5').update(fileName).digest('hex').slice(0, 6);
|
|
61
|
+
const rawSource = fs.readFileSync(filePath, 'utf-8');
|
|
62
|
+
const parsed = parseFile(rawSource, { kind: 'component', filePath });
|
|
63
|
+
const propsCode = parsed.script ? stripTopLevelImports(parsed.script) : '';
|
|
64
|
+
const devDecls = buildDevAliasDeclarations(parsed.devAliases, isDev);
|
|
65
|
+
const effectivePropsCode = [devDecls, propsCode].filter(Boolean).join('\n');
|
|
66
|
+
const transpiledPropsCode = propsCode
|
|
67
|
+
? transpileTypeScript(effectivePropsCode, `component-script:${fileName}.ts`)
|
|
68
|
+
: devDecls
|
|
69
|
+
? transpileTypeScript(devDecls, `component-script:${fileName}.ts`)
|
|
70
|
+
: '';
|
|
71
|
+
let source = parsed.template;
|
|
72
|
+
let styleBlock = '';
|
|
73
|
+
const styleMatch = source.match(/<style[\s>][\s\S]*?<\/style>/i);
|
|
74
|
+
if (styleMatch) {
|
|
75
|
+
styleBlock = styleMatch[0];
|
|
76
|
+
source = source.replace(styleMatch[0], '').trim();
|
|
77
|
+
}
|
|
78
|
+
let scopedStyle = '';
|
|
79
|
+
if (styleBlock) {
|
|
80
|
+
const cssContent = styleBlock.replace(/<style[^>]*>/i, '').replace(/<\/style>/i, '').trim();
|
|
81
|
+
scopedStyle = `<style>${scopeComponentCss(cssContent, scopeHash)}</style>`;
|
|
82
|
+
}
|
|
83
|
+
componentStyleCache.set(fileName, scopedStyle ? escapeTemplateLiteral(scopedStyle) : '');
|
|
84
|
+
source = source.replace(/<slot\s*><\/slot>/g, '{@raw props.children || ""}');
|
|
85
|
+
source = source.replace(/<slot\s*\/>/g, '{@raw props.children || ""}');
|
|
86
|
+
const subComponentNames = collectComponentMap(parsed.componentImports);
|
|
87
|
+
for (const subFileName of subComponentNames.values()) {
|
|
88
|
+
const subStyle = componentStyleCache.get(subFileName);
|
|
89
|
+
if (subStyle) {
|
|
90
|
+
const existing = componentStyleCache.get(fileName) || '';
|
|
91
|
+
if (!existing.includes(subStyle)) {
|
|
92
|
+
componentStyleCache.set(fileName, existing + subStyle);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const actionPropNames = new Set();
|
|
97
|
+
for (const match of source.matchAll(/\baction=\{([A-Za-z_$][\w$]*)\}/g)) {
|
|
98
|
+
actionPropNames.add(match[1]);
|
|
99
|
+
}
|
|
100
|
+
componentActionCache.set(fileName, actionPropNames);
|
|
101
|
+
const body = compileTemplate(source, subComponentNames, undefined, undefined);
|
|
102
|
+
const scopeOpen = `__html += '<div class="${scopeHash}">';`;
|
|
103
|
+
const scopeClose = `__html += '</div>';`;
|
|
104
|
+
const bodyLines = body.split('\n');
|
|
105
|
+
const scopedBody = [bodyLines[0], scopeOpen, ...bodyLines.slice(1), scopeClose].join('\n');
|
|
106
|
+
const fnBody = transpiledPropsCode ? `${transpiledPropsCode}\n ${scopedBody}` : scopedBody;
|
|
107
|
+
const compiled = `function ${funcName}(props, __esc) {\n ${fnBody}\n return __html;\n}`;
|
|
108
|
+
compiledComponentCache.set(fileName, compiled);
|
|
109
|
+
return compiled;
|
|
110
|
+
}
|
|
111
|
+
function collectComponentMap(componentImports) {
|
|
112
|
+
const componentNames = new Map();
|
|
113
|
+
for (const [pascalName, fileName] of Object.entries(componentImports)) {
|
|
114
|
+
ensureCompiled(fileName);
|
|
115
|
+
componentNames.set(pascalName, fileName);
|
|
116
|
+
}
|
|
117
|
+
return componentNames;
|
|
118
|
+
}
|
|
119
|
+
function getActionPropNames(fileName) {
|
|
120
|
+
return componentActionCache.get(fileName) ?? new Set();
|
|
121
|
+
}
|
|
122
|
+
function collectStyles(componentNames) {
|
|
123
|
+
const styles = [];
|
|
124
|
+
for (const fileName of componentNames.values()) {
|
|
125
|
+
const css = componentStyleCache.get(fileName);
|
|
126
|
+
if (css)
|
|
127
|
+
styles.push(css);
|
|
128
|
+
}
|
|
129
|
+
return styles;
|
|
130
|
+
}
|
|
131
|
+
function resolveActionProps(template, componentNames, shouldInclude) {
|
|
132
|
+
const names = new Set();
|
|
133
|
+
for (const [pascalName, compFileName] of componentNames.entries()) {
|
|
134
|
+
const actionPropNames = getActionPropNames(compFileName);
|
|
135
|
+
const compTagRegex = new RegExp(`<${pascalName}\\b([\\s\\S]*?)(?:/?)>`, 'g');
|
|
136
|
+
for (const tagMatch of template.matchAll(compTagRegex)) {
|
|
137
|
+
const attrs = tagMatch[1];
|
|
138
|
+
for (const propName of actionPropNames) {
|
|
139
|
+
const propRegex = new RegExp(`\\b${propName}=\\{([A-Za-z_$][\\w$]*)\\}`);
|
|
140
|
+
const propMatch = attrs.match(propRegex);
|
|
141
|
+
if (!propMatch)
|
|
142
|
+
continue;
|
|
143
|
+
const fnName = propMatch[1];
|
|
144
|
+
if (!shouldInclude || shouldInclude(fnName)) {
|
|
145
|
+
names.add(fnName);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return names;
|
|
151
|
+
}
|
|
152
|
+
function getCompiledComponents() {
|
|
153
|
+
return Array.from(compiledComponentCache.values());
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
ensureCompiled,
|
|
157
|
+
collectComponentMap,
|
|
158
|
+
getActionPropNames,
|
|
159
|
+
collectStyles,
|
|
160
|
+
resolveActionProps,
|
|
161
|
+
getCompiledComponents,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AuthConfigEntry, type DoConfigEntry, type OrmDatabaseEntry, type WorkerClassConfigEntry } from './compiler-shared.js';
|
|
2
|
+
export declare function readUiTheme(projectDir: string): string | null;
|
|
3
|
+
export declare function readUiConfigValues(projectDir: string): {
|
|
4
|
+
theme: string;
|
|
5
|
+
radius: string;
|
|
6
|
+
} | null;
|
|
7
|
+
export declare function readOrmConfig(projectDir: string): OrmDatabaseEntry[];
|
|
8
|
+
export declare function readAuthConfig(projectDir: string): AuthConfigEntry | null;
|
|
9
|
+
export declare function readDoConfig(projectDir: string): DoConfigEntry[];
|
|
10
|
+
export declare function readWorkerClassConfig(projectDir: string, key: 'containers' | 'workflows'): WorkerClassConfigEntry[];
|
|
11
|
+
export declare function readAssetsPrefix(projectDir: string): string;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
function skipWhitespace(source, start) {
|
|
4
|
+
let i = start;
|
|
5
|
+
while (i < source.length && /\s/.test(source[i]))
|
|
6
|
+
i++;
|
|
7
|
+
return i;
|
|
8
|
+
}
|
|
9
|
+
function extractBalancedBody(source, start, openChar, closeChar) {
|
|
10
|
+
if (source[start] !== openChar)
|
|
11
|
+
return null;
|
|
12
|
+
let depth = 0;
|
|
13
|
+
for (let i = start; i < source.length; i++) {
|
|
14
|
+
if (source[i] === openChar)
|
|
15
|
+
depth++;
|
|
16
|
+
else if (source[i] === closeChar) {
|
|
17
|
+
depth--;
|
|
18
|
+
if (depth === 0)
|
|
19
|
+
return source.slice(start + 1, i);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function readConfigBlock(source, key) {
|
|
25
|
+
const keyRegex = new RegExp(`\\b${key}\\s*:`);
|
|
26
|
+
const keyMatch = keyRegex.exec(source);
|
|
27
|
+
if (!keyMatch)
|
|
28
|
+
return null;
|
|
29
|
+
const colonIdx = source.indexOf(':', keyMatch.index);
|
|
30
|
+
if (colonIdx === -1)
|
|
31
|
+
return null;
|
|
32
|
+
const valueIdx = skipWhitespace(source, colonIdx + 1);
|
|
33
|
+
if (valueIdx >= source.length)
|
|
34
|
+
return null;
|
|
35
|
+
if (source[valueIdx] === '{') {
|
|
36
|
+
throw new Error(`[kuratchi] "${key}" config must use an adapter call (e.g. ${key}: kuratchi${key[0].toUpperCase()}${key.slice(1)}Config({...})).`);
|
|
37
|
+
}
|
|
38
|
+
const callOpen = source.indexOf('(', valueIdx);
|
|
39
|
+
if (callOpen === -1)
|
|
40
|
+
return null;
|
|
41
|
+
const argIdx = skipWhitespace(source, callOpen + 1);
|
|
42
|
+
if (argIdx >= source.length)
|
|
43
|
+
return null;
|
|
44
|
+
if (source[argIdx] === ')')
|
|
45
|
+
return { kind: 'call-empty', body: '' };
|
|
46
|
+
if (source[argIdx] === '{') {
|
|
47
|
+
const body = extractBalancedBody(source, argIdx, '{', '}');
|
|
48
|
+
if (body == null)
|
|
49
|
+
return null;
|
|
50
|
+
return { kind: 'call-object', body };
|
|
51
|
+
}
|
|
52
|
+
return { kind: 'call-empty', body: '' };
|
|
53
|
+
}
|
|
54
|
+
export function readUiTheme(projectDir) {
|
|
55
|
+
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
56
|
+
if (!fs.existsSync(configPath))
|
|
57
|
+
return null;
|
|
58
|
+
const source = fs.readFileSync(configPath, 'utf-8');
|
|
59
|
+
const uiBlock = readConfigBlock(source, 'ui');
|
|
60
|
+
if (!uiBlock)
|
|
61
|
+
return null;
|
|
62
|
+
const themeMatch = uiBlock.body.match(/theme\s*:\s*['"]([^'"]+)['"]/);
|
|
63
|
+
const themeValue = themeMatch?.[1] ?? 'default';
|
|
64
|
+
if (themeValue === 'default' || themeValue === 'dark' || themeValue === 'light' || themeValue === 'system') {
|
|
65
|
+
const candidates = [
|
|
66
|
+
path.join(projectDir, 'node_modules', '@kuratchi/ui', 'src', 'styles', 'theme.css'),
|
|
67
|
+
path.join(path.resolve(projectDir, '../..'), 'packages', 'kuratchi-ui', 'src', 'styles', 'theme.css'),
|
|
68
|
+
path.join(path.resolve(projectDir, '../..'), 'node_modules', '@kuratchi/ui', 'src', 'styles', 'theme.css'),
|
|
69
|
+
];
|
|
70
|
+
for (const candidate of candidates) {
|
|
71
|
+
if (fs.existsSync(candidate)) {
|
|
72
|
+
return fs.readFileSync(candidate, 'utf-8');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
console.warn(`[kuratchi] ui.theme: "${themeValue}" configured but @kuratchi/ui theme.css not found`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const customPath = path.resolve(projectDir, themeValue);
|
|
79
|
+
if (fs.existsSync(customPath)) {
|
|
80
|
+
return fs.readFileSync(customPath, 'utf-8');
|
|
81
|
+
}
|
|
82
|
+
console.warn(`[kuratchi] ui.theme: "${themeValue}" not found at ${customPath}`);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
export function readUiConfigValues(projectDir) {
|
|
86
|
+
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
87
|
+
if (!fs.existsSync(configPath))
|
|
88
|
+
return null;
|
|
89
|
+
const source = fs.readFileSync(configPath, 'utf-8');
|
|
90
|
+
const uiBlock = readConfigBlock(source, 'ui');
|
|
91
|
+
if (!uiBlock)
|
|
92
|
+
return null;
|
|
93
|
+
const themeMatch = uiBlock.body.match(/theme\s*:\s*['"]([^'"]+)['"]/);
|
|
94
|
+
const radiusMatch = uiBlock.body.match(/radius\s*:\s*['"]([^'"]+)['"]/);
|
|
95
|
+
return {
|
|
96
|
+
theme: themeMatch?.[1] ?? 'dark',
|
|
97
|
+
radius: radiusMatch?.[1] ?? 'default',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function readOrmConfig(projectDir) {
|
|
101
|
+
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
102
|
+
if (!fs.existsSync(configPath))
|
|
103
|
+
return [];
|
|
104
|
+
const source = fs.readFileSync(configPath, 'utf-8');
|
|
105
|
+
const ormBlock = readConfigBlock(source, 'orm');
|
|
106
|
+
if (!ormBlock)
|
|
107
|
+
return [];
|
|
108
|
+
const importMap = new Map();
|
|
109
|
+
const importRegex = /import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
110
|
+
let match;
|
|
111
|
+
while ((match = importRegex.exec(source)) !== null) {
|
|
112
|
+
const names = match[1].split(',').map((name) => name.trim()).filter(Boolean);
|
|
113
|
+
const importPath = match[2];
|
|
114
|
+
for (const name of names) {
|
|
115
|
+
importMap.set(name, importPath);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const databasesIdx = ormBlock.body.search(/databases\s*:\s*\{/);
|
|
119
|
+
if (databasesIdx === -1)
|
|
120
|
+
return [];
|
|
121
|
+
const dbBraceStart = ormBlock.body.indexOf('{', databasesIdx);
|
|
122
|
+
if (dbBraceStart === -1)
|
|
123
|
+
return [];
|
|
124
|
+
const databasesBody = extractBalancedBody(ormBlock.body, dbBraceStart, '{', '}');
|
|
125
|
+
if (databasesBody == null)
|
|
126
|
+
return [];
|
|
127
|
+
const entries = [];
|
|
128
|
+
const entryRegex = /(\w+)\s*:\s*\{\s*schema\s*:\s*(\w+)([^}]*)\}/g;
|
|
129
|
+
while ((match = entryRegex.exec(databasesBody)) !== null) {
|
|
130
|
+
const binding = match[1];
|
|
131
|
+
const schemaExportName = match[2];
|
|
132
|
+
const rest = match[3] || '';
|
|
133
|
+
const skipMatch = rest.match(/skipMigrations\s*:\s*(true|false)/);
|
|
134
|
+
const skipMigrations = skipMatch?.[1] === 'true';
|
|
135
|
+
const typeMatch = rest.match(/type\s*:\s*['"]?(d1|do)['"]?/);
|
|
136
|
+
const type = typeMatch?.[1] ?? 'd1';
|
|
137
|
+
const schemaImportPath = importMap.get(schemaExportName);
|
|
138
|
+
if (!schemaImportPath)
|
|
139
|
+
continue;
|
|
140
|
+
entries.push({ binding, schemaImportPath, schemaExportName, skipMigrations, type });
|
|
141
|
+
}
|
|
142
|
+
return entries;
|
|
143
|
+
}
|
|
144
|
+
export function readAuthConfig(projectDir) {
|
|
145
|
+
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
146
|
+
if (!fs.existsSync(configPath))
|
|
147
|
+
return null;
|
|
148
|
+
const source = fs.readFileSync(configPath, 'utf-8');
|
|
149
|
+
const authBlockMatch = readConfigBlock(source, 'auth');
|
|
150
|
+
if (!authBlockMatch)
|
|
151
|
+
return null;
|
|
152
|
+
const authBlock = authBlockMatch.body;
|
|
153
|
+
const cookieMatch = authBlock.match(/cookieName\s*:\s*['"]([^'"]+)['"]/);
|
|
154
|
+
const secretMatch = authBlock.match(/secretEnvKey\s*:\s*['"]([^'"]+)['"]/);
|
|
155
|
+
const sessionMatch = authBlock.match(/sessionEnabled\s*:\s*(true|false)/);
|
|
156
|
+
return {
|
|
157
|
+
cookieName: cookieMatch?.[1] ?? 'kuratchi_session',
|
|
158
|
+
secretEnvKey: secretMatch?.[1] ?? 'AUTH_SECRET',
|
|
159
|
+
sessionEnabled: sessionMatch?.[1] !== 'false',
|
|
160
|
+
hasCredentials: /credentials\s*:/.test(authBlock),
|
|
161
|
+
hasActivity: /activity\s*:/.test(authBlock),
|
|
162
|
+
hasRoles: /roles\s*:/.test(authBlock),
|
|
163
|
+
hasOAuth: /oauth\s*:/.test(authBlock),
|
|
164
|
+
hasGuards: /guards\s*:/.test(authBlock),
|
|
165
|
+
hasRateLimit: /rateLimit\s*:/.test(authBlock),
|
|
166
|
+
hasTurnstile: /turnstile\s*:/.test(authBlock),
|
|
167
|
+
hasOrganization: /organizations\s*:/.test(authBlock),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export function readDoConfig(projectDir) {
|
|
171
|
+
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
172
|
+
if (!fs.existsSync(configPath))
|
|
173
|
+
return [];
|
|
174
|
+
const source = fs.readFileSync(configPath, 'utf-8');
|
|
175
|
+
const doIdx = source.search(/durableObjects\s*:\s*\{/);
|
|
176
|
+
if (doIdx === -1)
|
|
177
|
+
return [];
|
|
178
|
+
const braceStart = source.indexOf('{', doIdx);
|
|
179
|
+
if (braceStart === -1)
|
|
180
|
+
return [];
|
|
181
|
+
let depth = 0;
|
|
182
|
+
let braceEnd = braceStart;
|
|
183
|
+
for (let i = braceStart; i < source.length; i++) {
|
|
184
|
+
if (source[i] === '{')
|
|
185
|
+
depth++;
|
|
186
|
+
else if (source[i] === '}') {
|
|
187
|
+
depth--;
|
|
188
|
+
if (depth === 0) {
|
|
189
|
+
braceEnd = i;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const doBlock = source.slice(braceStart + 1, braceEnd);
|
|
195
|
+
const entries = [];
|
|
196
|
+
const objRegex = /(\w+)\s*:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/g;
|
|
197
|
+
let match;
|
|
198
|
+
while ((match = objRegex.exec(doBlock)) !== null) {
|
|
199
|
+
const binding = match[1];
|
|
200
|
+
const body = match[2];
|
|
201
|
+
const cnMatch = body.match(/className\s*:\s*['"](\w+)['"]/);
|
|
202
|
+
if (!cnMatch)
|
|
203
|
+
continue;
|
|
204
|
+
const entry = { binding, className: cnMatch[1] };
|
|
205
|
+
const stubIdMatch = body.match(/stubId\s*:\s*['"]([^'"]+)['"]/);
|
|
206
|
+
if (stubIdMatch)
|
|
207
|
+
entry.stubId = stubIdMatch[1];
|
|
208
|
+
const filesMatch = body.match(/files\s*:\s*\[([\s\S]*?)\]/);
|
|
209
|
+
if (filesMatch) {
|
|
210
|
+
const list = [];
|
|
211
|
+
const itemRegex = /['"]([^'"]+)['"]/g;
|
|
212
|
+
let fileMatch;
|
|
213
|
+
while ((fileMatch = itemRegex.exec(filesMatch[1])) !== null) {
|
|
214
|
+
list.push(fileMatch[1]);
|
|
215
|
+
}
|
|
216
|
+
if (list.length > 0)
|
|
217
|
+
entry.files = list;
|
|
218
|
+
}
|
|
219
|
+
entries.push(entry);
|
|
220
|
+
}
|
|
221
|
+
const foundBindings = new Set(entries.map((entry) => entry.binding));
|
|
222
|
+
const pairRegex = /(\w+)\s*:\s*['"](\w+)['"]\s*[,}\n]/g;
|
|
223
|
+
while ((match = pairRegex.exec(doBlock)) !== null) {
|
|
224
|
+
if (foundBindings.has(match[1]))
|
|
225
|
+
continue;
|
|
226
|
+
if (['className', 'stubId'].includes(match[1]))
|
|
227
|
+
continue;
|
|
228
|
+
entries.push({ binding: match[1], className: match[2] });
|
|
229
|
+
}
|
|
230
|
+
return entries;
|
|
231
|
+
}
|
|
232
|
+
export function readWorkerClassConfig(projectDir, key) {
|
|
233
|
+
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
234
|
+
if (!fs.existsSync(configPath))
|
|
235
|
+
return [];
|
|
236
|
+
const source = fs.readFileSync(configPath, 'utf-8');
|
|
237
|
+
const keyIdx = source.search(new RegExp(`\\b${key}\\s*:\\s*\\{`));
|
|
238
|
+
if (keyIdx === -1)
|
|
239
|
+
return [];
|
|
240
|
+
const braceStart = source.indexOf('{', keyIdx);
|
|
241
|
+
if (braceStart === -1)
|
|
242
|
+
return [];
|
|
243
|
+
const body = extractBalancedBody(source, braceStart, '{', '}');
|
|
244
|
+
if (body == null)
|
|
245
|
+
return [];
|
|
246
|
+
const entries = [];
|
|
247
|
+
const expectedSuffix = key === 'containers' ? '.container' : '.workflow';
|
|
248
|
+
const allowedExt = /\.(ts|js|mjs|cjs)$/i;
|
|
249
|
+
const requiredFilePattern = new RegExp(`\\${expectedSuffix}\\.(ts|js|mjs|cjs)$`, 'i');
|
|
250
|
+
const resolveClassFromFile = (binding, filePath) => {
|
|
251
|
+
if (!requiredFilePattern.test(filePath)) {
|
|
252
|
+
throw new Error(`[kuratchi] ${key}.${binding} must reference a file ending in "${expectedSuffix}.ts|js|mjs|cjs". Received: ${filePath}`);
|
|
253
|
+
}
|
|
254
|
+
if (!allowedExt.test(filePath)) {
|
|
255
|
+
throw new Error(`[kuratchi] ${key}.${binding} file must be a TypeScript or JavaScript module. Received: ${filePath}`);
|
|
256
|
+
}
|
|
257
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
|
|
258
|
+
if (!fs.existsSync(absPath)) {
|
|
259
|
+
throw new Error(`[kuratchi] ${key}.${binding} file not found: ${filePath}`);
|
|
260
|
+
}
|
|
261
|
+
const fileSource = fs.readFileSync(absPath, 'utf-8');
|
|
262
|
+
const defaultClass = fileSource.match(/export\s+default\s+class\s+(\w+)/);
|
|
263
|
+
if (defaultClass) {
|
|
264
|
+
return { className: defaultClass[1], exportKind: 'default' };
|
|
265
|
+
}
|
|
266
|
+
const namedClass = fileSource.match(/export\s+class\s+(\w+)/);
|
|
267
|
+
if (namedClass) {
|
|
268
|
+
return { className: namedClass[1], exportKind: 'named' };
|
|
269
|
+
}
|
|
270
|
+
throw new Error(`[kuratchi] ${key}.${binding} must export a class via "export class X" or "export default class X". File: ${filePath}`);
|
|
271
|
+
};
|
|
272
|
+
const objRegex = /(\w+)\s*:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/g;
|
|
273
|
+
let match;
|
|
274
|
+
while ((match = objRegex.exec(body)) !== null) {
|
|
275
|
+
const binding = match[1];
|
|
276
|
+
const entryBody = match[2];
|
|
277
|
+
const fileMatch = entryBody.match(/file\s*:\s*['"]([^'"]+)['"]/);
|
|
278
|
+
if (!fileMatch)
|
|
279
|
+
continue;
|
|
280
|
+
const inferred = resolveClassFromFile(binding, fileMatch[1]);
|
|
281
|
+
const classMatch = entryBody.match(/className\s*:\s*['"](\w+)['"]/);
|
|
282
|
+
const className = classMatch?.[1] ?? inferred.className;
|
|
283
|
+
entries.push({
|
|
284
|
+
binding,
|
|
285
|
+
className,
|
|
286
|
+
file: fileMatch[1],
|
|
287
|
+
exportKind: inferred.exportKind,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
const foundBindings = new Set(entries.map((entry) => entry.binding));
|
|
291
|
+
const pairRegex = /(\w+)\s*:\s*['"]([^'"]+)['"]\s*[,}\n]/g;
|
|
292
|
+
while ((match = pairRegex.exec(body)) !== null) {
|
|
293
|
+
const binding = match[1];
|
|
294
|
+
const file = match[2];
|
|
295
|
+
if (foundBindings.has(binding))
|
|
296
|
+
continue;
|
|
297
|
+
if (binding === 'file' || binding === 'className')
|
|
298
|
+
continue;
|
|
299
|
+
const inferred = resolveClassFromFile(binding, file);
|
|
300
|
+
entries.push({
|
|
301
|
+
binding,
|
|
302
|
+
className: inferred.className,
|
|
303
|
+
file,
|
|
304
|
+
exportKind: inferred.exportKind,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return entries;
|
|
308
|
+
}
|
|
309
|
+
export function readAssetsPrefix(projectDir) {
|
|
310
|
+
const configPath = path.join(projectDir, 'kuratchi.config.ts');
|
|
311
|
+
if (!fs.existsSync(configPath))
|
|
312
|
+
return '/assets/';
|
|
313
|
+
const source = fs.readFileSync(configPath, 'utf-8');
|
|
314
|
+
const match = source.match(/assetsPrefix\s*:\s*['"]([^'"]+)['"]/);
|
|
315
|
+
if (!match)
|
|
316
|
+
return '/assets/';
|
|
317
|
+
let prefix = match[1];
|
|
318
|
+
if (!prefix.startsWith('/'))
|
|
319
|
+
prefix = '/' + prefix;
|
|
320
|
+
if (!prefix.endsWith('/'))
|
|
321
|
+
prefix += '/';
|
|
322
|
+
return prefix;
|
|
323
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ConventionClassEntry, type WorkerClassConfigEntry } from './compiler-shared.js';
|
|
2
|
+
export declare function resolveClassExportFromFile(absPath: string, errorLabel: string): {
|
|
3
|
+
className: string;
|
|
4
|
+
exportKind: 'named' | 'default';
|
|
5
|
+
};
|
|
6
|
+
export declare function discoverConventionClassFiles(projectDir: string, dir: string, suffix: string, errorLabel: string): ConventionClassEntry[];
|
|
7
|
+
export declare function discoverFilesWithSuffix(dir: string, suffix: string): string[];
|
|
8
|
+
export declare function discoverWorkflowFiles(projectDir: string): WorkerClassConfigEntry[];
|
|
9
|
+
export declare function discoverContainerFiles(projectDir: string): WorkerClassConfigEntry[];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export function resolveClassExportFromFile(absPath, errorLabel) {
|
|
4
|
+
if (!fs.existsSync(absPath)) {
|
|
5
|
+
throw new Error(`[kuratchi] ${errorLabel} file not found: ${absPath}`);
|
|
6
|
+
}
|
|
7
|
+
const fileSource = fs.readFileSync(absPath, 'utf-8');
|
|
8
|
+
const defaultClass = fileSource.match(/export\s+default\s+class\s+(\w+)/);
|
|
9
|
+
if (defaultClass) {
|
|
10
|
+
return { className: defaultClass[1], exportKind: 'default' };
|
|
11
|
+
}
|
|
12
|
+
const namedClass = fileSource.match(/export\s+class\s+(\w+)/);
|
|
13
|
+
if (namedClass) {
|
|
14
|
+
return { className: namedClass[1], exportKind: 'named' };
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`[kuratchi] ${errorLabel} must export a class via "export class X" or "export default class X". File: ${absPath}`);
|
|
17
|
+
}
|
|
18
|
+
export function discoverConventionClassFiles(projectDir, dir, suffix, errorLabel) {
|
|
19
|
+
const absDir = path.join(projectDir, dir);
|
|
20
|
+
const files = discoverFilesWithSuffix(absDir, suffix);
|
|
21
|
+
if (files.length === 0)
|
|
22
|
+
return [];
|
|
23
|
+
return files.map((absPath) => {
|
|
24
|
+
const resolved = resolveClassExportFromFile(absPath, errorLabel);
|
|
25
|
+
return {
|
|
26
|
+
className: resolved.className,
|
|
27
|
+
file: path.relative(projectDir, absPath).replace(/\\/g, '/'),
|
|
28
|
+
exportKind: resolved.exportKind,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function discoverFilesWithSuffix(dir, suffix) {
|
|
33
|
+
if (!fs.existsSync(dir))
|
|
34
|
+
return [];
|
|
35
|
+
const out = [];
|
|
36
|
+
const walk = (absDir) => {
|
|
37
|
+
for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) {
|
|
38
|
+
const abs = path.join(absDir, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
walk(abs);
|
|
41
|
+
}
|
|
42
|
+
else if (entry.isFile() && abs.endsWith(suffix)) {
|
|
43
|
+
out.push(abs);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
walk(dir);
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
export function discoverWorkflowFiles(projectDir) {
|
|
51
|
+
const serverDir = path.join(projectDir, 'src', 'server');
|
|
52
|
+
const files = discoverFilesWithSuffix(serverDir, '.workflow.ts');
|
|
53
|
+
if (files.length === 0)
|
|
54
|
+
return [];
|
|
55
|
+
return files.map((absPath) => {
|
|
56
|
+
const fileName = path.basename(absPath, '.workflow.ts');
|
|
57
|
+
const binding = fileName.toUpperCase().replace(/-/g, '_') + '_WORKFLOW';
|
|
58
|
+
const resolved = resolveClassExportFromFile(absPath, '.workflow');
|
|
59
|
+
return {
|
|
60
|
+
binding,
|
|
61
|
+
className: resolved.className,
|
|
62
|
+
file: path.relative(projectDir, absPath).replace(/\\/g, '/'),
|
|
63
|
+
exportKind: resolved.exportKind,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function discoverContainerFiles(projectDir) {
|
|
68
|
+
const serverDir = path.join(projectDir, 'src', 'server');
|
|
69
|
+
const files = discoverFilesWithSuffix(serverDir, '.container.ts');
|
|
70
|
+
if (files.length === 0)
|
|
71
|
+
return [];
|
|
72
|
+
return files.map((absPath) => {
|
|
73
|
+
const fileName = path.basename(absPath, '.container.ts');
|
|
74
|
+
const binding = fileName.toUpperCase().replace(/-/g, '_') + '_CONTAINER';
|
|
75
|
+
const resolved = resolveClassExportFromFile(absPath, '.container');
|
|
76
|
+
return {
|
|
77
|
+
binding,
|
|
78
|
+
className: resolved.className,
|
|
79
|
+
file: path.relative(projectDir, absPath).replace(/\\/g, '/'),
|
|
80
|
+
exportKind: resolved.exportKind,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type DoConfigEntry, type DoHandlerEntry, type OrmDatabaseEntry } from './compiler-shared.js';
|
|
2
|
+
export declare function discoverDurableObjects(srcDir: string, configDoEntries: DoConfigEntry[], ormDatabases: OrmDatabaseEntry[]): {
|
|
3
|
+
config: DoConfigEntry[];
|
|
4
|
+
handlers: DoHandlerEntry[];
|
|
5
|
+
};
|
|
6
|
+
export declare function generateHandlerProxy(handler: DoHandlerEntry, opts: {
|
|
7
|
+
projectDir: string;
|
|
8
|
+
runtimeDoImport: string;
|
|
9
|
+
}): string;
|