@forwardimpact/libcodegen 0.1.36 → 0.1.39
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/base.js +40 -16
- package/bin/fit-codegen.js +147 -39
- package/package.json +8 -5
- package/types.js +12 -1
package/base.js
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
* Implements dependency injection pattern with explicit validation
|
|
7
7
|
*/
|
|
8
8
|
export class CodegenBase {
|
|
9
|
+
#protoDirs;
|
|
9
10
|
#projectRoot;
|
|
10
11
|
#path;
|
|
11
12
|
#mustache;
|
|
@@ -14,19 +15,24 @@ export class CodegenBase {
|
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Creates a new codegen base instance with dependency injection
|
|
17
|
-
* @param {string}
|
|
18
|
+
* @param {string[]} protoDirs - Array of proto directory paths to scan
|
|
19
|
+
* @param {string} projectRoot - Project root directory path (for tools/ discovery)
|
|
18
20
|
* @param {object} path - Path module for file operations
|
|
19
21
|
* @param {object} mustache - Mustache template rendering module
|
|
20
22
|
* @param {object} protoLoader - Protocol buffer loader module
|
|
21
23
|
* @param {object} fs - File system module (sync operations only)
|
|
22
24
|
*/
|
|
23
|
-
constructor(projectRoot, path, mustache, protoLoader, fs) {
|
|
25
|
+
constructor(protoDirs, projectRoot, path, mustache, protoLoader, fs) {
|
|
26
|
+
if (!protoDirs || !Array.isArray(protoDirs) || protoDirs.length === 0) {
|
|
27
|
+
throw new Error("protoDirs must be a non-empty array");
|
|
28
|
+
}
|
|
24
29
|
if (!projectRoot) throw new Error("projectRoot is required");
|
|
25
30
|
if (!path) throw new Error("path module is required");
|
|
26
31
|
if (!mustache) throw new Error("mustache module is required");
|
|
27
32
|
if (!protoLoader) throw new Error("protoLoader module is required");
|
|
28
33
|
if (!fs) throw new Error("fs module is required");
|
|
29
34
|
|
|
35
|
+
this.#protoDirs = protoDirs;
|
|
30
36
|
this.#projectRoot = projectRoot;
|
|
31
37
|
this.#path = path;
|
|
32
38
|
this.#mustache = mustache;
|
|
@@ -35,36 +41,46 @@ export class CodegenBase {
|
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/**
|
|
38
|
-
* Collect all proto files from
|
|
44
|
+
* Collect all proto files from discovered proto directories and tools directory.
|
|
45
|
+
* Deduplicates by filename (last occurrence wins), maintains common.proto-first
|
|
46
|
+
* ordering, and includes all discovered directories as proto include paths.
|
|
39
47
|
* @param {object} opts - Collection options
|
|
40
48
|
* @param {boolean} opts.includeTools - Whether to include tool proto files
|
|
41
49
|
* @returns {string[]} Array of absolute proto file paths
|
|
42
50
|
*/
|
|
43
51
|
collectProtoFiles(opts = {}) {
|
|
44
52
|
const { includeTools = true } = opts;
|
|
45
|
-
const protoDir = this.#path.join(this.#projectRoot, "proto");
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.
|
|
54
|
+
// Collect from all proto directories, dedup by filename (last wins)
|
|
55
|
+
const byName = new Map();
|
|
56
|
+
for (const dir of this.#protoDirs) {
|
|
57
|
+
if (!this.#fs.existsSync(dir)) continue;
|
|
58
|
+
for (const file of this.#fs.readdirSync(dir)) {
|
|
59
|
+
if (file.endsWith(".proto")) {
|
|
60
|
+
byName.set(file, this.#path.join(dir, file));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
51
64
|
|
|
52
|
-
|
|
65
|
+
// Sort and ensure common.proto comes first
|
|
66
|
+
const sorted = Array.from(byName.keys()).sort();
|
|
67
|
+
const ordered = sorted.includes("common.proto")
|
|
53
68
|
? [
|
|
54
|
-
|
|
55
|
-
...
|
|
69
|
+
byName.get("common.proto"),
|
|
70
|
+
...sorted
|
|
56
71
|
.filter((f) => f !== "common.proto")
|
|
57
|
-
.map((f) =>
|
|
72
|
+
.map((f) => byName.get(f)),
|
|
58
73
|
]
|
|
59
|
-
:
|
|
74
|
+
: sorted.map((f) => byName.get(f));
|
|
60
75
|
|
|
61
76
|
if (includeTools) {
|
|
62
77
|
try {
|
|
78
|
+
const toolsDir = this.#path.join(this.#projectRoot, "tools");
|
|
63
79
|
ordered.push(
|
|
64
80
|
...this.#fs
|
|
65
|
-
.readdirSync(
|
|
81
|
+
.readdirSync(toolsDir)
|
|
66
82
|
.filter((f) => f.endsWith(".proto"))
|
|
67
|
-
.map((f) => this.#path.join(
|
|
83
|
+
.map((f) => this.#path.join(toolsDir, f)),
|
|
68
84
|
);
|
|
69
85
|
} catch {
|
|
70
86
|
// tools directory may not exist; ignore
|
|
@@ -74,6 +90,14 @@ export class CodegenBase {
|
|
|
74
90
|
return ordered;
|
|
75
91
|
}
|
|
76
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Get all proto include directories for cross-file import resolution
|
|
95
|
+
* @returns {string[]} Array of absolute paths to proto directories
|
|
96
|
+
*/
|
|
97
|
+
get includeDirs() {
|
|
98
|
+
return this.#protoDirs.filter((dir) => this.#fs.existsSync(dir));
|
|
99
|
+
}
|
|
100
|
+
|
|
77
101
|
/**
|
|
78
102
|
* Load mustache template for given kind
|
|
79
103
|
* @param {"service"|"client"|"exports"|"definition"|"definitions-exports"|"services-exports"} kind - Template kind
|
|
@@ -145,7 +169,7 @@ export class CodegenBase {
|
|
|
145
169
|
*/
|
|
146
170
|
parseProtoFile(protoPath) {
|
|
147
171
|
const def = this.#protoLoader.loadSync(protoPath, {
|
|
148
|
-
includeDirs:
|
|
172
|
+
includeDirs: this.includeDirs,
|
|
149
173
|
keepCase: true,
|
|
150
174
|
});
|
|
151
175
|
|
package/bin/fit-codegen.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import fsAsync from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
6
|
import { execFileSync } from "node:child_process";
|
|
8
7
|
import { parseArgs } from "node:util";
|
|
9
8
|
|
|
@@ -20,9 +19,6 @@ import {
|
|
|
20
19
|
} from "@forwardimpact/libcodegen";
|
|
21
20
|
import { createStorage } from "@forwardimpact/libstorage";
|
|
22
21
|
|
|
23
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
-
const __dirname = path.dirname(__filename);
|
|
25
|
-
|
|
26
22
|
/**
|
|
27
23
|
* Create tar.gz bundle of all directories inside sourcePath
|
|
28
24
|
* @param {string} sourcePath - Path containing directories to bundle
|
|
@@ -63,11 +59,11 @@ function printUsage() {
|
|
|
63
59
|
process.stdout.write(
|
|
64
60
|
[
|
|
65
61
|
"Usage:",
|
|
66
|
-
`
|
|
67
|
-
`
|
|
68
|
-
`
|
|
69
|
-
`
|
|
70
|
-
`
|
|
62
|
+
` npx fit-codegen --all # Generate all code`,
|
|
63
|
+
` npx fit-codegen --type # Generate protobuf types only`,
|
|
64
|
+
` npx fit-codegen --service # Generate service bases only`,
|
|
65
|
+
` npx fit-codegen --client # Generate clients only`,
|
|
66
|
+
` npx fit-codegen --definition # Generate service definitions only`,
|
|
71
67
|
].join("\n") + "\n",
|
|
72
68
|
);
|
|
73
69
|
}
|
|
@@ -117,16 +113,65 @@ function parseFlags() {
|
|
|
117
113
|
}
|
|
118
114
|
|
|
119
115
|
/**
|
|
120
|
-
*
|
|
116
|
+
* Discover proto directories from installed @forwardimpact/* packages
|
|
117
|
+
* and the project root. Scans node_modules for packages that include
|
|
118
|
+
* a proto/ subdirectory, plus the project's own proto/ if present.
|
|
121
119
|
* @param {string} projectRoot - Project root directory path
|
|
120
|
+
* @returns {string[]} Array of absolute paths to proto directories
|
|
121
|
+
*/
|
|
122
|
+
function discoverProtoDirs(projectRoot) {
|
|
123
|
+
const protoDirs = [];
|
|
124
|
+
|
|
125
|
+
// Scan node_modules/@forwardimpact/*/proto/ for package-owned protos
|
|
126
|
+
// Use fs.statSync to follow workspace symlinks (entry.isDirectory() is false for symlinks)
|
|
127
|
+
const scopeDir = path.join(projectRoot, "node_modules", "@forwardimpact");
|
|
128
|
+
if (fs.existsSync(scopeDir)) {
|
|
129
|
+
for (const name of fs.readdirSync(scopeDir)) {
|
|
130
|
+
const protoDir = path.join(scopeDir, name, "proto");
|
|
131
|
+
if (fs.existsSync(protoDir) && fs.statSync(protoDir).isDirectory()) {
|
|
132
|
+
protoDirs.push(fs.realpathSync(protoDir));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Also check workspace-linked packages (monorepo with symlinked node_modules)
|
|
138
|
+
// The loop above handles this since workspace packages appear in node_modules
|
|
139
|
+
|
|
140
|
+
// Include the project's own proto/ directory for custom protos
|
|
141
|
+
const projectProtoDir = path.join(projectRoot, "proto");
|
|
142
|
+
if (fs.existsSync(projectProtoDir)) {
|
|
143
|
+
protoDirs.push(projectProtoDir);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return protoDirs;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create codegen instances
|
|
151
|
+
* @param {string[]} protoDirs - Array of proto directory paths
|
|
152
|
+
* @param {string} projectRoot - Project root for tools/ discovery
|
|
122
153
|
* @param {object} path - Path module
|
|
123
154
|
* @param {object} mustache - Mustache module
|
|
124
155
|
* @param {object} protoLoader - Proto loader module
|
|
125
156
|
* @param {object} fs - File system module
|
|
126
157
|
* @returns {object} Codegen instances
|
|
127
158
|
*/
|
|
128
|
-
function createCodegen(
|
|
129
|
-
|
|
159
|
+
function createCodegen(
|
|
160
|
+
protoDirs,
|
|
161
|
+
projectRoot,
|
|
162
|
+
path,
|
|
163
|
+
mustache,
|
|
164
|
+
protoLoader,
|
|
165
|
+
fs,
|
|
166
|
+
) {
|
|
167
|
+
const base = new CodegenBase(
|
|
168
|
+
protoDirs,
|
|
169
|
+
projectRoot,
|
|
170
|
+
path,
|
|
171
|
+
mustache,
|
|
172
|
+
protoLoader,
|
|
173
|
+
fs,
|
|
174
|
+
);
|
|
130
175
|
return {
|
|
131
176
|
types: new CodegenTypes(base),
|
|
132
177
|
services: new CodegenServices(base),
|
|
@@ -134,6 +179,63 @@ function createCodegen(projectRoot, path, mustache, protoLoader, fs) {
|
|
|
134
179
|
};
|
|
135
180
|
}
|
|
136
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Count files recursively in a directory
|
|
184
|
+
* @param {string} dirPath - Directory to count files in
|
|
185
|
+
* @returns {number} Total file count
|
|
186
|
+
*/
|
|
187
|
+
function countFiles(dirPath) {
|
|
188
|
+
let count = 0;
|
|
189
|
+
if (!fs.existsSync(dirPath)) return count;
|
|
190
|
+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
191
|
+
if (entry.isDirectory()) {
|
|
192
|
+
count += countFiles(path.join(dirPath, entry.name));
|
|
193
|
+
} else if (!entry.name.endsWith(".tar.gz")) {
|
|
194
|
+
count++;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return count;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Print a summary of generated code
|
|
202
|
+
* @param {string} sourcePath - Path to generated directory
|
|
203
|
+
* @param {object} flags - Parsed generation flags
|
|
204
|
+
*/
|
|
205
|
+
function printSummary(sourcePath, flags) {
|
|
206
|
+
const totalFiles = countFiles(sourcePath);
|
|
207
|
+
const relPath = path.relative(process.cwd(), sourcePath);
|
|
208
|
+
const lines = [`Generated ${totalFiles} files in ./${relPath}/`];
|
|
209
|
+
|
|
210
|
+
const dirLabels = {
|
|
211
|
+
types: "Protocol Buffer types",
|
|
212
|
+
proto: "Proto source files",
|
|
213
|
+
services: "Service bases and clients",
|
|
214
|
+
definitions: "Service definitions",
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (fs.existsSync(sourcePath)) {
|
|
218
|
+
const dirs = fs
|
|
219
|
+
.readdirSync(sourcePath, { withFileTypes: true })
|
|
220
|
+
.filter((e) => e.isDirectory());
|
|
221
|
+
|
|
222
|
+
for (const dir of dirs) {
|
|
223
|
+
const label = dirLabels[dir.name];
|
|
224
|
+
if (label) lines.push(` ${dir.name}/ — ${label}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const generated = [
|
|
229
|
+
flags.doTypes && "types",
|
|
230
|
+
flags.doServices && "services",
|
|
231
|
+
flags.doClients && "clients",
|
|
232
|
+
flags.doDefinitions && "definitions",
|
|
233
|
+
].filter(Boolean);
|
|
234
|
+
lines.push(`\nCode generation complete (${generated.join(", ")}).`);
|
|
235
|
+
|
|
236
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
237
|
+
}
|
|
238
|
+
|
|
137
239
|
/**
|
|
138
240
|
* Execute code generation tasks
|
|
139
241
|
* @param {object} codegens - Codegen instances
|
|
@@ -175,11 +277,12 @@ async function executeGeneration(codegens, sourcePath, flags) {
|
|
|
175
277
|
}
|
|
176
278
|
|
|
177
279
|
/**
|
|
178
|
-
*
|
|
280
|
+
* Run code generation pipeline
|
|
281
|
+
* @param {string[]} protoDirs - Discovered proto directories
|
|
179
282
|
* @param {string} projectRoot - Project root directory path
|
|
180
283
|
* @param {object} finder - Finder instance for path management
|
|
181
284
|
*/
|
|
182
|
-
async function runCodegen(projectRoot, finder) {
|
|
285
|
+
async function runCodegen(protoDirs, projectRoot, finder) {
|
|
183
286
|
const parsedFlags = parseFlags();
|
|
184
287
|
|
|
185
288
|
if (!parsedFlags.hasGenerationFlags()) {
|
|
@@ -193,34 +296,30 @@ async function runCodegen(projectRoot, finder) {
|
|
|
193
296
|
|
|
194
297
|
await generatedStorage.ensureBucket();
|
|
195
298
|
|
|
196
|
-
|
|
299
|
+
// Write package.json with "type": "module" so Node.js treats generated
|
|
300
|
+
// ES module files correctly and avoids MODULE_TYPELESS_PACKAGE_JSON warnings.
|
|
301
|
+
const generatedPkgPath = path.join(sourcePath, "package.json");
|
|
302
|
+
if (!fs.existsSync(generatedPkgPath)) {
|
|
303
|
+
fs.writeFileSync(
|
|
304
|
+
generatedPkgPath,
|
|
305
|
+
JSON.stringify({ type: "module" }, null, 2) + "\n",
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const codegens = createCodegen(
|
|
310
|
+
protoDirs,
|
|
311
|
+
projectRoot,
|
|
312
|
+
path,
|
|
313
|
+
mustache,
|
|
314
|
+
protoLoader,
|
|
315
|
+
fs,
|
|
316
|
+
);
|
|
197
317
|
await executeGeneration(codegens, sourcePath, parsedFlags);
|
|
198
318
|
|
|
199
319
|
await finder.createPackageSymlinks(sourcePath);
|
|
200
320
|
await createBundle(sourcePath);
|
|
201
|
-
}
|
|
202
321
|
|
|
203
|
-
|
|
204
|
-
* Find the monorepo root directory (the one with workspaces)
|
|
205
|
-
* @param {string} startPath - Starting directory path
|
|
206
|
-
* @returns {string} Project root directory path
|
|
207
|
-
*/
|
|
208
|
-
function findMonorepoRoot(startPath) {
|
|
209
|
-
let current = startPath;
|
|
210
|
-
for (let depth = 0; depth < 10; depth++) {
|
|
211
|
-
const packageJsonPath = path.join(current, "package.json");
|
|
212
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
213
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
214
|
-
// Check if this package.json has workspaces (indicates monorepo root)
|
|
215
|
-
if (packageJson.workspaces) {
|
|
216
|
-
return current;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
const parent = path.dirname(current);
|
|
220
|
-
if (parent === current) break;
|
|
221
|
-
current = parent;
|
|
222
|
-
}
|
|
223
|
-
throw new Error("Could not find monorepo root");
|
|
322
|
+
printSummary(sourcePath, parsedFlags);
|
|
224
323
|
}
|
|
225
324
|
|
|
226
325
|
/**
|
|
@@ -230,9 +329,18 @@ async function main() {
|
|
|
230
329
|
try {
|
|
231
330
|
const logger = new Logger("codegen");
|
|
232
331
|
const finder = new Finder(fsAsync, logger, process);
|
|
233
|
-
const projectRoot =
|
|
332
|
+
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
333
|
+
|
|
334
|
+
const protoDirs = discoverProtoDirs(projectRoot);
|
|
335
|
+
if (protoDirs.length === 0) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
"No proto directories found. Ensure @forwardimpact packages " +
|
|
338
|
+
"with proto/ directories are installed, or add proto files " +
|
|
339
|
+
"to your project's proto/ directory.",
|
|
340
|
+
);
|
|
341
|
+
}
|
|
234
342
|
|
|
235
|
-
await runCodegen(projectRoot, finder);
|
|
343
|
+
await runCodegen(protoDirs, projectRoot, finder);
|
|
236
344
|
} catch (err) {
|
|
237
345
|
process.stderr.write(`Error: ${err.message}\n`);
|
|
238
346
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/libcodegen",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
4
4
|
"description": "Protocol Buffer code generation utilities for Guide",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "D. Olsson <hi@senzilla.io>",
|
|
@@ -17,14 +17,17 @@
|
|
|
17
17
|
"test": "bun run node --test test/*.test.js"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"@forwardimpact/libstorage": "^0.1.53",
|
|
21
|
+
"@forwardimpact/libtelemetry": "^0.1.22",
|
|
22
|
+
"@forwardimpact/libutil": "^0.1.64",
|
|
20
23
|
"@grpc/proto-loader": "^0.8.0",
|
|
21
|
-
"mustache": "^4.2.0"
|
|
22
|
-
},
|
|
23
|
-
"devDependencies": {
|
|
24
|
-
"@forwardimpact/libharness": "^0.1.5",
|
|
24
|
+
"mustache": "^4.2.0",
|
|
25
25
|
"protobufjs": "^7.5.4",
|
|
26
26
|
"protobufjs-cli": "^2.0.0"
|
|
27
27
|
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@forwardimpact/libharness": "^0.1.5"
|
|
30
|
+
},
|
|
28
31
|
"publishConfig": {
|
|
29
32
|
"access": "public"
|
|
30
33
|
}
|
package/types.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Handles JavaScript type generation from protobuf files
|
|
3
5
|
* Specializes in Protocol Buffer to JavaScript type conversion
|
|
@@ -70,7 +72,15 @@ export class CodegenTypes {
|
|
|
70
72
|
* @returns {Promise<void>}
|
|
71
73
|
*/
|
|
72
74
|
async generateJavaScriptTypes(protoFiles, outFile) {
|
|
75
|
+
// Resolve pbjs binary from protobufjs-cli package — works in both Bun and Node
|
|
76
|
+
const require = createRequire(import.meta.url);
|
|
77
|
+
const pbjsBin = require.resolve("protobufjs-cli/bin/pbjs");
|
|
78
|
+
|
|
79
|
+
// Pass all proto directories as include paths so cross-file imports resolve
|
|
80
|
+
const includePaths = this.#base.includeDirs.flatMap((dir) => ["-p", dir]);
|
|
81
|
+
|
|
73
82
|
const args = [
|
|
83
|
+
pbjsBin,
|
|
74
84
|
"-t",
|
|
75
85
|
"static-module",
|
|
76
86
|
"-w",
|
|
@@ -80,12 +90,13 @@ export class CodegenTypes {
|
|
|
80
90
|
"--no-service",
|
|
81
91
|
"--force-message",
|
|
82
92
|
"--keep-case",
|
|
93
|
+
...includePaths,
|
|
83
94
|
"-o",
|
|
84
95
|
outFile,
|
|
85
96
|
...protoFiles,
|
|
86
97
|
];
|
|
87
98
|
|
|
88
|
-
await this.#base.run(
|
|
99
|
+
await this.#base.run(process.execPath, args, {
|
|
89
100
|
cwd: this.#base.projectRoot,
|
|
90
101
|
});
|
|
91
102
|
}
|