@forwardimpact/libcodegen 0.1.36 → 0.1.37
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 +79 -40
- 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),
|
|
@@ -175,11 +220,12 @@ async function executeGeneration(codegens, sourcePath, flags) {
|
|
|
175
220
|
}
|
|
176
221
|
|
|
177
222
|
/**
|
|
178
|
-
*
|
|
223
|
+
* Run code generation pipeline
|
|
224
|
+
* @param {string[]} protoDirs - Discovered proto directories
|
|
179
225
|
* @param {string} projectRoot - Project root directory path
|
|
180
226
|
* @param {object} finder - Finder instance for path management
|
|
181
227
|
*/
|
|
182
|
-
async function runCodegen(projectRoot, finder) {
|
|
228
|
+
async function runCodegen(protoDirs, projectRoot, finder) {
|
|
183
229
|
const parsedFlags = parseFlags();
|
|
184
230
|
|
|
185
231
|
if (!parsedFlags.hasGenerationFlags()) {
|
|
@@ -193,36 +239,20 @@ async function runCodegen(projectRoot, finder) {
|
|
|
193
239
|
|
|
194
240
|
await generatedStorage.ensureBucket();
|
|
195
241
|
|
|
196
|
-
const codegens = createCodegen(
|
|
242
|
+
const codegens = createCodegen(
|
|
243
|
+
protoDirs,
|
|
244
|
+
projectRoot,
|
|
245
|
+
path,
|
|
246
|
+
mustache,
|
|
247
|
+
protoLoader,
|
|
248
|
+
fs,
|
|
249
|
+
);
|
|
197
250
|
await executeGeneration(codegens, sourcePath, parsedFlags);
|
|
198
251
|
|
|
199
252
|
await finder.createPackageSymlinks(sourcePath);
|
|
200
253
|
await createBundle(sourcePath);
|
|
201
254
|
}
|
|
202
255
|
|
|
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");
|
|
224
|
-
}
|
|
225
|
-
|
|
226
256
|
/**
|
|
227
257
|
* CLI entry point
|
|
228
258
|
*/
|
|
@@ -230,9 +260,18 @@ async function main() {
|
|
|
230
260
|
try {
|
|
231
261
|
const logger = new Logger("codegen");
|
|
232
262
|
const finder = new Finder(fsAsync, logger, process);
|
|
233
|
-
const projectRoot =
|
|
263
|
+
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
264
|
+
|
|
265
|
+
const protoDirs = discoverProtoDirs(projectRoot);
|
|
266
|
+
if (protoDirs.length === 0) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
"No proto directories found. Ensure @forwardimpact packages " +
|
|
269
|
+
"with proto/ directories are installed, or add proto files " +
|
|
270
|
+
"to your project's proto/ directory.",
|
|
271
|
+
);
|
|
272
|
+
}
|
|
234
273
|
|
|
235
|
-
await runCodegen(projectRoot, finder);
|
|
274
|
+
await runCodegen(protoDirs, projectRoot, finder);
|
|
236
275
|
} catch (err) {
|
|
237
276
|
process.stderr.write(`Error: ${err.message}\n`);
|
|
238
277
|
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.37",
|
|
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
|
}
|