bazis 0.106.0 → 0.107.0

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 CHANGED
@@ -14,9 +14,13 @@ export interface BuildOptions {
14
14
  readonly outfile?: string;
15
15
  /** Let the executable read `.env` files from its working directory (off by default). */
16
16
  readonly dotenv?: boolean;
17
+ /** Another platform, `linux-x64`: `bun build --target=bun-linux-x64`. */
18
+ readonly platform?: string;
17
19
  }
18
20
 
19
21
  export interface CompileOptions {
22
+ /** Cross-compile for `bun-<platform>`; Bun downloads that runtime once. */
23
+ readonly platform?: string;
20
24
  /** Let the executable read `.env`, `.env.local`, `.env.<NODE_ENV>` from its working directory. */
21
25
  readonly dotenv?: boolean;
22
26
  }
@@ -149,10 +153,11 @@ export async function runBuild(cwd: string, options: BuildOptions, codegen: Code
149
153
  const s = ui.out;
150
154
  const t = s.t;
151
155
  const startedAt = performance.now();
152
- const outfile = options.bin ? path.resolve(cwd, options.outfile ?? path.join("bin", await binaryName(cwd))) : undefined;
156
+ const outfile = options.bin ? path.resolve(cwd, options.outfile ?? path.join("bin", await defaultBinaryName(cwd, options.platform))) : undefined;
153
157
  commandHeader(s, "build", cwd, [
154
158
  s.kv(t.meta.target, s.text(options.bin ? "executable" : "typecheck")),
155
159
  ...(outfile === undefined ? [] : [s.kv(t.meta.out, s.text(path.relative(cwd, outfile)))]),
160
+ ...(options.platform === undefined ? [] : [s.kv(t.meta.platform, s.text(options.platform))]),
156
161
  ]);
157
162
  s.section(t.section.build);
158
163
  const generated = await codegen(cwd, "all", { ui });
@@ -182,7 +187,7 @@ export async function runBuild(cwd: string, options: BuildOptions, codegen: Code
182
187
  s.section(t.section.binary);
183
188
  const stopCompile = s.progress(t.step.compile);
184
189
  const compileStarted = performance.now();
185
- const compiled = await compileBinaryAsync(bun, await projectEntry(cwd), outfile, { dotenv: options.dotenv });
190
+ const compiled = await compileBinaryAsync(bun, await projectEntry(cwd), outfile, { dotenv: options.dotenv, platform: options.platform });
186
191
  stopCompile();
187
192
  const compileMs = formatDuration(performance.now() - compileStarted);
188
193
  const relative = path.relative(cwd, outfile);
@@ -263,9 +268,10 @@ export async function compileBinaryAsync(bun: string, entry: string, outfile: st
263
268
  }
264
269
  }
265
270
 
266
- function compileArgs(bun: string, entry: string, outfile: string, options: CompileOptions): string[] {
271
+ export function compileArgs(bun: string, entry: string, outfile: string, options: CompileOptions): string[] {
267
272
  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)];
273
+ const target = options.platform === undefined ? [] : [`--target=bun-${options.platform}`];
274
+ return [bun, "build", "--compile", ...target, ...autoload, path.resolve(entry), "--outfile", path.resolve(outfile)];
269
275
  }
270
276
 
