@forwardimpact/libcodegen 0.1.59 → 0.1.61

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.
@@ -2,8 +2,15 @@
2
2
 
3
3
  import "@forwardimpact/libpreflight/node22";
4
4
 
5
- import fs, { readFileSync } from "node:fs";
6
- import fsAsync from "node:fs/promises";
5
+ // Bind protobufjs's `util.Long` before any other import evaluates. The
6
+ // `@grpc/proto-loader` import below pulls in protobufjs's descriptor extension,
7
+ // which calls `Root.fromJSON(...).resolveAll()` at module-evaluation time —
8
+ // resolving a 64-bit field default needs `util.Long`, which `bun --compile`
9
+ // leaves undefined (see long-init.js). This side-effect import must precede the
10
+ // proto-loader import so the binding is in place before that resolveAll runs.
11
+ import "../src/long-init.js";
12
+
13
+ import fs from "node:fs";
7
14
  import path from "node:path";
8
15
  import { execFileSync } from "node:child_process";
9
16
 
@@ -11,7 +18,7 @@ import protoLoader from "@grpc/proto-loader";
11
18
  import mustache from "mustache";
12
19
 
13
20
  import { createCli, SummaryRenderer } from "@forwardimpact/libcli";
14
- import { Finder } from "@forwardimpact/libutil";
21
+ import { createDefaultRuntime } from "@forwardimpact/libutil/runtime";
15
22
  import { Logger } from "@forwardimpact/libtelemetry";
