bazis 0.97.2 → 0.97.4

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
@@ -13,7 +13,7 @@ export interface BuildOptions {
13
13
  }
14
14
 
15
15
  /**
16
- * `bazis dev`: codegen, then run the default target's entrypoint from source.
16
+ * `bazis dev`: codegen of every target, then run the default target's entrypoint from source.
17
17
  * With `watch`, a change under src/ (except src/generated) stops the app,
18
18
  * reruns codegen and starts it again; a failed codegen waits for the next change.
19
19
  * The app runs as `development` unless BAZIS_ENV is set in the shell.
@@ -28,7 +28,7 @@ export async function runDev(cwd: string, codegen: Codegen, log: (message: strin
28
28
  const entry = await projectEntry(cwd);
29
29
  const env = devEnvironment(process.env);
30
30
  if (!options.watch) {
31
- const generated = await codegen(cwd);
31
+ const generated = await codegen(cwd, "all");
32
32
  if (generated !== 0) return generated;
33
33
  const child = Bun.spawn([bun, "run", entry], { cwd, env, stdin: "inherit", stdout: "inherit", stderr: "inherit" });
34
34
  // The app owns graceful shutdown; forward the signal instead of dying first.
@@ -55,7 +55,7 @@ export async function runDev(cwd: string, codegen: Codegen, log: (message: strin
55
55
  await running.exited;
56
56
  };
57
57
  const start = async () => {
58
- if (await codegen(cwd) !== 0) { log("[bazis] codegen failed; waiting for changes..."); return; }
58
+ if (await codegen(cwd, "all") !== 0) { log("[bazis] codegen failed; waiting for changes..."); return; }
59
59
  child = Bun.spawn([bun, "run", entry], { cwd, env, stdin: "inherit", stdout: "inherit", stderr: "inherit" });
60
60
  };
61
61
  const watcher = watch(path.join(cwd, "src"), { recursive: true }, (_event, file) => {
@@ -84,16 +84,16 @@ export async function runDev(cwd: string, codegen: Codegen, log: (message: strin
84
84
  }
85
85
  }
86
86
 
87
- /** `bazis test [args]`: codegen, then `bun test` with the given arguments. */
87
+ /** `bazis test [args]`: codegen of every target, then `bun test` with the given arguments. */
88
88
  export async function runTest(cwd: string, args: readonly string[], codegen: Codegen): Promise<number> {
89
- const generated = await codegen(cwd);
89
+ const generated = await codegen(cwd, "all");
90
90
  if (generated !== 0) return generated;
91
91
  return await Bun.spawn([await resolveBun(cwd), "test", ...args], { cwd, stdin: "inherit", stdout: "inherit", stderr: "inherit" }).exited;
92
92
  }
93
93
 
