bazis 0.97.3 → 0.97.5

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 CHANGED
@@ -15,9 +15,10 @@ executable.
15
15
  (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), and the runtime uses Bun
16
16
  APIs throughout: `Bun.serve`, `Bun.SQL`, the built-in Redis client,
17
17
  `bun:test`, `bun build --compile`.
18
- - TypeScript 5.9 or newer as a peer dependency: the code generator reads
19
- your sources through the TypeScript compiler API. Nothing is compiled to
20
- JavaScript.
18
+ - TypeScript 5.9, 6 or 7 as a peer dependency: the code generator reads
19
+ your sources through the TypeScript compiler API. With TypeScript 7, which
20
+ has no such API, also install `@typescript/typescript6`. Nothing is
21
+ compiled to JavaScript.
21
22
  - PostgreSQL for the ORM.
22
23
  - Qualified platforms: macOS arm64, Linux x64 and arm64.
23
24
 
@@ -40,9 +41,20 @@ bunx bazis dev # GET http://127.0.0.1:3000/health
40
41
 
41
42
  ```sh
42
43
  bun add bazis # or a fixed version: bun add bazis@0.96.2
43
- bun add -d typescript@^6 @types/bun # TypeScript 7 is not supported yet
44
+ bun add -d typescript@^6 @types/bun
44
45
  ```
45
46
 
47
+ TypeScript 7 also works. It ships no compiler API, which the code generator
48
+ needs, so add Microsoft's TypeScript 6 API package next to it:
49
+
50
+ ```sh
51
+ bun add -d typescript@^7 @typescript/typescript6 @types/bun
52
+ ```
53
+
54
+ The project keeps type-checking with TypeScript 7; only codegen reads the
55
+ sources through the TypeScript 6 API. Without `@typescript/typescript6`
56
+ codegen stops with `BAZIS_TYPESCRIPT_API_MISSING` and this install command.
57
+
46
58
  Then take `tsconfig.json`, `bazis.config.json` and `src/index.ts` from a
47
59
  project made by `bazis new` as the starting point, and run `bunx bazis codegen`.
48
60
 
package/cli/codegen.ts CHANGED
@@ -11,7 +11,10 @@ export async function runCodegen(cwd: string, target?: string): Promise<number>
11
11
  const config = await configFile.json();
12
12
  if (target !== "all" && !Object.hasOwn(config.targets ?? {}, target)) throw new Error(`Unknown codegen target: ${target}`);
13
13
  }
14
- const args = [await resolveBun(cwd), resolveGenerator(cwd), ...(target === undefined ? [] : ["--target", target])];
14
+ const generator = resolveGenerator(cwd);
15
+ // TypeScript 7 has no compiler API: the preload switches it to @typescript/typescript6.
16
+ const preload = path.join(path.dirname(generator), "typescriptApiPreload.ts");
17
+ const args = [await resolveBun(cwd), ...(existsSync(preload) ? ["--preload", preload] : []), generator, ...(target === undefined ? [] : ["--target", target])];
15
18
  return await Bun.spawn(args, { cwd, stdout: "inherit", stderr: "inherit" }).exited;
16
19
  }
17
20
 
package/cli/main.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { generateModule, generateModulePack } from "./generateModule";
2
+ import { useTypeScriptCompilerApi } from "../core/scripts/typescriptApi";
3
3
  import { parseCliArgs } from "./parseCli";
4
4
  import { runCodegen } from "./codegen";
5
5
  import { generateProject } from "./generateProject";
@@ -100,6 +100,10 @@ export async function runCli(argv: readonly string[], runtime: CliRuntime = defa
100
100
  name: args.name, modulesRoot: args.modulesRoot, appModulePath: args.appModule,
101
101
  register: args.register, force: args.force, dryRun: args.dryRun,
102
102
  };
103
+ // Module registration parses the host module with the TypeScript API: load it
104
+ // only after switching TypeScript 7 to @typescript/typescript6.
105
+ useTypeScriptCompilerApi(process.cwd());
106
+ const { generateModule, generateModulePack } = await import("./generateModule");
103
107
  const result = args.generator === "pack"
104
108
  ? await generateModulePack({ ...options, parts: args.parts })
105
109
  : await generateModule({ ...options, profile: args.profile });
@@ -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
  }