16
23
  import {
17
24
  CodegenBase,
@@ -22,17 +29,8 @@ import {
22
29
  } from "@forwardimpact/libcodegen";
23
30
  import { createStorage } from "@forwardimpact/libstorage";
24
31
 
25
- // `bun build --compile` injects FIT_CODEGEN_VERSION via --define, eliminating
26
- // the readFileSync branch in the compiled binary (which would ENOENT against
27
- // the bunfs virtual mount). Source execution falls through to package.json.
28
- const VERSION =
29
- process.env.FIT_CODEGEN_VERSION ||
30
- JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))
31
- .version;
32
-
33
32
  const definition = {
34
33
  name: "fit-codegen",
35
- version: VERSION,
36
34
  description:
37
35
  "Generate protobuf types, service clients, and definitions from .proto files in installed @forwardimpact/* packages (node_modules/@forwardimpact/*/proto/) and an optional project-local proto/ directory.",
38
36
  globalOptions: {
@@ -62,7 +60,11 @@ const definition = {
62
60
  ],
63
61
  };
64
62
 
65
- const cli = createCli(definition);
63
+ const runtime = createDefaultRuntime();
64
+ const cli = createCli(definition, {
65
+ runtime,
66
+ packageJsonUrl: new URL("../package.json", import.meta.url),
67
+ });
66
68
 
67
69
  /**
68
70
  * Create tar.gz bundle of all directories inside sourcePath
@@ -110,6 +112,7 @@ function parseFlags() {
110
112
  const { values } = parsed;
111
113
  const doAll = values.all;
112
114
  return {
115
+ doAll,
113
116
  doTypes: doAll || values.type,
114
117
  doServices: doAll || values.service,
115
118
  doClients: doAll || values.client,
@@ -178,6 +181,7 @@ function createCodegen(
178
181
  mustache,
179
182
  protoLoader,
180
183
  fs,
184
+ runtime,
181
185
  ) {
182
186
  const base = new CodegenBase(
183
187
  protoDirs,
@@ -186,6 +190,7 @@ function createCodegen(
186
190
  mustache,
187
191
  protoLoader,
188
192
  fs,
193
+ runtime,
189
194
  );
190
195
  return {
191
196
  types: new CodegenTypes(base),
@@ -327,6 +332,17 @@ async function runCodegen(protoDirs, projectRoot, finder) {
327
332
 
328
333
  await generatedStorage.ensureBucket();
329
334
 
335
+ // Full regeneration (--all) clears the content directories first so that a
336
+ // renamed or removed proto leaves no orphaned per-proto artifacts. The
337
+ // services exports step scans the services/ directory, so a stale service
338
+ // dir would otherwise be re-exported and import types that no longer exist.
339
+ // Partial flags intentionally preserve sibling artifacts and are not cleaned.
340
+ if (parsedFlags.doAll) {
341
+ for (const dir of ["types", "services", "definitions", "proto"]) {
342
+ fs.rmSync(path.join(sourcePath, dir), { recursive: true, force: true });
343
+ }
344
+ }
345
+
330
346
  // Write package.json with "type": "module" so Node.js treats generated
331
347
  // ES module files correctly and avoids MODULE_TYPELESS_PACKAGE_JSON warnings.
332
348
  const generatedPkgPath = path.join(sourcePath, "package.json");
@@ -344,6 +360,7 @@ async function runCodegen(protoDirs, projectRoot, finder) {
344
360
  mustache,
345
361
  protoLoader,
346
362
  fs,
363
+ runtime,
347
364
  );
348
365
  await executeGeneration(codegens, sourcePath, parsedFlags);
349
366
 
@@ -358,8 +375,11 @@ async function runCodegen(protoDirs, projectRoot, finder) {
358
375
  */
359
376
  async function main() {
360
377
  try {
361
- const logger = new Logger("codegen");
362
- const finder = new Finder(fsAsync, logger, process);
378
+ const logger = new Logger("codegen", runtime);
379
+ // The shared runtime.finder carries a no-op logger; bind this CLI's logger
380
+ // so createPackageSymlinks (the one logging Finder consumer) keeps emitting
381
+ // symlink debug logs.
382
+ const finder = runtime.finder.withLogger(logger);
363
383
  const projectRoot = finder.findProjectRoot(process.cwd());
364
384
 
365
385
  const protoDirs = discoverProtoDirs(projectRoot);
@@ -373,7 +393,7 @@ async function main() {
373
393
 
374
394
  await runCodegen(protoDirs, projectRoot, finder);
375
395
  } catch (err) {
376
- const logger = new Logger("codegen");
396
+ const logger = new Logger("codegen", runtime);
377
397
  logger.exception("main", err);
378
398
  cli.error(err.message);
379
399
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libcodegen",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "description": "Protobuf code generation — keep types in sync with proto definitions without hand-writing.",
5
5
  "keywords": [
6
6
  "codegen",
package/src/base.js CHANGED
@@ -1,6 +1,6 @@
1
- import { execFile } from "node:child_process";
2
1
  import { fileURLToPath } from "node:url";
3
2
  import protobuf from "protobufjs";
3
+ import "./long-init.js";
4
4
 
5
5
  /** Convert camelCase to snake_case (protobufjs normalizes field names) */
6
6
  function camelToSnake(str) {
@@ -61,6 +61,7 @@ export class CodegenBase {
61
61
  #mustache;
62
62
  #protoLoader;
63
63
  #fs;
64
+ #subprocess;
64
65
 
65
66
  /**
66
67
  * Creates a new codegen base instance with dependency injection
@@ -70,8 +71,17 @@ export class CodegenBase {
70
71
  * @param {object} mustache - Mustache template rendering module
71
72
  * @param {object} protoLoader - Protocol buffer loader module
72
73
  * @param {object} fs - File system module (sync operations only)
74
+ * @param {import("@forwardimpact/libutil/runtime").Runtime} runtime - Injected runtime bag
73
75
  */
74
- constructor(protoDirs, projectRoot, path, mustache, protoLoader, fs) {
76
+ constructor(
77
+ protoDirs,
78
+ projectRoot,
79
+ path,
80
+ mustache,
81
+ protoLoader,
82
+ fs,
83
+ runtime,
84
+ ) {
75
85
  if (!protoDirs || !Array.isArray(protoDirs) || protoDirs.length === 0) {
76
86
  throw new Error("protoDirs must be a non-empty array");
77
87
  }
@@ -80,6 +90,7 @@ export class CodegenBase {
80
90
  if (!mustache) throw new Error("mustache module is required");
81
91
  if (!protoLoader) throw new Error("protoLoader module is required");
82
92
  if (!fs) throw new Error("fs module is required");
93
+ if (!runtime) throw new Error("runtime is required");
83
94
 
84
95
  this.#protoDirs = protoDirs;
85
96
  this.#projectRoot = projectRoot;
@@ -87,6 +98,7 @@ export class CodegenBase {
87
98
  this.#mustache = mustache;
88
99
  this.#protoLoader = protoLoader;
89
100
  this.#fs = fs;
101
+ this.#subprocess = runtime.subprocess;
90
102
  }
91
103
 
92
104
  /**
@@ -149,7 +161,7 @@ export class CodegenBase {
149
161
 
150
162
  /**
151
163
  * Load mustache template for given kind
152
- * @param {"service"|"client"|"exports"|"definition"|"definitions-exports"|"services-exports"} kind - Template kind
164
+ * @param {"service"|"client"|"definition"|"definitions-exports"|"services-exports"} kind - Template kind
153
165
  * @returns {string} Template content
154
166
  */
155
167
  loadTemplate(kind) {
@@ -182,22 +194,28 @@ export class CodegenBase {
182
194
  * Run a command with arguments and options
183
195
  * @param {string} cmd - Command to execute
184
196
  * @param {string[]} args - Command-line arguments
185
- * @param {object} [opts] - Child process options
197
+ * @param {object} [opts] - Subprocess options (e.g. cwd)
186
198
  * @returns {Promise<void>} Resolves when the command completes successfully
187
199
  */
188
- run(cmd, args, opts = {}) {
189
- return new Promise((resolvePromise, reject) => {
190
- const child = execFile(
191
- cmd,
192
- args,
193
- { stdio: "inherit", ...opts },
194
- (err) => {
195
- if (err) reject(err);
196
- else resolvePromise();
197
- },
198
- );
199
- child.on("error", reject);
200
+ async run(cmd, args, opts = {}) {
201
+ // `stdio: "inherit"` forwards to the underlying execFile/spawn so the
202
+ // child's stdout/stderr go straight to the parent's fds — preserving the
203
+ // exact pre-1370 behavior (origin/main also ran execFile with
204
+ // stdio:"inherit"), so `just codegen` shows protoc/pbjs progress live.
205
+ // With inherited stdio the buffered result is empty, so the error path
206
+ // below falls back to the exit code. Capture-mode callers override via
207
+ // `opts.stdio`.
208
+ const result = await this.#subprocess.run(cmd, args, {
209
+ stdio: "inherit",
210
+ ...opts,
200
211
  });
212
+ if (result.exitCode !== 0) {
213
+ const msg =
214
+ result.stderr?.trim() ||
215
+ result.stdout?.trim() ||
216
+ `exited with code ${result.exitCode}`;
217
+ throw new Error(`Command failed: ${cmd} ${args.join(" ")}\n${msg}`);
218
+ }
201
219
  }
202
220
 
203
221
  /**
@@ -0,0 +1,17 @@
1
+ // protobufjs populates `util.Long` through a dynamic `inquire("long")` that
2
+ // `bun build --compile` cannot resolve, leaving it undefined when a 64-bit
3
+ // field default is computed — crashing the compiled fit-codegen binary at
4
+ // startup (`util.Long.fromNumber is not a function`). Bind it explicitly.
5
+ //
6
+ // Imported for its side effect both by `bin/fit-codegen.js` (ahead of the
7
+ // `@grpc/proto-loader` import, whose descriptor extension runs `resolveAll()`
8
+ // at module-evaluation time) and by base.js (ahead of its own runtime
9
+ // `Root.resolveAll()`). ES module imports evaluate in source order, so an
10
+ // ordered side-effect import binds before the proto-loading code runs —
11
+ // inline binding statements in an entry body would not, since imports hoist
12
+ // above them.
13
+ import protobuf from "protobufjs";
14
+ import Long from "long";
15
+
16
+ protobuf.util.Long = Long;
17
+ protobuf.configure();
@@ -13,12 +13,13 @@ export class {{className}} extends Client {
13
13
  /**
14
14
  * Creates a new {{serviceName}} client instance
15
15
  * @param {object} config - Service configuration
16
+ * @param {import("@forwardimpact/libutil/runtime").Runtime} runtime - Injected runtime bag (required; the default auth factory reads SERVICE_SECRET from it)
16
17
  * @param {import("@forwardimpact/libtelemetry").Logger} [logger] - Optional logger instance
17
18
  * @param {import("@forwardimpact/libtelemetry").Tracer} [tracer] - Optional tracer for distributed tracing
18
19
  * @param {Function} [authFn] - Optional authentication function
19
20
  */
20
- constructor(config, logger = null, tracer = null, authFn = createAuth) {
21
- super(config, logger, tracer, createObserver, createGrpc, authFn);
21
+ constructor(config, runtime, logger = null, tracer = null, authFn = createAuth) {
22
+ super(config, runtime, logger, tracer, createObserver, createGrpc, authFn);
22
23
  }
23
24
 
24
25
  {{#methods}}