271
277
  function removeWorkDir(workDir: string): void {
@@ -287,6 +293,12 @@ async function projectEntry(cwd: string): Promise<string> {
287
293
  return path.resolve(cwd, entry);
288
294
  }
289
295
 
296
+ /** `bin/<name>`, or `bin/<name>-<platform>` for another platform, so the local build is kept. */
297
+ async function defaultBinaryName(cwd: string, platform: string | undefined): Promise<string> {
298
+ const name = await binaryName(cwd);
299
+ return platform === undefined ? name : `${name}-${platform}`;
300
+ }
301
+
290
302
  async function binaryName(cwd: string): Promise<string> {
291
303
  const manifest = Bun.file(path.join(cwd, "package.json"));
292
304
  const name = await manifest.exists() ? (await manifest.json() as { name?: unknown }).name : undefined;
package/cli/main.ts CHANGED
@@ -20,6 +20,8 @@ export const USAGE = `Usage:
20
20
  bazis build Codegen and typecheck
21
21
  bazis build --bin [--outfile <path>] Also compile a standalone executable (default bin/<name>)
22
22
  bazis build --bin --dotenv The executable also reads .env files from its working directory
23
+ bazis build --bin --platform <os-arch> For another platform, e.g. linux-x64, linux-arm64-musl
24
+ (default output bin/<name>-<os-arch>)
23
25
  bazis --help
24
26
  bazis --version Print the bazis version (also -v)
25
27
  g can also be written as generate.
@@ -130,7 +132,7 @@ export async function runCli(argv: readonly string[], runtime: CliRuntime = defa
130
132
  if (parsed.kind === "codegen") return await runCodegenCommand(cwd, parsed.target, codegen, ui);
131
133
  if (parsed.kind === "dev") return await runDev(cwd, codegen, ui, { watch: parsed.watch });
132
134
  if (parsed.kind === "test") return await runTest(cwd, parsed.args, codegen, ui);
133
- if (parsed.kind === "build") return await runBuild(cwd, { bin: parsed.bin, outfile: parsed.outfile, dotenv: parsed.dotenv }, codegen, ui);
135
+ if (parsed.kind === "build") return await runBuild(cwd, { bin: parsed.bin, outfile: parsed.outfile, dotenv: parsed.dotenv, platform: parsed.platform }, codegen, ui);
134
136
  if (parsed.kind === "new") return await runNew(parsed, ui);
135
137
  // Module registration parses the host module with the TypeScript API: load it
136
138
  // only after switching TypeScript 7 to @typescript/typescript6.
package/cli/parseCli.ts CHANGED
@@ -26,14 +26,16 @@ export type ParseCliResult =
26
26
  | { readonly kind: "codegen"; readonly target?: string }
27
27
  | { readonly kind: "dev"; readonly watch: boolean }
28
28
  | { readonly kind: "test"; readonly args: readonly string[] }
29
- | { readonly kind: "build"; readonly bin: boolean; readonly outfile?: string; readonly dotenv: boolean }
29
+ | { readonly kind: "build"; readonly bin: boolean; readonly outfile?: string; readonly dotenv: boolean; readonly platform?: string }
30
30
  | { readonly kind: "error"; readonly message: string };
31
31
 
32
32
  const GENERATOR_ALIASES = new Map<string, "module" | "pack">([
33
33
  ["module", "module"], ["m", "module"],
34
34
  ["pack", "pack"], ["p", "pack"], ["module-pack", "pack"],
35
35
  ]);
36
- const VALUE_OPTIONS = new Set(["--modules-root", "--app-module", "--pack", "--parts", "--target", "--path", "--framework", "--outfile"]);
36
+ const VALUE_OPTIONS = new Set(["--modules-root", "--app-module", "--pack", "--parts", "--target", "--path", "--framework", "--outfile", "--platform"]);
37
+ /** Platforms of `bun build --compile --target=bun-<platform>`. */
38
+ export const BINARY_PLATFORM = /^(linux|darwin|windows)-(x64|arm64)(-musl)?(-baseline|-modern)?$/;
37
39
  const FLAG_OPTIONS = new Set(["--no-register", "--force", "--full", "--enterprise", "--minimal", "--empty", "--dry-run", "--no-codegen", "--link-framework", "--vendor", "--bin", "--watch", "--dotenv"]);
38
40
 
39
41
  /** Parsing is pure: help and invalid input can never start generation. */
@@ -91,15 +93,20 @@ export function parseCliArgs(argv: readonly string[]): ParseCliResult {
91
93
  }
92
94
  if (options.has("--watch")) return error("--watch is only supported by dev.");
93
95
  if (command === "build") {
94
- if (positional.length !== 1) return error("Use: bazis build [--bin [--outfile <path>] [--dotenv]]");
96
+ if (positional.length !== 1) return error("Use: bazis build [--bin [--outfile <path>] [--platform <os-arch>] [--dotenv]]");
95
97
  for (const option of options.keys()) {
96
- if (option !== "--bin" && option !== "--outfile" && option !== "--dotenv") return error(`Option ${option} is not supported by build.`);
98
+ if (option !== "--bin" && option !== "--outfile" && option !== "--dotenv" && option !== "--platform") return error(`Option ${option} is not supported by build.`);
97
99
  }
98
100
  if (options.has("--outfile") && !options.has("--bin")) return error("--outfile requires --bin.");
99
101
  if (options.has("--dotenv") && !options.has("--bin")) return error("--dotenv requires --bin.");
100
- return { kind: "build", bin: options.has("--bin"), outfile: options.get("--outfile"), dotenv: options.has("--dotenv") };
102
+ if (options.has("--platform") && !options.has("--bin")) return error("--platform requires --bin.");
103
+ const platform = options.get("--platform");
104
+ if (platform !== undefined && !BINARY_PLATFORM.test(platform)) {
105
+ return error(`Unknown platform: ${platform}. Use <os>-<arch>[-musl]: linux-x64, linux-arm64, linux-x64-musl, linux-arm64-musl, darwin-arm64, darwin-x64, windows-x64.`);
106
+ }
107
+ return { kind: "build", bin: options.has("--bin"), outfile: options.get("--outfile"), dotenv: options.has("--dotenv"), ...(platform === undefined ? {} : { platform }) };
101
108
  }
102
- if (options.has("--bin") || options.has("--outfile") || options.has("--dotenv")) return error("--bin, --outfile and --dotenv are only supported by build.");
109
+ if (options.has("--bin") || options.has("--outfile") || options.has("--dotenv") || options.has("--platform")) return error("--bin, --outfile, --platform and --dotenv are only supported by build.");
103
110
  if (command === "codegen") {
104
111
  if (positional.length !== 1) return error("Use: bazis codegen [--target <name|all>]");
105
112
  for (const option of options.keys()) {
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
232
232
  if (method === "account/updated") this.modelCache = undefined;
233
233
  }, () => { this.clearLogin(); this.modelCache = undefined; });
234
234
  const initialized = await client.request("initialize", {
235
- clientInfo: { name: "bazis", title: "Bazis", version: "0.106.0" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.107.0" }, capabilities: { experimentalApi: true },
236
236
  }, 15_000, signal);
237
237
  if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
238
238
  client.notify("initialized");
@@ -261,18 +261,32 @@ export class Kernel {
261
261
  if (this.startupCleanupFailure !== undefined) return Promise.reject(this.startupCleanupFailure);
262
262
  return Promise.resolve();
263
263
  }
264
+ // Only an application that started announces its stop.
265
+ const wasStarted = this.state === "started";
264
266
  this.state = "stopping";
265
- this.shutdownPromise = this.stopCore(request);
267
+ this.shutdownPromise = this.stopCore(request, wasStarted);
266
268
  return this.shutdownPromise;
267
269
  }
268
270
 
269
- private async stopCore(request: StopRequest): Promise<void> {
271
+ private async stopCore(request: StopRequest, wasStarted: boolean): Promise<void> {
270
272
  // The whole graceful-stop sequence — lifetime notifications, the STOPPING
271
273
  // event, hosted services/hooks and disposal — is bounded by one budget. A
272
274
  // single hanging callback or handler must not wedge shutdown forever.
275
+ const announce = wasStarted && this.options.startupReport && this.options.schemaOnly !== true;
276
+ const logger = announce ? this.container.tryResolve(LOGGER) : undefined;
277
+ const startedAt = performance.now();
273
278
  const work = this.runShutdownSequence(request);
274
279
  try {
275
280
  await awaitShutdown(work, this.options.shutdownTimeoutMs);
281
+ if (announce) {
282
+ const fields = {
283
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
284
+ exitCode: request.exitCode,
285
+ shutdownMs: Number((performance.now() - startedAt).toFixed(1)),
286
+ };
287
+ if (logger) logger.info("bazis stopped", fields);
288
+ else console.log(`[bazis] stopped ${JSON.stringify(fields)}`);
289
+ }
276
290
  } finally {
277
291
  this.state = "stopped";
278
292
  }
@@ -441,6 +455,12 @@ export class Kernel {
441
455
  console.error("[bazis] Unhandled error, shutting down:", redacted);
442
456
  }
443
457
 
458
+ /** Where the servers listen: `{ listening: ["http://localhost:3000"] }`. */
459
+ private listening(): { listening?: readonly string[] } {
460
+ const urls = (this.container.tryResolve(StartupReport)?.listeners ?? []).map((listener) => listener.url);
461
+ return urls.length > 0 ? { listening: urls } : {};
462
+ }
463
+
444
464
  private printStartupReport(): void {
445
465
  if (this.options.onStarted !== undefined) {
446
466
  try {
@@ -460,6 +480,7 @@ export class Kernel {
460
480
  hostedServices: this.coordinator.startedHostedCount,
461
481
  hooks: this.coordinator.hookCount,
462
482
  startupMs: Number(this.startupMs.toFixed(1)),
483
+ ...this.listening(),
463
484
  };
464
485
  if (logger) {
465
486
  logger.info("bazis started", fields);
@@ -166,9 +166,11 @@ export class KernelBuilder {
166
166
  const userRoot = resolveUserRoot(this.rootModule, environment);
167
167
  const configs = collectModuleConfigs([userRoot]);
168
168
  const configRegistry = new ConfigRegistry(configs, environment.name, configuration);
169
- const startupReport = this.startupReport ?? !environment.isProduction;
170
- // The screen replaces the report line, so it follows the same switch.
171
- const screen = this.startupScreen !== undefined && startupReport;
169
+ // The `bazis started` / `bazis stopped` lines are printed in every
170
+ // environment: in production they are the only sign that the process is up.
171
+ const startupReport = this.startupReport ?? true;
172
+ // The screen replaces the started line outside production.
173
+ const screen = this.startupScreen !== undefined && (this.startupReport ?? !environment.isProduction);
172
174
  const report = new StartupReport(screen);
173
175
  report.configuration(configRegistry.inspect());
174
176
  this.builtReport = report;
@@ -98,7 +98,7 @@ const en = {
98
98
  codegen: "codegen", typecheck: "typecheck", config: "config", connections: "connections", schema: "schema",
99
99
  modules: "modules", services: "services", tests: "bun test", compile: "compile", files: "files", register: "register",
100
100
  },
101
- meta: { env: "env", watch: "watch", on: "on", off: "off", src: "src", pid: "pid", target: "target", out: "output", path: "path" },
101
+ meta: { env: "env", watch: "watch", on: "on", off: "off", src: "src", pid: "pid", target: "target", out: "output", path: "path", platform: "platform" },
102
102
  common: {
103
103
  of: "of", in: "in", and: "and", none: "none", ok: "ok", empty: "empty", plus: "plus", memory: "memory",
104
104
  declaredIn: "declared in", code: "code", where: "where", try: "try", logs: "logs", inProcess: "in process",
@@ -260,7 +260,7 @@ const ru: Messages = {
260
260
  codegen: "кодогенерация", typecheck: "типы", config: "конфигурация", connections: "подключения", schema: "схема",
261
261
  modules: "модули", services: "сервисы", tests: "bun test", compile: "компиляция", files: "файлы", register: "подключение",
262
262
  },
263
- meta: { env: "среда", watch: "слежение", on: "вкл", off: "выкл", src: "вход", pid: "pid", target: "цель", out: "файл", path: "путь" },
263
+ meta: { env: "среда", watch: "слежение", on: "вкл", off: "выкл", src: "вход", pid: "pid", target: "цель", out: "файл", path: "путь", platform: "платформа" },
264
264
  common: {
265
265
  of: "из", in: "в", and: "и", none: "нет", ok: "в порядке", empty: "пусто", plus: "ещё", memory: "память",
266
266
  declaredIn: "объявлено в", code: "код", where: "где", try: "попробуйте", logs: "журнал", inProcess: "в процессе",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.106.0",
3
+ "version": "0.107.0",
4
4
  "description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Alexander Alenin",