@@ -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.3" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.97.5" }, 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;
@@ -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>();
@@ -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
@@ -0,0 +1,61 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { plugin } from "bun";
4
+
5
+ /**
6
+ * bazis codegen uses the TypeScript compiler API (`createProgram`, the type
7
+ * checker). TypeScript 7 no longer ships it: `import ts from "typescript"`
8
+ * gives only the version. Microsoft publishes the TypeScript 6 API as
9
+ * `@typescript/typescript6`. When the `typescript` package seen from `fromDir`
10
+ * is 7 or newer, this makes `import ts from "typescript"` in the current
11
+ * process load that package instead. The project keeps TypeScript 7 as its
12
+ * compiler (`tsc`).
13
+ *
14
+ * Must run before anything imports "typescript": as a `--preload` of the
15
+ * codegen process, or in the CLI before its first dynamic import that needs it.
16
+ * Does nothing when `typescript` cannot be resolved (for example inside the
17
+ * compiled CLI binary, which bundles its own compiler API).
18
+ */
19
+ export function useTypeScriptCompilerApi(fromDir: string): void {
20
+ let entry: string;
21
+ try {
22
+ entry = Bun.resolveSync("typescript", fromDir);
23
+ } catch {
24
+ return;
25
+ }
26
+ const major = Number.parseInt(packageVersion(entry, "typescript")?.split(".")[0] ?? "", 10);
27
+ if (!Number.isFinite(major) || major < 7) return;
28
+
29
+ let api: string;
30
+ try {
31
+ api = Bun.resolveSync("@typescript/typescript6", fromDir);
32
+ } catch {
33
+ throw new Error(
34
+ `BAZIS_TYPESCRIPT_API_MISSING: TypeScript ${major} has no compiler API, and bazis codegen needs one. ` +
35
+ "Install the TypeScript 6 API next to it: bun add -d @typescript/typescript6",
36
+ );
37
+ }
38
+ const exact = new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`);
39
+ plugin({
40
+ name: "bazis-typescript6-api",
41
+ setup(build) {
42
+ build.onLoad({ filter: exact }, () => ({
43
+ contents: `import ts from ${JSON.stringify(api)};\nexport default ts;\n`,
44
+ loader: "js",
45
+ }));
46
+ },
47
+ });
48
+ }
49
+
50
+ /** Version from the package.json of the package that contains `file`. */
51
+ function packageVersion(file: string, name: string): string | undefined {
52
+ for (let dir = path.dirname(file); dir !== path.dirname(dir); dir = path.dirname(dir)) {
53
+ try {
54
+ const manifest = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8")) as { name?: string; version?: string };
55
+ if (manifest.name === name) return manifest.version;
56
+ } catch {
57
+ // No package.json at this level: keep walking up.
58
+ }
59
+ }
60
+ return undefined;
61
+ }
@@ -0,0 +1,9 @@
1
+ // Preloaded into the codegen process (`bun --preload`): see useTypeScriptCompilerApi.
2
+ import { useTypeScriptCompilerApi } from "./typescriptApi";
3
+
4
+ try {
5
+ useTypeScriptCompilerApi(import.meta.dir);
6
+ } catch (error) {
7
+ console.error(`[bazis] ${error instanceof Error ? error.message : String(error)}`);
8
+ process.exit(1);
9
+ }
@@ -653,8 +653,11 @@ export class OwnedStoreCatalogReader {
653
653
  readonly OwnedCatalogArrayTypeV1[]
654
654
  ]> {
655
655
  const relationOids = relations.filter(relation => relation.rowTypeOid !== null).map(relation => relation.oid);
656
- if (!relationOids.length)
657
- return Object.freeze([Object.freeze([]), Object.freeze([])]);
656
+ if (!relationOids.length) {
657
+ // An explicit tuple: TypeScript 7 infers Object.freeze([a, b]) as an array.
658
+ const empty: readonly [readonly OwnedCatalogRowTypeV1[], readonly OwnedCatalogArrayTypeV1[]] = [Object.freeze([]), Object.freeze([])];
659
+ return Object.freeze(empty);
660
+ }
658
661
  const rowTypes: OwnedCatalogRowTypeV1[] = [], arrayTypes: OwnedCatalogArrayTypeV1[] = [];
659
662
  for (let offset = 0; offset < relationOids.length; offset += 1024) {
660
663
  const batch = relationOids.slice(offset, offset + 1024), parameter = this.encodePgOidArrayParameter(batch), issuedLimit = Math.min(batch.length + 1, budget.remaining + 1);
@@ -693,7 +696,8 @@ export class OwnedStoreCatalogReader {
693
696
  this.source.drift();
694
697
  }
695
698
  }
696
- return Object.freeze([Object.freeze(rowTypes), Object.freeze(arrayTypes)]);
699
+ const types: readonly [readonly OwnedCatalogRowTypeV1[], readonly OwnedCatalogArrayTypeV1[]] = [Object.freeze(rowTypes), Object.freeze(arrayTypes)];
700
+ return Object.freeze(types);
697
701
  }
698
702
  private async readColumns(relations: readonly OwnedCatalogRelationV1[], operation: "registry-read" | "catalog-read", budget: {
699
703
  remaining: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.97.3",
3
+ "version": "0.97.5",
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",
@@ -71,10 +71,16 @@
71
71
  "bun": ">=1.4.0"
72
72
  },
73
73
  "peerDependencies": {
74
- "typescript": "^5.9.3 || ^6.0.0"
74
+ "typescript": "^5.9.3 || ^6.0.0 || ^7.0.0",
75
+ "@typescript/typescript6": "^6.0.0"
75
76
  },
76
77
  "publishConfig": {
77
78
  "access": "public",
78
79
  "provenance": true
80
+ },
81
+ "peerDependenciesMeta": {
82
+ "@typescript/typescript6": {
83
+ "optional": true
84
+ }
79
85
  }
80
86
  }