@forwardimpact/libcodegen 0.1.43 → 0.1.45
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 +13 -0
- package/bin/fit-codegen.js +17 -2
- package/package.json +1 -1
- package/src/base.js +95 -0
- package/src/index.js +1 -0
- package/src/metadata.js +50 -0
package/README.md
ADDED
package/bin/fit-codegen.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
CodegenTypes,
|
|
17
17
|
CodegenServices,
|
|
18
18
|
CodegenDefinitions,
|
|
19
|
+
CodegenMetadata,
|
|
19
20
|
} from "@forwardimpact/libcodegen";
|
|
20
21
|
import { createStorage } from "@forwardimpact/libstorage";
|
|
21
22
|
|
|
@@ -39,6 +40,10 @@ const definition = {
|
|
|
39
40
|
type: "boolean",
|
|
40
41
|
description: "Generate service definitions only",
|
|
41
42
|
},
|
|
43
|
+
metadata: {
|
|
44
|
+
type: "boolean",
|
|
45
|
+
description: "Generate field metadata only",
|
|
46
|
+
},
|
|
42
47
|
help: { type: "boolean", short: "h", description: "Show this help" },
|
|
43
48
|
version: { type: "boolean", description: "Show version" },
|
|
44
49
|
json: { type: "boolean", description: "Output help as JSON" },
|
|
@@ -102,9 +107,14 @@ function parseFlags() {
|
|
|
102
107
|
doServices: doAll || values.service,
|
|
103
108
|
doClients: doAll || values.client,
|
|
104
109
|
doDefinitions: doAll || values.definition,
|
|
110
|
+
doMetadata: doAll || values.metadata,
|
|
105
111
|
hasGenerationFlags() {
|
|
106
112
|
return (
|
|
107
|
-
this.doTypes ||
|
|
113
|
+
this.doTypes ||
|
|
114
|
+
this.doServices ||
|
|
115
|
+
this.doClients ||
|
|
116
|
+
this.doDefinitions ||
|
|
117
|
+
this.doMetadata
|
|
108
118
|
);
|
|
109
119
|
},
|
|
110
120
|
};
|
|
@@ -174,6 +184,7 @@ function createCodegen(
|
|
|
174
184
|
types: new CodegenTypes(base),
|
|
175
185
|
services: new CodegenServices(base),
|
|
176
186
|
definitions: new CodegenDefinitions(base),
|
|
187
|
+
metadata: new CodegenMetadata(base),
|
|
177
188
|
};
|
|
178
189
|
}
|
|
179
190
|
|
|
@@ -237,6 +248,7 @@ function printSummary(sourcePath, flags) {
|
|
|
237
248
|
flags.doServices && "services",
|
|
238
249
|
flags.doClients && "clients",
|
|
239
250
|
flags.doDefinitions && "definitions",
|
|
251
|
+
flags.doMetadata && "metadata",
|
|
240
252
|
].filter(Boolean);
|
|
241
253
|
process.stdout.write(
|
|
242
254
|
`\nCode generation complete (${generated.join(", ")}).\n`,
|
|
@@ -265,6 +277,9 @@ async function executeGeneration(codegens, sourcePath, flags) {
|
|
|
265
277
|
if (flags.doDefinitions) {
|
|
266
278
|
tasks.push(codegens.definitions.run(sourcePath));
|
|
267
279
|
}
|
|
280
|
+
if (flags.doMetadata) {
|
|
281
|
+
tasks.push(codegens.metadata.run(sourcePath));
|
|
282
|
+
}
|
|
268
283
|
|
|
269
284
|
await Promise.all(tasks);
|
|
270
285
|
|
|
@@ -294,7 +309,7 @@ async function runCodegen(protoDirs, projectRoot, finder) {
|
|
|
294
309
|
|
|
295
310
|
if (!parsedFlags.hasGenerationFlags()) {
|
|
296
311
|
cli.usageError(
|
|
297
|
-
"no generation flags specified (use --all, --type, --service, --client, or --
|
|
312
|
+
"no generation flags specified (use --all, --type, --service, --client, --definition, or --metadata)",
|
|
298
313
|
);
|
|
299
314
|
process.exit(2);
|
|
300
315
|
}
|
package/package.json
CHANGED
package/src/base.js
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import protobuf from "protobufjs";
|
|
4
|
+
|
|
5
|
+
/** Convert camelCase to snake_case (protobufjs normalizes field names) */
|
|
6
|
+
function camelToSnake(str) {
|
|
7
|
+
return str.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Protobufjs scalar type names are already human-readable (string, int32, bool, etc.)
|
|
11
|
+
// so most pass through as-is. This map handles any aliases needed.
|
|
12
|
+
const PBJS_TYPE_MAP = {
|
|
13
|
+
string: "string",
|
|
14
|
+
int32: "int32",
|
|
15
|
+
int64: "int64",
|
|
16
|
+
uint32: "uint32",
|
|
17
|
+
uint64: "uint64",
|
|
18
|
+
sint32: "sint32",
|
|
19
|
+
sint64: "sint64",
|
|
20
|
+
fixed32: "fixed32",
|
|
21
|
+
fixed64: "fixed64",
|
|
22
|
+
sfixed32: "sfixed32",
|
|
23
|
+
sfixed64: "sfixed64",
|
|
24
|
+
float: "float",
|
|
25
|
+
double: "double",
|
|
26
|
+
bool: "bool",
|
|
27
|
+
bytes: "bytes",
|
|
28
|
+
};
|
|
3
29
|
|
|
4
30
|
/**
|
|
5
31
|
* Base class for code generation utilities providing shared functionality
|
|
@@ -242,6 +268,75 @@ export class CodegenBase {
|
|
|
242
268
|
};
|
|
243
269
|
}
|
|
244
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Parse metadata for a proto file: service methods with request type fields.
|
|
273
|
+
* Uses protobufjs for field types, optionality, and comments.
|
|
274
|
+
* @param {string} protoPath - Absolute path to .proto file
|
|
275
|
+
* @returns {{packageName:string, serviceName:string, methods:object}|null}
|
|
276
|
+
*/
|
|
277
|
+
parseMetadata(protoPath) {
|
|
278
|
+
const parsed = this.parseProtoFile(protoPath);
|
|
279
|
+
if (!parsed) return null;
|
|
280
|
+
|
|
281
|
+
const { packageName, serviceName, methods: parsedMethods } = parsed;
|
|
282
|
+
|
|
283
|
+
// Load with protobufjs for field types, optionality, and comments
|
|
284
|
+
const root = new protobuf.Root();
|
|
285
|
+
root.resolvePath = (origin, target) => {
|
|
286
|
+
for (const dir of this.includeDirs) {
|
|
287
|
+
const candidate = this.#path.join(dir, target);
|
|
288
|
+
if (this.#fs.existsSync(candidate)) return candidate;
|
|
289
|
+
}
|
|
290
|
+
return target;
|
|
291
|
+
};
|
|
292
|
+
root.loadSync(protoPath, { alternateCommentMode: true });
|
|
293
|
+
root.resolveAll();
|
|
294
|
+
|
|
295
|
+
const methods = {};
|
|
296
|
+
for (const method of parsedMethods) {
|
|
297
|
+
const reqTypeName = `${method.requestTypeNamespace}.${method.requestType}`;
|
|
298
|
+
let pbjsType = null;
|
|
299
|
+
try {
|
|
300
|
+
pbjsType = root.lookupType(reqTypeName);
|
|
301
|
+
} catch {
|
|
302
|
+
try {
|
|
303
|
+
pbjsType = root.lookupType(method.requestType);
|
|
304
|
+
} catch {
|
|
305
|
+
// no type found
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const fields = {};
|
|
310
|
+
if (pbjsType) {
|
|
311
|
+
for (const [camelName, field] of Object.entries(pbjsType.fields)) {
|
|
312
|
+
const name = camelToSnake(camelName);
|
|
313
|
+
// Map protobufjs type to simple type name
|
|
314
|
+
const typeName = field.resolvedType
|
|
315
|
+
? "message"
|
|
316
|
+
: PBJS_TYPE_MAP[field.type] || field.type;
|
|
317
|
+
|
|
318
|
+
// proto3 optional uses synthetic oneofs named _fieldname
|
|
319
|
+
const hasOptionalKeyword =
|
|
320
|
+
pbjsType.oneofs?.[`_${camelName}`] !== undefined;
|
|
321
|
+
|
|
322
|
+
fields[name] = {
|
|
323
|
+
type: typeName,
|
|
324
|
+
optional: hasOptionalKeyword,
|
|
325
|
+
repeated: field.repeated || false,
|
|
326
|
+
description: field.comment || null,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
methods[method.name] = {
|
|
332
|
+
requestType: `${method.requestTypeNamespace}.${method.requestType}`,
|
|
333
|
+
fields,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { packageName, serviceName, methods };
|
|
338
|
+
}
|
|
339
|
+
|
|
245
340
|
/**
|
|
246
341
|
* Find the namespace for a given type by comparing structure
|
|
247
342
|
* @param {object} typeToFind - The type definition to find namespace for
|
package/src/index.js
CHANGED
package/src/metadata.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates field metadata for all service methods from proto files.
|
|
3
|
+
* Output is a static ESM module consumed by libmcp for schema generation.
|
|
4
|
+
*/
|
|
5
|
+
export class CodegenMetadata {
|
|
6
|
+
#base;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {object} base - CodegenBase instance providing shared utilities
|
|
10
|
+
*/
|
|
11
|
+
constructor(base) {
|
|
12
|
+
if (!base) throw new Error("CodegenBase instance is required");
|
|
13
|
+
this.#base = base;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate metadata.js containing field descriptors for all service methods.
|
|
18
|
+
* @param {string} generatedPath - Path to generated code directory
|
|
19
|
+
* @returns {Promise<void>}
|
|
20
|
+
*/
|
|
21
|
+
async run(generatedPath) {
|
|
22
|
+
if (!generatedPath) throw new Error("generatedPath is required");
|
|
23
|
+
|
|
24
|
+
const protoFiles = this.#base
|
|
25
|
+
.collectProtoFiles({ includeTools: false })
|
|
26
|
+
.filter((file) => !file.endsWith(this.#base.path.sep + "common.proto"));
|
|
27
|
+
|
|
28
|
+
const metadata = {};
|
|
29
|
+
|
|
30
|
+
for (const protoFile of protoFiles) {
|
|
31
|
+
const parsed = this.#base.parseMetadata(protoFile);
|
|
32
|
+
if (!parsed) continue;
|
|
33
|
+
|
|
34
|
+
const serviceKey = `${parsed.packageName}.${parsed.serviceName}`;
|
|
35
|
+
metadata[serviceKey] = parsed.methods;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const typesDir = this.#base.path.join(generatedPath, "types");
|
|
39
|
+
this.#base.fs.mkdirSync(typesDir, { recursive: true });
|
|
40
|
+
|
|
41
|
+
const content =
|
|
42
|
+
`/** @generated by fit-codegen — do not edit */\n` +
|
|
43
|
+
`export const metadata = ${JSON.stringify(metadata, null, 2)};\n`;
|
|
44
|
+
|
|
45
|
+
this.#base.fs.writeFileSync(
|
|
46
|
+
this.#base.path.join(typesDir, "metadata.js"),
|
|
47
|
+
content,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|