bazis 0.99.6 → 0.100.1
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/cli/build.ts +171 -36
- package/cli/codegen.ts +155 -4
- package/cli/generateProject.ts +8 -2
- package/cli/main.ts +134 -51
- package/cli/naming.ts +3 -0
- package/cli/ui.ts +86 -0
- package/core/agent/index.ts +8 -0
- package/core/app/runApp.ts +4 -1
- package/core/di/extensions/hosted-service.ts +6 -0
- package/core/http/HttpServer.ts +33 -2
- package/core/http/Routing/RouterBuilder.ts +21 -0
- package/core/infra/Infra.ts +8 -2
- package/core/infra/InfraConnector.ts +13 -0
- package/core/infra/InfraLifecycle.ts +27 -0
- package/core/infra/connectorConfig.ts +10 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/infra/connectors/llm.ts +23 -2
- package/core/infra/connectors/openaiCompatible.ts +26 -1
- package/core/infra/connectors/opensearch.ts +36 -8
- package/core/infra/connectors/postgres.ts +29 -4
- package/core/infra/connectors/redis.ts +26 -3
- package/core/infra/index.ts +2 -2
- package/core/kernel/Bazis.ts +21 -1
- package/core/kernel/Kernel.ts +54 -0
- package/core/kernel/KernelBuilder.ts +39 -1
- package/core/kernel/LifecycleCoordinator.ts +101 -9
- package/core/kernel/config/ConfigRegistry.ts +5 -0
- package/core/kernel/config/ConfigValidationError.ts +37 -0
- package/core/kernel/config/callerFile.ts +23 -0
- package/core/kernel/config/defineConfig.ts +21 -17
- package/core/kernel/errors/StartupFailuresError.ts +17 -0
- package/core/kernel/errors/index.ts +1 -0
- package/core/kernel/index.ts +12 -0
- package/core/kernel/report/StartupReport.ts +134 -0
- package/core/kernel/report/StartupScreen.ts +459 -0
- package/core/kernel/report/moduleTree.ts +97 -0
- package/core/kernel/report/startupProblems.ts +186 -0
- package/core/orm/OrmHostedPlan.validator.ts +3 -3
- package/core/orm/OrmLifecycle.ts +29 -7
- package/core/orm/buildOrmProviders.ts +7 -3
- package/core/orm/databaseConnector.ts +17 -1
- package/core/orm/index.ts +11 -0
- package/core/orm/strictSchemaHostedIdentity.ts +16 -1
- package/core/scripts/di-generate.ts +58 -9
- package/library/terminal/index.ts +4 -0
- package/library/terminal/language.ts +60 -0
- package/library/terminal/messages.ts +400 -0
- package/library/terminal/screen.ts +358 -0
- package/library/terminal/style.ts +143 -0
- package/package.json +2 -1
package/cli/build.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { existsSync, mkdtempSync, rmSync, watch } from "node:fs";
|
|
1
|
+
import { existsSync, mkdtempSync, rmSync, statSync, watch } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { PALETTE, formatBytes, formatDuration, type Problem } from "../library/terminal";
|
|
5
|
+
import { resolveBun, type CodegenOptions } from "./codegen";
|
|
6
|
+
import { commandFailed, commandHeader, createScreens, readFrameLines, type CliOutput } from "./ui";
|
|
5
7
|
|
|
6
|
-
type Codegen = (cwd: string, target?: string) => Promise<number>;
|
|
8
|
+
type Codegen = (cwd: string, target?: string, options?: CodegenOptions) => Promise<number>;
|
|
7
9
|
|
|
8
10
|
export interface BuildOptions {
|
|
9
11
|
/** Also compile the entrypoint into a standalone executable. */
|
|
@@ -19,23 +21,46 @@ export interface CompileOptions {
|
|
|
19
21
|
readonly dotenv?: boolean;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* With `watch`, a change under src/ (except src/generated) stops the app,
|
|
25
|
-
* reruns codegen and starts it again; a failed codegen waits for the next change.
|
|
26
|
-
* The app runs as `development` unless BAZIS_ENV is set in the shell.
|
|
27
|
-
*/
|
|
24
|
+
const defaultUi = (): CliOutput => createScreens(process.stdout, process.stderr);
|
|
25
|
+
|
|
28
26
|
/** `bazis dev` means development; an BAZIS_ENV from the shell still wins. */
|
|
29
27
|
export function devEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
30
28
|
return { ...env, BAZIS_ENV: env.BAZIS_ENV || "development" };
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
/**
|
|
32
|
+
* `bazis dev`: codegen of every target, then run the default target's entrypoint from source.
|
|
33
|
+
* With `watch`, a change under src/ (except src/generated) stops the app,
|
|
34
|
+
* reruns codegen and starts it again; a failed codegen waits for the next change.
|
|
35
|
+
* The app runs as `development` unless BAZIS_ENV is set in the shell. The CLI
|
|
36
|
+
* prints the header and the build; the application prints the rest of the screen.
|
|
37
|
+
*/
|
|
38
|
+
export async function runDev(cwd: string, codegen: Codegen, ui: CliOutput = defaultUi(), options: { readonly watch?: boolean } = {}): Promise<number> {
|
|
39
|
+
const s = ui.out;
|
|
40
|
+
const t = s.t;
|
|
34
41
|
const bun = await resolveBun(cwd);
|
|
35
42
|
const entry = await projectEntry(cwd);
|
|
36
|
-
const
|
|
43
|
+
const watchDir = "src/";
|
|
44
|
+
const env: NodeJS.ProcessEnv = {
|
|
45
|
+
...devEnvironment(process.env),
|
|
46
|
+
// The CLI already printed the logo; the application continues the screen.
|
|
47
|
+
BAZIS_UI_HEADER: "0",
|
|
48
|
+
BAZIS_LANG: process.env.BAZIS_LANG || s.language,
|
|
49
|
+
...(options.watch ? { BAZIS_UI_WATCH: watchDir } : {}),
|
|
50
|
+
};
|
|
51
|
+
commandHeader(s, "dev", cwd, [
|
|
52
|
+
s.kv(t.meta.env, s.strong(env.BAZIS_ENV ?? "development", PALETTE.yellow)),
|
|
53
|
+
s.kv(t.meta.watch, options.watch ? s.strong(t.meta.on, PALETTE.lime) : s.muted(t.meta.off)),
|
|
54
|
+
s.kv(t.meta.src, s.text(path.relative(cwd, entry))),
|
|
55
|
+
]);
|
|
56
|
+
const build = async (footer?: string): Promise<number> => {
|
|
57
|
+
s.section(t.section.build);
|
|
58
|
+
const generated = await codegen(cwd, "all", { ui, ...(footer === undefined ? {} : { footer }) });
|
|
59
|
+
if (generated === 0) s.line();
|
|
60
|
+
return generated;
|
|
61
|
+
};
|
|
37
62
|
if (!options.watch) {
|
|
38
|
-
const generated = await
|
|
63
|
+
const generated = await build();
|
|
39
64
|
if (generated !== 0) return generated;
|
|
40
65
|
const child = Bun.spawn([bun, "run", entry], { cwd, env, stdin: "inherit", stdout: "inherit", stderr: "inherit" });
|
|
41
66
|
// The app owns graceful shutdown; forward the signal instead of dying first.
|
|
@@ -62,7 +87,7 @@ export async function runDev(cwd: string, codegen: Codegen, log: (message: strin
|
|
|
62
87
|
await running.exited;
|
|
63
88
|
};
|
|
64
89
|
const start = async () => {
|
|
65
|
-
if (await
|
|
90
|
+
if (await build(t.failure.exitWatch(1, watchDir)) !== 0) return;
|
|
66
91
|
child = Bun.spawn([bun, "run", entry], { cwd, env, stdin: "inherit", stdout: "inherit", stderr: "inherit" });
|
|
67
92
|
};
|
|
68
93
|
const watcher = watch(path.join(cwd, "src"), { recursive: true }, (_event, file) => {
|
|
@@ -70,7 +95,13 @@ export async function runDev(cwd: string, codegen: Codegen, log: (message: strin
|
|
|
70
95
|
if (!file || /^generated([\\/]|$)/.test(String(file))) return;
|
|
71
96
|
clearTimeout(timer);
|
|
72
97
|
timer = setTimeout(() => {
|
|
73
|
-
queue = queue.then(async () => {
|
|
98
|
+
queue = queue.then(async () => {
|
|
99
|
+
s.line();
|
|
100
|
+
s.line(` ${s.strong("↻", PALETTE.link)} ${s.text(`src/${String(file).replaceAll("\\", "/")}`)} ${s.muted(t.dev.restarting)}`);
|
|
101
|
+
s.line();
|
|
102
|
+
await stop();
|
|
103
|
+
await start();
|
|
104
|
+
});
|
|
74
105
|
}, 150);
|
|
75
106
|
});
|
|
76
107
|
const finished = Promise.withResolvers<number>();
|
|
@@ -81,7 +112,6 @@ export async function runDev(cwd: string, codegen: Codegen, log: (message: strin
|
|
|
81
112
|
};
|
|
82
113
|
process.once("SIGINT", shutdown);
|
|
83
114
|
process.once("SIGTERM", shutdown);
|
|
84
|
-
log("[bazis] watching src/ for changes (Ctrl+C to stop)");
|
|
85
115
|
queue = queue.then(start);
|
|
86
116
|
try {
|
|
87
117
|
return await finished.promise;
|
|
@@ -92,25 +122,111 @@ export async function runDev(cwd: string, codegen: Codegen, log: (message: strin
|
|
|
92
122
|
}
|
|
93
123
|
|
|
94
124
|
/** `bazis test [args]`: codegen of every target, then `bun test` with the given arguments. */
|
|
95
|
-
export async function runTest(cwd: string, args: readonly string[], codegen: Codegen): Promise<number> {
|
|
96
|
-
const
|
|
125
|
+
export async function runTest(cwd: string, args: readonly string[], codegen: Codegen, ui: CliOutput = defaultUi()): Promise<number> {
|
|
126
|
+
const s = ui.out;
|
|
127
|
+
const t = s.t;
|
|
128
|
+
commandHeader(s, "test", cwd, [s.kv(t.meta.env, s.strong("test", PALETTE.link)), ...(args.length ? [s.text(`bun test ${args.join(" ")}`)] : [])]);
|
|
129
|
+
s.section(t.section.build);
|
|
130
|
+
const generated = await codegen(cwd, "all", { ui });
|
|
97
131
|
if (generated !== 0) return generated;
|
|
98
|
-
|
|
132
|
+
s.line();
|
|
133
|
+
s.section(t.section.tests);
|
|
134
|
+
s.line();
|
|
135
|
+
const startedAt = performance.now();
|
|
136
|
+
const exitCode = await Bun.spawn([await resolveBun(cwd), "test", ...args], { cwd, stdin: "inherit", stdout: "inherit", stderr: "inherit" }).exited;
|
|
137
|
+
s.line();
|
|
138
|
+
if (exitCode === 0) {
|
|
139
|
+
s.banner("success", `${t.test.done} · ${formatDuration(performance.now() - startedAt)}`);
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
ui.err.banner("failed", t.test.failed(exitCode));
|
|
143
|
+
ui.err.footer(t.failure.exit(exitCode));
|
|
144
|
+
return exitCode;
|
|
99
145
|
}
|
|
100
146
|
|
|
101
147
|
/** `bazis build [--bin]`: codegen of every target and typecheck; with `bin`, also compile. */
|
|
102
|
-
export async function runBuild(cwd: string, options: BuildOptions, codegen: Codegen,
|
|
103
|
-
const
|
|
148
|
+
export async function runBuild(cwd: string, options: BuildOptions, codegen: Codegen, ui: CliOutput = defaultUi()): Promise<number> {
|
|
149
|
+
const s = ui.out;
|
|
150
|
+
const t = s.t;
|
|
151
|
+
const startedAt = performance.now();
|
|
152
|
+
const outfile = options.bin ? path.resolve(cwd, options.outfile ?? path.join("bin", await binaryName(cwd))) : undefined;
|
|
153
|
+
commandHeader(s, "build", cwd, [
|
|
154
|
+
s.kv(t.meta.target, s.text(options.bin ? "executable" : "typecheck")),
|
|
155
|
+
...(outfile === undefined ? [] : [s.kv(t.meta.out, s.text(path.relative(cwd, outfile)))]),
|
|
156
|
+
]);
|
|
157
|
+
s.section(t.section.build);
|
|
158
|
+
const generated = await codegen(cwd, "all", { ui });
|
|
104
159
|
if (generated !== 0) return generated;
|
|
105
160
|
const bun = await resolveBun(cwd);
|
|
106
161
|
const tsc = path.join(cwd, "node_modules/typescript/bin/tsc");
|
|
107
162
|
if (!existsSync(tsc)) throw new Error("TypeScript is not installed in this project. Run: bun add -d typescript");
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
163
|
+
const stop = s.progress(t.step.typecheck);
|
|
164
|
+
const typecheckStarted = performance.now();
|
|
165
|
+
const child = Bun.spawn([bun, tsc, "--noEmit", "--pretty", "false"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
166
|
+
const [stdout, stderr, typecheck] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
167
|
+
stop();
|
|
168
|
+
const typecheckMs = formatDuration(performance.now() - typecheckStarted);
|
|
169
|
+
if (typecheck !== 0) {
|
|
170
|
+
const problems = typeProblems(`${stdout}\n${stderr}`, cwd);
|
|
171
|
+
s.step("fail", t.step.typecheck, s.num(problems.length, t.noun("error", problems.length), PALETTE.red), typecheckMs);
|
|
172
|
+
s.line();
|
|
173
|
+
if (problems.length === 0) ui.err.line(`${stdout}${stderr}`.trimEnd());
|
|
174
|
+
return commandFailed(ui.err, t.failure.typecheck(problems.length), problems.slice(0, 20), typecheck);
|
|
175
|
+
}
|
|
176
|
+
s.step("ok", t.step.typecheck, s.muted(t.build.typecheckOk), typecheckMs);
|
|
177
|
+
s.line();
|
|
178
|
+
if (outfile === undefined) {
|
|
179
|
+
s.banner("success", t.build.done(formatDuration(performance.now() - startedAt)));
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
s.section(t.section.binary);
|
|
183
|
+
const stopCompile = s.progress(t.step.compile);
|
|
184
|
+
const compileStarted = performance.now();
|
|
185
|
+
const compiled = await compileBinaryAsync(bun, await projectEntry(cwd), outfile, { dotenv: options.dotenv });
|
|
186
|
+
stopCompile();
|
|
187
|
+
const compileMs = formatDuration(performance.now() - compileStarted);
|
|
188
|
+
const relative = path.relative(cwd, outfile);
|
|
189
|
+
if (compiled.exitCode !== 0) {
|
|
190
|
+
s.step("fail", t.step.compile, "", compileMs);
|
|
191
|
+
s.line();
|
|
192
|
+
return commandFailed(ui.err, t.failure.command, [{ tag: "COMPILE", where: relative, cause: compiled.output.trim() || `bun build exited with code ${compiled.exitCode}` }], compiled.exitCode);
|
|
193
|
+
}
|
|
194
|
+
s.step("ok", t.step.compile, `${s.text(relative)}${s.dot}${s.strong(formatBytes(statSync(outfile).size))}`, compileMs);
|
|
195
|
+
s.line();
|
|
196
|
+
s.banner("success", t.build.binaryDone(relative));
|
|
197
|
+
s.panel([
|
|
198
|
+
s.panelRow(t.build.run, s.code(`./${relative}`)),
|
|
199
|
+
"",
|
|
200
|
+
s.muted(t.build.production),
|
|
201
|
+
...(options.dotenv ? [s.muted(t.build.dotenv)] : []),
|
|
202
|
+
]);
|
|
203
|
+
s.line();
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** `src/a.ts(12,5): error TS2322: …` → a problem with the code around the line. */
|
|
208
|
+
export function typeProblems(output: string, cwd: string): Problem[] {
|
|
209
|
+
const problems: Problem[] = [];
|
|
210
|
+
for (const line of output.split("\n")) {
|
|
211
|
+
const match = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.*)$/.exec(line.trim());
|
|
212
|
+
if (!match) {
|
|
213
|
+
// tsc continues a message on indented lines.
|
|
214
|
+
const last = problems.at(-1);
|
|
215
|
+
if (last !== undefined && /^\s+\S/.test(line)) problems[problems.length - 1] = { ...last, cause: `${last.cause}\n${line.trim()}` };
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const [, file, lineText, columnText, code, message] = match as unknown as [string, string, string, string, string, string];
|
|
219
|
+
const lineNumber = Number(lineText);
|
|
220
|
+
const lines = readFrameLines(path.resolve(cwd, file), lineNumber);
|
|
221
|
+
problems.push({
|
|
222
|
+
tag: "TYPE",
|
|
223
|
+
where: `${file}:${lineNumber}:${columnText}`,
|
|
224
|
+
cause: message,
|
|
225
|
+
...(lines.length === 0 ? {} : { frame: { lines, line: lineNumber, column: Number(columnText), length: 1 } }),
|
|
226
|
+
code,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return problems;
|
|
114
230
|
}
|
|
115
231
|
|
|
116
232
|
/**
|
|
@@ -127,19 +243,38 @@ export async function runBuild(cwd: string, options: BuildOptions, codegen: Code
|
|
|
127
243
|
*/
|
|
128
244
|
export function compileBinary(bun: string, entry: string, outfile: string, options: CompileOptions = {}): number {
|
|
129
245
|
const workDir = mkdtempSync(path.join(tmpdir(), "bazis-compile-"));
|
|
130
|
-
const autoload = ["--no-compile-autoload-bunfig", options.dotenv === true ? "--compile-autoload-dotenv" : "--no-compile-autoload-dotenv"];
|
|
131
246
|
try {
|
|
132
|
-
const result = Bun.spawnSync(
|
|
133
|
-
{ cwd: workDir, stdout: "inherit", stderr: "inherit" });
|
|
247
|
+
const result = Bun.spawnSync(compileArgs(bun, entry, outfile, options), { cwd: workDir, stdout: "inherit", stderr: "inherit" });
|
|
134
248
|
return result.exitCode ?? 1;
|
|
135
249
|
} finally {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
250
|
+
removeWorkDir(workDir);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** {@link compileBinary} without blocking, with the compiler's output captured for the screen. */
|
|
255
|
+
export async function compileBinaryAsync(bun: string, entry: string, outfile: string, options: CompileOptions = {}): Promise<{ exitCode: number; output: string }> {
|
|
256
|
+
const workDir = mkdtempSync(path.join(tmpdir(), "bazis-compile-"));
|
|
257
|
+
try {
|
|
258
|
+
const child = Bun.spawn(compileArgs(bun, entry, outfile, options), { cwd: workDir, stdout: "pipe", stderr: "pipe" });
|
|
259
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
260
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
261
|
+
} finally {
|
|
262
|
+
removeWorkDir(workDir);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function compileArgs(bun: string, entry: string, outfile: string, options: CompileOptions): string[] {
|
|
267
|
+
const autoload = ["--no-compile-autoload-bunfig", options.dotenv === true ? "--compile-autoload-dotenv" : "--no-compile-autoload-dotenv"];
|
|
268
|
+
return [bun, "build", "--compile", ...autoload, path.resolve(entry), "--outfile", path.resolve(outfile)];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function removeWorkDir(workDir: string): void {
|
|
272
|
+
if (existsSync("/usr/bin/chflags")) Bun.spawnSync(["/usr/bin/chflags", "-R", "nouchg", workDir]);
|
|
273
|
+
try {
|
|
274
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
275
|
+
} catch (error) {
|
|
276
|
+
// The binary itself is complete; report the leftover without failing the build.
|
|
277
|
+
console.error(`[bazis] temporary directory was not removed: ${workDir}`, error);
|
|
143
278
|
}
|
|
144
279
|
}
|
|
145
280
|
|
package/cli/codegen.ts
CHANGED
|
@@ -1,21 +1,172 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import { PALETTE, formatDuration, type Problem, type Screen } from "../library/terminal";
|
|
5
|
+
import { commandFailed, createScreens, readFrameLines, type CliOutput } from "./ui";
|
|
3
6
|
|
|
4
7
|
const GENERATOR = "core/scripts/di-generate.ts";
|
|
5
8
|
|
|
6
|
-
/**
|
|
7
|
-
export
|
|
9
|
+
/** What the generator reports through `BAZIS_CODEGEN_REPORT`. */
|
|
10
|
+
export interface CodegenReport {
|
|
11
|
+
readonly ok: boolean;
|
|
12
|
+
readonly ms: number;
|
|
13
|
+
readonly targets: readonly {
|
|
14
|
+
readonly name: string; readonly sources: number; readonly deps: number; readonly controllers: number;
|
|
15
|
+
readonly requestModels: number; readonly listModels: number; readonly tools: number; readonly ms: number;
|
|
16
|
+
}[];
|
|
17
|
+
readonly outputs: readonly string[];
|
|
18
|
+
readonly warnings: readonly string[];
|
|
19
|
+
readonly errors: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CodegenOptions {
|
|
23
|
+
readonly ui?: CliOutput;
|
|
24
|
+
/** The last line of a failure: the exit code, or what `dev --watch` does next. */
|
|
25
|
+
readonly footer?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Runs the framework generator for the project in `cwd` and renders its
|
|
30
|
+
* result as the `codegen` step: counts and generated files, warnings, and on
|
|
31
|
+
* failure one problem per error with the code around it. Returns its exit code.
|
|
32
|
+
*/
|
|
33
|
+
export async function runCodegen(cwd: string, target?: string, options: CodegenOptions = {}): Promise<number> {
|
|
8
34
|
if (target !== undefined) {
|
|
9
35
|
const configFile = Bun.file(path.join(cwd, "bazis.config.json"));
|
|
10
36
|
if (!await configFile.exists()) throw new Error("--target requires bazis.config.json in the current directory.");
|
|
11
37
|
const config = await configFile.json();
|
|
12
38
|
if (target !== "all" && !Object.hasOwn(config.targets ?? {}, target)) throw new Error(`Unknown codegen target: ${target}`);
|
|
13
39
|
}
|
|
40
|
+
const ui = options.ui ?? createScreens(process.stdout, process.stderr);
|
|
41
|
+
const s = ui.out;
|
|
14
42
|
const generator = resolveGenerator(cwd);
|
|
15
43
|
// TypeScript 7 has no compiler API: the preload switches it to @typescript/typescript6.
|
|
16
44
|
const preload = path.join(path.dirname(generator), "typescriptApiPreload.ts");
|
|
17
45
|
const args = [await resolveBun(cwd), ...(existsSync(preload) ? ["--preload", preload] : []), generator, ...(target === undefined ? [] : ["--target", target])];
|
|
18
|
-
|
|
46
|
+
const workDir = mkdtempSync(path.join(tmpdir(), "bazis-codegen-"));
|
|
47
|
+
const reportFile = path.join(workDir, "report.json");
|
|
48
|
+
const stop = s.progress(s.t.step.codegen);
|
|
49
|
+
const startedAt = performance.now();
|
|
50
|
+
try {
|
|
51
|
+
const child = Bun.spawn(args, { cwd, env: { ...process.env, BAZIS_CODEGEN_REPORT: reportFile }, stdout: "pipe", stderr: "pipe" });
|
|
52
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
53
|
+
stop();
|
|
54
|
+
const report = readReport(reportFile);
|
|
55
|
+
if (report === undefined) {
|
|
56
|
+
// The generator did not get to its report: show what it printed.
|
|
57
|
+
s.step("fail", s.t.step.codegen, "", formatDuration(performance.now() - startedAt));
|
|
58
|
+
ui.err.line([stdout, stderr].filter((text) => text.trim()).join("\n").trimEnd());
|
|
59
|
+
return exitCode === 0 ? 1 : exitCode;
|
|
60
|
+
}
|
|
61
|
+
renderCodegen(ui, report, cwd, options.footer);
|
|
62
|
+
return report.ok && exitCode === 0 ? 0 : (exitCode || 1);
|
|
63
|
+
} finally {
|
|
64
|
+
stop();
|
|
65
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readReport(file: string): CodegenReport | undefined {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(readFileSync(file, "utf8")) as CodegenReport;
|
|
72
|
+
} catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function renderCodegen(ui: CliOutput, report: CodegenReport, cwd: string, footer?: string): void {
|
|
78
|
+
const s = ui.out;
|
|
79
|
+
const t = s.t;
|
|
80
|
+
const time = formatDuration(report.ms);
|
|
81
|
+
const target = report.targets.find((item) => item.name === "production") ?? report.targets[0];
|
|
82
|
+
if (!report.ok) {
|
|
83
|
+
s.step("fail", t.step.codegen, `${s.num(report.errors.length, t.noun("error", report.errors.length), PALETTE.red)}${report.warnings.length ? `${s.dot}${s.num(report.warnings.length, t.noun("warning", report.warnings.length), PALETTE.orange)}` : ""}`, time);
|
|
84
|
+
printWarnings(s, report.warnings);
|
|
85
|
+
s.line();
|
|
86
|
+
commandFailed(ui.err, t.failure.codegen(report.errors.length), report.errors.map((error) => codegenProblem(error, cwd, ui.err)), 1, footer);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const counts = target === undefined ? [] : [
|
|
90
|
+
s.num(target.sources, t.noun("file", target.sources)),
|
|
91
|
+
s.num(target.deps, t.noun("dep", target.deps)),
|
|
92
|
+
s.num(target.controllers, t.noun("controller", target.controllers)),
|
|
93
|
+
s.num(target.requestModels, t.noun("requestModel", target.requestModels)),
|
|
94
|
+
s.num(target.listModels, t.noun("list", target.listModels)),
|
|
95
|
+
s.num(target.tools, t.noun("tool", target.tools)),
|
|
96
|
+
];
|
|
97
|
+
s.step("ok", t.step.codegen, counts.join(s.dot), time);
|
|
98
|
+
const directories = new Map<string, string[]>();
|
|
99
|
+
for (const output of report.outputs) {
|
|
100
|
+
const directory = path.dirname(output);
|
|
101
|
+
directories.set(directory, [...(directories.get(directory) ?? []), path.basename(output, ".ts")]);
|
|
102
|
+
}
|
|
103
|
+
s.tree([...directories].map(([directory, files]) => ({ text: `${s.muted(`${directory}/`)} ${s.text(files.join(" · "))}` })), s.style.color ? 9 : 4);
|
|
104
|
+
printWarnings(s, report.warnings);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function printWarnings(s: Screen, warnings: readonly string[]): void {
|
|
108
|
+
for (const warning of warnings) {
|
|
109
|
+
const unassigned = /^BAZIS_CODEGEN_SOURCE_UNASSIGNED: ([^:]+):/.exec(warning);
|
|
110
|
+
if (unassigned) {
|
|
111
|
+
s.warning(`${s.t.codegen.unreachable(unassigned[1]!)}${s.dot}${s.muted(s.t.codegen.unreachableFix)}`);
|
|
112
|
+
} else {
|
|
113
|
+
s.warning(warning);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `CODE: file:line: message` → a problem with the code around the line. */
|
|
119
|
+
export function codegenProblem(error: string, cwd: string, screen: Screen): Problem {
|
|
120
|
+
const t = screen.t;
|
|
121
|
+
const parsed = /^([A-Z][A-Z0-9_]+): (?:([^\s:]+\.tsx?):(\d+): )?([\s\S]*)$/.exec(error);
|
|
122
|
+
if (parsed === null) return { tag: "CODEGEN", where: "codegen", cause: error };
|
|
123
|
+
const [, code, file, lineText, message] = parsed as unknown as [string, string, string | undefined, string | undefined, string];
|
|
124
|
+
const tag = code.startsWith("BAZIS_DI_") ? "DI" : code.startsWith("BAZIS_HTTP_") ? "HTTP" : "CODEGEN";
|
|
125
|
+
const fixes: string[] = [];
|
|
126
|
+
let frame: Problem["frame"];
|
|
127
|
+
let line = lineText === undefined ? undefined : Number(lineText);
|
|
128
|
+
if (file !== undefined && line !== undefined) {
|
|
129
|
+
const type = /has type "([^"]+)"/.exec(message)?.[1];
|
|
130
|
+
// The location is the class; the parameter with that type is a few lines below.
|
|
131
|
+
if (type !== undefined) line = parameterLine(path.resolve(cwd, file), line, type) ?? line;
|
|
132
|
+
const lines = readFrameLines(path.resolve(cwd, file), line);
|
|
133
|
+
const text = lines.find(([number]) => number === line)?.[1] ?? "";
|
|
134
|
+
const column = type === undefined ? -1 : text.search(new RegExp(`(?<=:\\s*)${escapeRegExp(type)}\\b`));
|
|
135
|
+
frame = lines.length === 0 ? undefined : {
|
|
136
|
+
lines,
|
|
137
|
+
line,
|
|
138
|
+
...(column >= 0 ? { column: column + 1, length: type!.length } : {}),
|
|
139
|
+
};
|
|
140
|
+
if (type === "SQL") fixes.push(t.codegen.sqlFix);
|
|
141
|
+
else if (type === "RedisClient") fixes.push(t.codegen.redisFix);
|
|
142
|
+
else if (code === "BAZIS_DI_DEPENDENCY_UNKNOWN") fixes.push(t.codegen.unknownDepFix);
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
tag,
|
|
146
|
+
where: file === undefined ? code : `${file}:${line}`,
|
|
147
|
+
cause: message.trim(),
|
|
148
|
+
fixes,
|
|
149
|
+
...(frame === undefined ? {} : { frame }),
|
|
150
|
+
code,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The first line from `from` on that declares something of type `type`. */
|
|
155
|
+
function parameterLine(file: string, from: number, type: string): number | undefined {
|
|
156
|
+
try {
|
|
157
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
158
|
+
const pattern = new RegExp(`:\\s*${escapeRegExp(type)}\\b`);
|
|
159
|
+
for (let index = from - 1; index < Math.min(lines.length, from + 40); index += 1) {
|
|
160
|
+
if (pattern.test(lines[index] ?? "")) return index + 1;
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function escapeRegExp(text: string): string {
|
|
169
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
19
170
|
}
|
|
20
171
|
|
|
21
172
|
/**
|
package/cli/generateProject.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { copyFile, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { parseModuleName } from "./naming";
|
|
4
|
+
import { parseLanguage, systemLanguage } from "../library/terminal";
|
|
4
5
|
|
|
5
6
|
export interface GenerateProjectOptions {
|
|
6
7
|
readonly name: string;
|
|
@@ -159,7 +160,9 @@ function buildProjectFiles(name: string, dependency: string, frameworkMode: Fram
|
|
|
159
160
|
},
|
|
160
161
|
include: ["src/**/*"],
|
|
161
162
|
};
|
|
162
|
-
|
|
163
|
+
// Output language of the CLI and the startup screen, from the system locale; BAZIS_LANG and --lang override it.
|
|
164
|
+
const language = parseLanguage(systemLanguage()) ?? "en";
|
|
165
|
+
const codegen = { version: 1, defaultTarget: "production", language, targets: { production: { entrypoints: ["src/index.ts"] } } };
|
|
163
166
|
return [
|
|
164
167
|
["package.json", `${JSON.stringify(manifest, null, 2)}\n`],
|
|
165
168
|
["tsconfig.json", `${JSON.stringify(tsconfig, null, 2)}\n`],
|
|
@@ -184,7 +187,10 @@ const HEALTH_TEST = `import { expect, test } from "bun:test";
|
|
|
184
187
|
|
|
185
188
|
// Starts the app from source exactly as \`bazis dev\` does and checks /health.
|
|
186
189
|
test("app starts and answers /health", async () => {
|
|
187
|
-
|
|
190
|
+
// A free port from the operating system, not a random guess that may be taken.
|
|
191
|
+
const probe = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response() });
|
|
192
|
+
const port = String(probe.port);
|
|
193
|
+
probe.stop(true);
|
|
188
194
|
const app = Bun.spawn([process.execPath, "run", "src/index.ts"], { env: { ...process.env, HOST: "127.0.0.1", PORT: port }, stdout: "ignore", stderr: "inherit" });
|
|
189
195
|
try {
|
|
190
196
|
let status = 0;
|