94
- /** `bazis build [--bin]`: codegen and typecheck; with `bin`, also compile. */
94
+ /** `bazis build [--bin]`: codegen of every target and typecheck; with `bin`, also compile. */
95
95
  export async function runBuild(cwd: string, options: BuildOptions, codegen: Codegen, log: (message: string) => void): Promise<number> {
96
- const generated = await codegen(cwd);
96
+ const generated = await codegen(cwd, "all");
97
97
  if (generated !== 0) return generated;
98
98
  const bun = await resolveBun(cwd);
99
99
  const tsc = path.join(cwd, "node_modules/typescript/bin/tsc");
package/cli/main.ts CHANGED
@@ -115,7 +115,7 @@ export async function runCli(argv: readonly string[], runtime: CliRuntime = defa
115
115
  runtime.log(`[bazis] codegen skipped: ${!args.codegen ? "--no-codegen" : "module is not connected by this command"}.`);
116
116
  return 0;
117
117
  }
118
- runtime.log("[bazis] running di:generate...");
118
+ runtime.log("[bazis] running codegen...");
119
119
  const exitCode = await runtime.codegen(process.cwd(), args.target);
120
120
  if (exitCode !== 0) runtime.error("[bazis] codegen failed; scaffold files were kept. Fix the error and run bazis codegen.");
121
121
  return exitCode;
@@ -1,4 +1,4 @@
1
- // This file is auto-generated by `bun run di:generate`.
1
+ // This file is auto-generated by `bazis codegen`.
2
2
  // Do not edit manually.
3
3
 
4
4
  import type { Class } from "../../di";
@@ -746,7 +746,7 @@ function resolveAndValidateResponseSchema(
746
746
  diagnostics.push(uiDiagnosticV1(
747
747
  "error",
748
748
  "UIV1_RESPONSE_MODEL_NOT_REGISTERED",
749
- `${profileName} response class is not bound to a generated OpenAPI schema. Run bun run di:generate.`,
749
+ `${profileName} response class is not bound to a generated OpenAPI schema. Run \`bazis codegen\`.`,
750
750
  { profile: profileName, path: "response" },
751
751
  ));
752
752
  return actual;
@@ -112,6 +112,23 @@ export class ServiceProvider implements ServiceResolver {
112
112
  return this.resolveSingleAsync(token, key, this.lifetime.root, []);
113
113
  }
114
114
 
115
+ /**
116
+ * Creates every singleton registered with an async factory (keyed ones
117
+ * included), in registration order. Afterwards `resolve` and constructor
118
+ * injection return the cached instances. The kernel calls it at startup, so a
119
+ * failing factory stops the start instead of failing every request.
120
+ */
121
+ public async initializeAsyncSingletons(): Promise<void> {
122
+ this.lifetime.assertLive(this.lifetime.root);
123
+ for (const group of this.registry.groups()) {
124
+ for (const registration of group) {
125
+ if (registration.lifetime === "singleton" && isAsyncFactoryProvider(registration.provider)) {
126
+ await this.resolveRegistrationAsync(registration, registration.key, this.lifetime.root, [], undefined);
127
+ }
128
+ }
129
+ }
130
+ }
131
+
115
132
  public tryResolve<T>(token: Token<T>, key?: ServiceKey): T | undefined {
116
133
  return this.tryResolveForScope(token, key, this.lifetime.root);
117
134
  }
@@ -9,7 +9,7 @@ export class ClassDependenciesMismatchError extends DiError {
9
9
  declaredDeps: number,
10
10
  ) {
11
11
  super(
12
- `Class provider "${tokenToDebugName(token)}" requires at least ${requiredConstructorParams} constructor deps, but only ${declaredDeps} declared.`,
12
+ `Class provider "${tokenToDebugName(token)}" requires at least ${requiredConstructorParams} constructor deps, but only ${declaredDeps} declared. Constructor dependencies are wired by codegen: if the constructor was added or changed after the last run, run \`bazis codegen\` (bazis dev, bazis test and bazis build run it automatically); otherwise pass the deps explicitly.`,
13
13
  );
14
14
  }
15
15
  }
@@ -70,7 +70,7 @@ export class GraphValidator {
70
70
  const shape = this.context.classShape(registration.provider);
71
71
  if (shape && shape.required > shape.declared) {
72
72
  issues.add(
73
- `Class provider "${tokenToDebugName(registration.token)}" requires at least ${shape.required} constructor deps, but only ${shape.declared} declared.`,
73
+ `Class provider "${tokenToDebugName(registration.token)}" requires at least ${shape.required} constructor deps, but only ${shape.declared} declared. Constructor dependencies are wired by codegen: if the constructor was added or changed after the last run, run \`bazis codegen\` (bazis dev, bazis test and bazis build run it automatically); otherwise pass the deps explicitly.`,
74
74
  );
75
75
  }
76
76
 
@@ -37,6 +37,16 @@ export function frameworkVersion(): string | undefined {
37
37
  * (or with another framework version): new routes and dependencies would be
38
38
  * silently missing. A compiled binary carries no sources, so it is skipped.
39
39
  */
40
+ let staleGeneratedSources = false;
41
+
42
+ /**
43
+ * True when the loaded generated code was produced from older sources (or
44
+ * another bazis version) than the ones running now.
45
+ */
46
+ export function generatedSourcesAreStale(): boolean {
47
+ return staleGeneratedSources;
48
+ }
49
+
40
50
  export function warnIfGeneratedSourcesChanged(
41
51
  generatedDir: string,
42
52
  fingerprint: GeneratedSourceFingerprint,
@@ -49,6 +59,7 @@ export function warnIfGeneratedSourcesChanged(
49
59
  const version = frameworkVersion();
50
60
  if (version !== undefined && version !== fingerprint.framework) changes.push(`bazis ${fingerprint.framework} -> ${version}`);
51
61
  if (changes.length === 0) return false;
62
+ staleGeneratedSources = true;
52
63
  warn([
53
64
  "",
54
65
  `[bazis] WARNING: generated code is out of date (${changes.join("; ")}).`,
@@ -24,7 +24,7 @@ export function registerGeneratedBindings(
24
24
  }
25
25
 
26
26
  /**
27
- * Binding conventions inferred by codegen (`bun run di:generate`) from the
27
+ * Binding conventions inferred by codegen (`bazis codegen`) from the
28
28
  * controller method signature, the counterpart of DI auto deps:
29
29
  *
30
30
  * - a parameter named like a route `:name` -> from the route;
@@ -72,7 +72,7 @@ function toBinding(spec: GeneratedBindingSpec, controllerName: string, methodNam
72
72
  if (ctor === undefined) {
73
73
  throw new HttpSetupError(
74
74
  `${controllerName}.${methodName}: list model "${spec.model}" is not registered. ` +
75
- `Make sure the class extends ListRequest and its module is imported, then run \`bun run di:generate\`.`,
75
+ `Make sure the class extends ListRequest and its module is imported, then run \`bazis codegen\`.`,
76
76
  );
77
77
  }
78
78
  if (ctor === AMBIGUOUS_LIST_MODEL) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Format of the generated binding conventions (`bun run di:generate`).
2
+ * Format of the generated binding conventions (`bazis codegen`).
3
3
  *
4
4
  * Plain data without class references: the body model is given **by name** and
5
5
  * resolved at startup through the class index of the generated target.
@@ -48,7 +48,7 @@ export interface ModelBindingOptions {
48
48
  *
49
49
  * The model class must have a parameterless constructor. Class-typed nested
50
50
  * DTOs (including arrays) are recursively hydrated from metadata emitted by
51
- * `bun run di:generate`; initialized nested DTO fields remain supported as a
51
+ * `bazis codegen`; initialized nested DTO fields remain supported as a
52
52
  * runtime fallback.
53
53
  */
54
54
  export function bindModel<T extends object>(model: Class<T>, data: unknown, validator?: ModelValidator, options: ModelBindingOptions = {}): T {
@@ -13,7 +13,7 @@ export const AMBIGUOUS_REQUEST_MODEL: unique symbol = Symbol("ambiguous-request-
13
13
  export type RequestModelClass = new () => object;
14
14
 
15
15
  /**
16
- * Runtime hydration metadata emitted by `bun run di:generate` for a class-
16
+ * Runtime hydration metadata emitted by `bazis codegen` for a class-
17
17
  * typed request-model field. Standard TC39 decorators deliberately do not
18
18
  * expose design types, so this tiny registry is the dependency-free bridge
19
19
  * from TypeScript source types to the HTTP binder.
@@ -1,6 +1,7 @@
1
1
  import type { Class, Token } from "../../di";
2
2
  import { bindArguments } from "../Binding/ParameterBinder";
3
3
  import { resolveGeneratedBindings } from "../Binding/autoBindings";
4
+ import { generatedSourcesAreStale } from "../../generatedFingerprint";
4
5
  import { controllerMetaOf, type ActionMeta, type ControllerMeta } from "../Decorators/metadata";
5
6
  import { HttpSetupError } from "../Errors/HttpError";
6
7
  import type { HttpContext } from "../HttpContext/HttpContext";
@@ -108,6 +109,7 @@ export class RouterBuilder {
108
109
  const filters: readonly ActionFilterHooks[] = [...meta.filters, ...action.filters];
109
110
  const catches = meta.catches;
110
111
  const bindings = resolveGeneratedBindings(controllerClass, methodName);
112
+ assertBindingsFresh(controllerClass, methodName, bindings);
111
113
  const responseDefaults = { httpCode: action.httpCode, produces: action.produces };
112
114
  const bindingDefaults = { consumes: action.consumes };
113
115
  const token = controllerClass as unknown as Token<object>;
@@ -182,3 +184,28 @@ export class RouterBuilder {
182
184
  return { boundary, terminal };
183
185
  }
184
186
  }
187
+
188
+ /**
189
+ * Without generated bindings a method receives only the HttpContext. When the
190
+ * generated code is older than the sources, a method that declares parameters
191
+ * but has no bindings was most likely added or changed after the last codegen:
192
+ * its arguments would silently get wrong values, so the server refuses to start.
193
+ */
194
+ function assertBindingsFresh(
195
+ controllerClass: Class<object>,
196
+ methodName: string | symbol,
197
+ bindings: readonly unknown[] | undefined,
198
+ ): void {
199
+ if (bindings !== undefined || !generatedSourcesAreStale()) {
200
+ return;
201
+ }
202
+ const method = (controllerClass.prototype as Record<string | symbol, unknown>)[methodName];
203
+ if (typeof method !== "function" || method.length === 0) {
204
+ return;
205
+ }
206
+ throw new HttpSetupError(
207
+ `${controllerClass.name}.${String(methodName)} has parameters but no generated argument bindings: ` +
208
+ "the generated code is older than this controller. Run `bazis codegen` " +
209
+ "(bazis dev, bazis test and bazis build run it automatically).",
210
+ );
211
+ }
@@ -116,7 +116,7 @@ export interface HttpModuleOptions {
116
116
  readonly health?: HealthEndpointOptions | boolean;
117
117
  /**
118
118
  * Built-in generated API docs. No user-facing Swagger decorators are needed:
119
- * `bun run di:generate` derives request models and schemas from controllers.
119
+ * `bazis codegen` derives request models and schemas from controllers.
120
120
  *
121
121
  * Default: enabled in debug environments, disabled in production. `true`
122
122
  * serves `/docs` and `/docs/openapi.json`; pass an object to change paths or
@@ -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.97.2" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.97.4" }, 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");
@@ -13,6 +13,7 @@ import type { UnhandledErrorPolicy } from "./types";
13
13
  import { redactSensitive } from "../../library/redaction";
14
14
  import { validateTimeout } from "./internal/validateTimeout";
15
15
  import { awaitShutdown } from "./internal/awaitShutdown";
16
+ import { awaitAbortable } from "./internal/awaitAbortable";
16
17
  import { reportDiagnosticFailure } from "./internal/reportDiagnosticFailure";
17
18
 
18
19
  export interface KernelTimings {
@@ -97,6 +98,9 @@ export class Kernel {
97
98
  );
98
99
  }
99
100
  try {
101
+ // Async singletons first: hosted services and controllers take them
102
+ // through constructors, which resolve synchronously.
103
+ await awaitAbortable(this.container.initializeAsyncSingletons(), abortController.signal);
100
104
  await this.coordinator.start(abortController.signal, this.options.shutdownTimeoutMs);
101
105
  coordinatorStarted = true;
102
106
  this.startupMs = performance.now() - this.timings.buildStartedAt;
@@ -879,7 +879,7 @@ function renderAgentCatalog(context: AgentCodegenContext): RenderedAgentCatalog
879
879
  const validPrompts = filterImportablePromptEntries(context.prompts, imports, context);
880
880
 
881
881
  const lines: string[] = [];
882
- lines.push("// This file is auto-generated by `bun run di:generate`.");
882
+ lines.push("// This file is auto-generated by `bazis codegen`.");
883
883
  lines.push("// Do not edit manually.");
884
884
  lines.push("");
885
885
  if (context.frameworkImports === "public") {
@@ -156,6 +156,8 @@ interface CollectedDependency { readonly name: string; readonly target?: ts.Clas
156
156
  interface ConstructorDependencies {
157
157
  readonly deps: CollectedDependency[];
158
158
  readonly unsupportedInheritedParameter?: string;
159
+ /** An own constructor parameter whose type cannot name a DI dependency (`string`, `number`, an inline type). */
160
+ readonly unsupportedParameter?: { readonly index: number; readonly name: string; readonly type: string };
159
161
  }
160
162
  let classDeps = new Map<ts.ClassDeclaration, readonly CollectedDependency[]>();
161
163
  let inferredDiClasses = new Set<ts.ClassDeclaration>();
@@ -545,7 +547,7 @@ function failOnFatalErrors(): void {
545
547
  function renderGeneratedRuntime(targetId: string): string {
546
548
  if (activeGeneratedDir !== APP_GENERATED_DIR) {
547
549
  return [
548
- "// This file is auto-generated by `bun run di:generate`.",
550
+ "// This file is auto-generated by `bazis codegen`.",
549
551
  "// Do not edit manually.",
550
552
  "",
551
553
  'import { registerBazisGeneratedTargetDescriptor } from "bazis/core/generatedRuntime";',
@@ -565,7 +567,7 @@ function renderGeneratedRuntime(targetId: string): string {
565
567
  ].join("\n");
566
568
  }
567
569
  return [
568
- "// This file is auto-generated by `bun run di:generate`.",
570
+ "// This file is auto-generated by `bazis codegen`.",
569
571
  "// Do not edit manually.",
570
572
  "",
571
573
  "let registered: Promise<void> | undefined;",
@@ -615,7 +617,7 @@ function renderTargetDeps(): string {
615
617
  if (dependency.target && !references.has(dependency.target)) references.set(dependency.target, `DependencyClass_${references.size}`);
616
618
  }
617
619
  }
618
- const lines = ["// This file is auto-generated by `bun run di:generate`.", "// Do not edit manually."];
620
+ const lines = ["// This file is auto-generated by `bazis codegen`.", "// Do not edit manually."];
619
621
  if (entries.some(([, deps]) => deps.some(dep => dep.target && dep.lazy))) lines.push('import { lazyDependency } from "bazis/core/di";');
620
622
  for (const [declaration, reference] of references) {
621
623
  const name = dependencyClassExportName(declaration);
@@ -668,7 +670,7 @@ function renderTargetBindings(): string {
668
670
  // bindable parameters as well as ordinary binding-bearing controllers.
669
671
  const entries = [...new Set([...Object.keys(httpBindings), ...Object.keys(openApiOperations)])]
670
672
  .sort((a, b) => a.localeCompare(b));
671
- const lines = ["// This file is auto-generated by `bun run di:generate`.", "// Do not edit manually."];
673
+ const lines = ["// This file is auto-generated by `bazis codegen`.", "// Do not edit manually."];
672
674
  for (let index = 0; index < entries.length; index += 1) {
673
675
  const name = entries[index] as string;
674
676
  const file = httpControllerFiles[name] ?? classFilesByName.get(name);
@@ -796,6 +798,10 @@ function collectClassDeps(source: ts.SourceFile): void {
796
798
  const visit = (node: ts.Node): void => {
797
799
  if (ts.isClassDeclaration(node) && node.name) {
798
800
  const result = readConstructorTypeNames(node);
801
+ if (result.unsupportedParameter !== undefined && needsInferredDiDeps(node)) {
802
+ const { index, name, type } = result.unsupportedParameter;
803
+ fatalErrors.push(`BAZIS_DI_DEPENDENCY_UNKNOWN: ${sourceLocation(node)}: constructor parameter ${index + 1} "${name}" of "${node.name.text}" has type "${type}", which cannot be injected: DI resolves dependencies by class, contract or token, and a plain type does not say which value to inject. Declare a token with the same name as a type alias (export type Prefix = string; export const Prefix = createToken<Prefix>("Prefix")) and register a value, or pass the deps explicitly: scoped(${node.name.text}, ${node.name.text}, [TOKEN] as const).`);
804
+ }
799
805
  if (result.unsupportedInheritedParameter !== undefined && needsInferredDiDeps(node)) {
800
806
  fatalErrors.push(`BAZIS_DI_CONSTRUCTOR_UNRESOLVED: ${sourceLocation(node)}: cannot infer inherited constructor parameter "${result.unsupportedInheritedParameter}" for "${node.name.text}". The parameter needs a runtime DI token or an explicit value/factory binding.`);
801
807
  }
@@ -984,7 +990,7 @@ function readConstructorTypeNames(node: ts.ClassDeclaration): ConstructorDepende
984
990
  const paramType = param.type;
985
991
  const refName = paramType ? getDependencyTypeName(paramType) : undefined;
986
992
  if (!refName || BUILTIN_DEPENDENCY_NAMES.has(refName)) {
987
- return { deps: [] };
993
+ return { deps: [], unsupportedParameter: { index, name: param.name.getText(), type: paramType?.getText() ?? "(no type)" } };
988
994
  }
989
995
 
990
996
  // `Lazy<X>` is a deferred dependency on X: encode it with a marker prefix
@@ -1677,7 +1683,7 @@ function sourceLocation(node: ts.Node): string {
1677
1683
 
1678
1684
  function renderListModels(imports: readonly RequestModelImport[]): string {
1679
1685
  const lines: string[] = [];
1680
- lines.push("// This file is auto-generated by `bun run di:generate`.");
1686
+ lines.push("// This file is auto-generated by `bazis codegen`.");
1681
1687
  lines.push("// Do not edit manually.");
1682
1688
  lines.push("//");
1683
1689
  lines.push("// Target-local immutable descriptor; runtime publication is staged by runtime.ts.");
@@ -1717,7 +1723,7 @@ function renderRequestModels(
1717
1723
  hydration: RequestModelHydration,
1718
1724
  ): string {
1719
1725
  const lines: string[] = [];
1720
- lines.push("// This file is auto-generated by `bun run di:generate`.");
1726
+ lines.push("// This file is auto-generated by `bazis codegen`.");
1721
1727
  lines.push("// Do not edit manually.");
1722
1728
  lines.push("//");
1723
1729
  lines.push("// Registers convention body models (class-typed action params) so they are");
@@ -1780,7 +1786,7 @@ function renderOpenApiMetadata(
1780
1786
  }
1781
1787
 
1782
1788
  const lines: string[] = [];
1783
- lines.push("// This file is auto-generated by `bun run di:generate`.");
1789
+ lines.push("// This file is auto-generated by `bazis codegen`.");
1784
1790
  lines.push("// Do not edit manually.");
1785
1791
  lines.push("");
1786
1792
  lines.push(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.97.2",
3
+ "version": "0.97.4",
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",