@forwardimpact/libcodegen 0.1.48 → 0.1.50
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 +6 -1
- package/bin/fit-codegen.js +1 -0
- package/package.json +21 -11
- package/src/base.js +59 -41
package/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# libcodegen
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<!-- BEGIN:description — Do not edit. Generated from package.json. -->
|
|
4
|
+
|
|
5
|
+
Protobuf code generation — keep types in sync with proto definitions without
|
|
6
|
+
hand-writing.
|
|
7
|
+
|
|
8
|
+
<!-- END:description -->
|
|
4
9
|
|
|
5
10
|
## Getting Started
|
|
6
11
|
|
package/bin/fit-codegen.js
CHANGED
package/package.json
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/libcodegen",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Protobuf code generation —
|
|
3
|
+
"version": "0.1.50",
|
|
4
|
+
"description": "Protobuf code generation — keep types in sync with proto definitions without hand-writing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codegen",
|
|
7
7
|
"protobuf",
|
|
8
8
|
"build-tool",
|
|
9
9
|
"agent"
|
|
10
10
|
],
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
"homepage": "https://www.forwardimpact.team",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/forwardimpact/monorepo.git",
|
|
15
|
+
"directory": "libraries/libcodegen"
|
|
16
16
|
},
|
|
17
17
|
"license": "Apache-2.0",
|
|
18
18
|
"author": "D. Olsson <hi@senzilla.io>",
|
|
19
|
+
"jobs": [
|
|
20
|
+
{
|
|
21
|
+
"user": "Platform Builders",
|
|
22
|
+
"goal": "Keep Service Contracts Typed",
|
|
23
|
+
"trigger": "Adding a proto definition and realizing the JavaScript types are already stale.",
|
|
24
|
+
"bigHire": "keep types in sync with proto definitions without hand-writing.",
|
|
25
|
+
"littleHire": "change a proto definition and trust the JavaScript types follow.",
|
|
26
|
+
"competesWith": "manual type definitions; hoping hand-written types match; skipping types entirely"
|
|
27
|
+
}
|
|
28
|
+
],
|
|
19
29
|
"type": "module",
|
|
20
30
|
"main": "./src/index.js",
|
|
21
31
|
"exports": {
|
|
@@ -31,10 +41,6 @@
|
|
|
31
41
|
"templates/**",
|
|
32
42
|
"README.md"
|
|
33
43
|
],
|
|
34
|
-
"engines": {
|
|
35
|
-
"bun": ">=1.2.0",
|
|
36
|
-
"node": ">=18.0.0"
|
|
37
|
-
},
|
|
38
44
|
"scripts": {
|
|
39
45
|
"test": "bun test test/*.test.js"
|
|
40
46
|
},
|
|
@@ -51,6 +57,10 @@
|
|
|
51
57
|
"devDependencies": {
|
|
52
58
|
"@forwardimpact/libharness": "^0.1.5"
|
|
53
59
|
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"bun": ">=1.2.0",
|
|
62
|
+
"node": ">=18.0.0"
|
|
63
|
+
},
|
|
54
64
|
"publishConfig": {
|
|
55
65
|
"access": "public"
|
|
56
66
|
}
|
package/src/base.js
CHANGED
|
@@ -27,6 +27,29 @@ const PBJS_TYPE_MAP = {
|
|
|
27
27
|
bytes: "bytes",
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Extract field metadata from a protobufjs Type
|
|
32
|
+
* @param {protobuf.Type} pbjsType - Resolved protobufjs type
|
|
33
|
+
* @returns {object} Map of snake_case field names to metadata
|
|
34
|
+
*/
|
|
35
|
+
function extractFields(pbjsType) {
|
|
36
|
+
const fields = {};
|
|
37
|
+
for (const [camelName, field] of Object.entries(pbjsType.fields)) {
|
|
38
|
+
const name = camelToSnake(camelName);
|
|
39
|
+
const typeName = field.resolvedType
|
|
40
|
+
? "message"
|
|
41
|
+
: PBJS_TYPE_MAP[field.type] || field.type;
|
|
42
|
+
const hasOptionalKeyword = pbjsType.oneofs?.[`_${camelName}`] !== undefined;
|
|
43
|
+
fields[name] = {
|
|
44
|
+
type: typeName,
|
|
45
|
+
optional: hasOptionalKeyword,
|
|
46
|
+
repeated: field.repeated || false,
|
|
47
|
+
description: field.comment || null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return fields;
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
/**
|
|
31
54
|
* Base class for code generation utilities providing shared functionality
|
|
32
55
|
* Implements dependency injection pattern with explicit validation
|
|
@@ -279,10 +302,29 @@ export class CodegenBase {
|
|
|
279
302
|
if (!parsed) return null;
|
|
280
303
|
|
|
281
304
|
const { packageName, serviceName, methods: parsedMethods } = parsed;
|
|
305
|
+
const root = this.#loadProtobufRoot(protoPath);
|
|
306
|
+
|
|
307
|
+
const methods = {};
|
|
308
|
+
for (const method of parsedMethods) {
|
|
309
|
+
const pbjsType = this.#resolveRequestType(root, method);
|
|
310
|
+
const fields = pbjsType ? extractFields(pbjsType) : {};
|
|
311
|
+
methods[method.name] = {
|
|
312
|
+
requestType: `${method.requestTypeNamespace}.${method.requestType}`,
|
|
313
|
+
fields,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
282
316
|
|
|
283
|
-
|
|
317
|
+
return { packageName, serviceName, methods };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Load a protobufjs Root with include-path resolution
|
|
322
|
+
* @param {string} protoPath - Absolute path to .proto file
|
|
323
|
+
* @returns {protobuf.Root}
|
|
324
|
+
*/
|
|
325
|
+
#loadProtobufRoot(protoPath) {
|
|
284
326
|
const root = new protobuf.Root();
|
|
285
|
-
root.resolvePath = (
|
|
327
|
+
root.resolvePath = (_origin, target) => {
|
|
286
328
|
for (const dir of this.includeDirs) {
|
|
287
329
|
const candidate = this.#path.join(dir, target);
|
|
288
330
|
if (this.#fs.existsSync(candidate)) return candidate;
|
|
@@ -291,50 +333,26 @@ export class CodegenBase {
|
|
|
291
333
|
};
|
|
292
334
|
root.loadSync(protoPath, { alternateCommentMode: true });
|
|
293
335
|
root.resolveAll();
|
|
336
|
+
return root;
|
|
337
|
+
}
|
|
294
338
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
339
|
+
/**
|
|
340
|
+
* Resolve the request type for a method from the protobufjs Root
|
|
341
|
+
* @param {protobuf.Root} root - Loaded protobufjs root
|
|
342
|
+
* @param {object} method - Parsed method descriptor
|
|
343
|
+
* @returns {protobuf.Type|null}
|
|
344
|
+
*/
|
|
345
|
+
#resolveRequestType(root, method) {
|
|
346
|
+
const qualified = `${method.requestTypeNamespace}.${method.requestType}`;
|
|
347
|
+
try {
|
|
348
|
+
return root.lookupType(qualified);
|
|
349
|
+
} catch {
|
|
299
350
|
try {
|
|
300
|
-
|
|
351
|
+
return root.lookupType(method.requestType);
|
|
301
352
|
} catch {
|
|
302
|
-
|
|
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
|
-
}
|
|
353
|
+
return null;
|
|
329
354
|
}
|
|
330
|
-
|
|
331
|
-
methods[method.name] = {
|
|
332
|
-
requestType: `${method.requestTypeNamespace}.${method.requestType}`,
|
|
333
|
-
fields,
|
|
334
|
-
};
|
|
335
355
|
}
|
|
336
|
-
|
|
337
|
-
return { packageName, serviceName, methods };
|
|
338
356
|
}
|
|
339
357
|
|
|
340
358
|
/**
|