@stripe/extensibility-custom-objects-tools 0.40.0

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.
Files changed (52) hide show
  1. package/LICENSE.md +19 -0
  2. package/README.md +32 -0
  3. package/dist/build/build.d.ts +46 -0
  4. package/dist/build/build.d.ts.map +1 -0
  5. package/dist/build/cli.cjs +992 -0
  6. package/dist/build/cli.d.ts +3 -0
  7. package/dist/build/cli.d.ts.map +1 -0
  8. package/dist/build/cli.js +991 -0
  9. package/dist/build/extract-schemas.cli.cjs +8747 -0
  10. package/dist/build/extract-schemas.cli.d.ts +9 -0
  11. package/dist/build/extract-schemas.cli.d.ts.map +1 -0
  12. package/dist/build/extract-schemas.cli.js +8738 -0
  13. package/dist/build/extract-schemas.d.ts +17 -0
  14. package/dist/build/extract-schemas.d.ts.map +1 -0
  15. package/dist/build/package-build.d.ts +115 -0
  16. package/dist/build/package-build.d.ts.map +1 -0
  17. package/dist/extensibility-custom-objects-tools-alpha.d.ts +153 -0
  18. package/dist/extensibility-custom-objects-tools-beta.d.ts +32 -0
  19. package/dist/extensibility-custom-objects-tools-internal-alpha.d.ts +77 -0
  20. package/dist/extensibility-custom-objects-tools-internal-beta.d.ts +77 -0
  21. package/dist/extensibility-custom-objects-tools-internal-internal.d.ts +475 -0
  22. package/dist/extensibility-custom-objects-tools-internal-public.d.ts +77 -0
  23. package/dist/extensibility-custom-objects-tools-internal.d.ts +153 -0
  24. package/dist/extensibility-custom-objects-tools-public.d.ts +32 -0
  25. package/dist/generators/index.d.ts +43 -0
  26. package/dist/generators/index.d.ts.map +1 -0
  27. package/dist/index.cjs +956 -0
  28. package/dist/index.d.ts +13 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +927 -0
  31. package/dist/internal.cjs +8820 -0
  32. package/dist/internal.d.ts +7 -0
  33. package/dist/internal.d.ts.map +1 -0
  34. package/dist/internal.js +8791 -0
  35. package/dist/transformer/classify-fields.d.ts +105 -0
  36. package/dist/transformer/classify-fields.d.ts.map +1 -0
  37. package/dist/transformer/generate-descriptors.d.ts +72 -0
  38. package/dist/transformer/generate-descriptors.d.ts.map +1 -0
  39. package/dist/transformer/metadata.d.ts +25 -0
  40. package/dist/transformer/metadata.d.ts.map +1 -0
  41. package/dist/transformer/transform.d.ts +35 -0
  42. package/dist/transformer/transform.d.ts.map +1 -0
  43. package/dist/transformer/types.d.ts +146 -0
  44. package/dist/transformer/types.d.ts.map +1 -0
  45. package/dist/transformer/visitors.d.ts +99 -0
  46. package/dist/transformer/visitors.d.ts.map +1 -0
  47. package/dist/tsconfig.build.tsbuildinfo +1 -0
  48. package/generators/custom-object/files/___packagesSubfolder___/src/___apiName___.object.ts.mustache +95 -0
  49. package/generators/custom-object-test/files/___packagesSubfolder___/src/___apiName___.object.test.ts.mustache +35 -0
  50. package/generators/custom-objects-workspace/files/___packagesSubfolder___/package.json.mustache +20 -0
  51. package/generators/custom-objects-workspace/files/___packagesSubfolder___/tsconfig.json +9 -0
  52. package/package.json +77 -0
@@ -0,0 +1,991 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/build/cli.ts
4
+ import { _createLogger } from "@stripe/extensibility-tool-utils";
5
+
6
+ // src/build/build.ts
7
+ import { mkdir, readdir, readFile as readFile2, writeFile } from "fs/promises";
8
+ import { dirname, join, relative } from "path";
9
+
10
+ // src/transformer/transform.ts
11
+ import { readFile } from "fs/promises";
12
+ import { Project } from "ts-morph";
13
+
14
+ // src/transformer/visitors.ts
15
+ import { Node, SyntaxKind } from "ts-morph";
16
+
17
+ // src/transformer/metadata.ts
18
+ function toSnakeCase(str) {
19
+ return str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/([0-9])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").toLowerCase();
20
+ }
21
+
22
+ // src/transformer/classify-fields.ts
23
+ function classifyJsonSchemaProperty(schema, typeName) {
24
+ const { type, format } = schema;
25
+ if (type === "string") {
26
+ if (format === "date-time") return { kind: "datetime" };
27
+ if (format === "decimal") return { kind: "decimal" };
28
+ if (schema.enum !== void 0 && schema.enum.length > 0) {
29
+ return { kind: "enum", typeName: typeName ?? "UnknownEnum" };
30
+ }
31
+ return { kind: "primitive" };
32
+ }
33
+ if (type === "array" && schema.items) {
34
+ const element = classifyJsonSchemaProperty(schema.items, typeName);
35
+ return { kind: "array", element };
36
+ }
37
+ if (type === "object") {
38
+ if (schema.additionalProperties !== void 0 && typeof schema.additionalProperties === "object" && (!schema.properties || Object.keys(schema.properties).length === 0)) {
39
+ const value = classifyJsonSchemaProperty(schema.additionalProperties, typeName);
40
+ return { kind: "map", value };
41
+ }
42
+ if (schema.properties && Object.keys(schema.properties).length > 0) {
43
+ return { kind: "interface", typeName: typeName ?? "UnknownInterface" };
44
+ }
45
+ return { kind: "primitive" };
46
+ }
47
+ return { kind: "primitive" };
48
+ }
49
+ function classifyObjectSchema(schema, prefix) {
50
+ const fields = {};
51
+ const optionality = {};
52
+ const nestedSchemas = [];
53
+ const requiredSet = new Set(schema.required ?? []);
54
+ const properties = schema.properties ?? {};
55
+ for (const [propName, propSchema] of Object.entries(properties)) {
56
+ const nestedTypeName = `${prefix}_${propName}`;
57
+ const classification = classifyJsonSchemaProperty(propSchema, nestedTypeName);
58
+ fields[propName] = classification;
59
+ optionality[propName] = !requiredSet.has(propName);
60
+ collectNestedSchemas(propSchema, nestedTypeName, nestedSchemas);
61
+ }
62
+ return { fields, optionality, nestedSchemas };
63
+ }
64
+ function collectNestedSchemas(schema, typeName, results) {
65
+ if (schema.type === "object" && schema.properties && Object.keys(schema.properties).length > 0) {
66
+ results.push({ typeName, schema });
67
+ for (const [propName, propSchema] of Object.entries(schema.properties)) {
68
+ collectNestedSchemas(propSchema, `${typeName}_${propName}`, results);
69
+ }
70
+ }
71
+ if (schema.type === "array" && schema.items) {
72
+ collectNestedSchemas(schema.items, typeName, results);
73
+ }
74
+ if (schema.type === "object" && typeof schema.additionalProperties === "object") {
75
+ collectNestedSchemas(schema.additionalProperties, typeName, results);
76
+ }
77
+ }
78
+
79
+ // src/transformer/generate-descriptors.ts
80
+ var toSchemaNode = (schema) => (
81
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-type-assertion -- single centralized cast boundary
82
+ schema
83
+ );
84
+ function getFieldTransform(cls) {
85
+ switch (cls.kind) {
86
+ case "primitive":
87
+ return "_identity";
88
+ case "decimal":
89
+ return "_translateDecimal";
90
+ case "datetime":
91
+ return "_translateDateTime";
92
+ case "enum":
93
+ return "_identity";
94
+ case "interface":
95
+ return `_translateShape(() => ${cls.typeName}Descriptor)`;
96
+ case "array":
97
+ return `_translateArray(${getFieldTransform(cls.element)})`;
98
+ case "map":
99
+ return `_translateMap(_identity, ${getFieldTransform(cls.value)})`;
100
+ default: {
101
+ const _exhaustive = cls;
102
+ return _exhaustive;
103
+ }
104
+ }
105
+ }
106
+ function wrapTransform(expr, isOptional) {
107
+ if (isOptional) return expr;
108
+ return expr === "_identity" ? "_required()" : `_required(${expr})`;
109
+ }
110
+ function generateShapeDescriptor(descriptorName, strategyType, classified, propertyNameMap) {
111
+ const fieldEntries = Object.entries(classified.fields).map(([schemaKey, cls]) => {
112
+ const isOptional = classified.optionality[schemaKey] ?? true;
113
+ const transformExpr = wrapTransform(getFieldTransform(cls), isOptional);
114
+ const tsKey = propertyNameMap?.[schemaKey] ?? schemaKey;
115
+ const wireClause = tsKey !== schemaKey ? `wire: '${schemaKey}', ` : "";
116
+ return ` { ${wireClause}type: '${tsKey}', transform: ${transformExpr} },`;
117
+ });
118
+ return [
119
+ `const ${descriptorName} = new _ShapeDescriptor<${strategyType}>('${descriptorName}', [`,
120
+ ...fieldEntries,
121
+ `]);`
122
+ ].join("\n");
123
+ }
124
+ function emitDescriptorsForSchema(schema, descriptorName, prefix, strategyType, code, propertyNameMap) {
125
+ const classified = classifyObjectSchema(schema, prefix);
126
+ for (const nested of classified.nestedSchemas) {
127
+ const nestedClassified = classifyObjectSchema(nested.schema, nested.typeName);
128
+ code.push("");
129
+ code.push(
130
+ generateShapeDescriptor(
131
+ `${nested.typeName}Descriptor`,
132
+ "typeof _ProtoWireToType | typeof _TypeToProtoWire",
133
+ nestedClassified
134
+ // Nested schemas don't get a property name map — they use schema keys directly.
135
+ // The mapping only applies at the top-level surface boundary.
136
+ )
137
+ );
138
+ }
139
+ code.push("");
140
+ code.push(
141
+ generateShapeDescriptor(descriptorName, strategyType, classified, propertyNameMap)
142
+ );
143
+ }
144
+ function generateDescriptors(input) {
145
+ const code = [];
146
+ const descriptorNames = {
147
+ fields: null,
148
+ actions: {}
149
+ };
150
+ const fieldsSchema = toSchemaNode(input.fieldsSchema);
151
+ if (fieldsSchema?.properties && Object.keys(fieldsSchema.properties).length > 0) {
152
+ const fieldsDescriptorName = `${input.className}_FieldsDescriptor`;
153
+ emitDescriptorsForSchema(
154
+ fieldsSchema,
155
+ fieldsDescriptorName,
156
+ `${input.className}_Fields`,
157
+ "typeof _ProtoWireToType",
158
+ code,
159
+ input.fieldsPropertyNameMap
160
+ );
161
+ descriptorNames.fields = fieldsDescriptorName;
162
+ }
163
+ for (const [apiName, action] of Object.entries(input.actions)) {
164
+ const actionDescriptors = {
165
+ input: null,
166
+ output: null
167
+ };
168
+ const inputSchema = toSchemaNode(action.inputSchema);
169
+ const outputSchema = toSchemaNode(action.outputSchema);
170
+ if (inputSchema?.properties && Object.keys(inputSchema.properties).length > 0) {
171
+ const inputDescriptorName = `${input.className}_${apiName}_InputDescriptor`;
172
+ emitDescriptorsForSchema(
173
+ inputSchema,
174
+ inputDescriptorName,
175
+ `${input.className}_${apiName}_Input`,
176
+ "typeof _ProtoWireToType",
177
+ code,
178
+ action.inputPropertyNameMap
179
+ );
180
+ actionDescriptors.input = inputDescriptorName;
181
+ }
182
+ if (outputSchema?.properties && Object.keys(outputSchema.properties).length > 0) {
183
+ const outputDescriptorName = `${input.className}_${apiName}_OutputDescriptor`;
184
+ emitDescriptorsForSchema(
185
+ outputSchema,
186
+ outputDescriptorName,
187
+ `${input.className}_${apiName}_Output`,
188
+ "typeof _TypeToProtoWire",
189
+ code,
190
+ action.outputPropertyNameMap
191
+ );
192
+ actionDescriptors.output = outputDescriptorName;
193
+ }
194
+ descriptorNames.actions[apiName] = actionDescriptors;
195
+ }
196
+ return { code, descriptorNames };
197
+ }
198
+
199
+ // src/transformer/visitors.ts
200
+ function getDecoratorMetadata(decorator) {
201
+ const expression = decorator.getExpression();
202
+ if (Node.isIdentifier(expression)) {
203
+ return { name: expression.getText() };
204
+ }
205
+ return null;
206
+ }
207
+ function hasCustomObjectDecorator(classDecl) {
208
+ return classDecl.getDecorators().some((decorator) => {
209
+ const metadata = getDecoratorMetadata(decorator);
210
+ return metadata?.name === "CustomObject";
211
+ });
212
+ }
213
+ function extractArgType(method) {
214
+ const params = method.getParameters();
215
+ if (params.length === 0) {
216
+ return "{}";
217
+ }
218
+ const firstParam = params[0];
219
+ if (!firstParam) {
220
+ return "{}";
221
+ }
222
+ const typeNode = firstParam.getTypeNode();
223
+ if (typeNode) {
224
+ return typeNode.getText();
225
+ }
226
+ const paramType = firstParam.getType();
227
+ return paramType.getText() || "unknown";
228
+ }
229
+ function extractReturnType(method) {
230
+ const returnTypeNode = method.getReturnTypeNode();
231
+ if (returnTypeNode) {
232
+ return returnTypeNode.getText();
233
+ }
234
+ const returnType = method.getReturnType();
235
+ const text = returnType.getText();
236
+ if (text === "void" || text === "undefined") {
237
+ return "void";
238
+ }
239
+ return text || "void";
240
+ }
241
+ function extractTSDocTag(node, tagName) {
242
+ const jsDocs = node.getJsDocs();
243
+ const lastDoc = jsDocs.at(-1);
244
+ if (!lastDoc) return void 0;
245
+ const tag = lastDoc.getTags().find((t) => t.getTagName() === tagName);
246
+ if (!tag) return void 0;
247
+ const text = tag.getCommentText()?.trim();
248
+ if (!text) return void 0;
249
+ return text;
250
+ }
251
+ function extractApiNameFromAction(method, fallbackMethodName) {
252
+ const tagValue = extractTSDocTag(method, "apiName");
253
+ if (tagValue) return tagValue;
254
+ return toSnakeCase(fallbackMethodName);
255
+ }
256
+ function extractActionMetadata(method) {
257
+ const decorators = method.getDecorators();
258
+ for (const decorator of decorators) {
259
+ const metadata = getDecoratorMetadata(decorator);
260
+ if (!metadata) {
261
+ continue;
262
+ }
263
+ if (metadata.name === "Action") {
264
+ const methodName = method.getName();
265
+ const argType = extractArgType(method);
266
+ const returnType = extractReturnType(method);
267
+ const apiName = extractApiNameFromAction(method, methodName);
268
+ const result = {
269
+ methodName,
270
+ argType,
271
+ returnType,
272
+ apiName
273
+ };
274
+ return result;
275
+ }
276
+ }
277
+ return null;
278
+ }
279
+ function extractApiName(classDecl) {
280
+ const tagValue = extractTSDocTag(classDecl, "apiName");
281
+ if (tagValue) return tagValue;
282
+ const className = classDecl.getName();
283
+ if (!className) {
284
+ throw new Error(
285
+ "Anonymous @CustomObject class must have an @apiName TSDoc tag. Either name the class or add /** @apiName my_name */ above the decorator."
286
+ );
287
+ }
288
+ return toSnakeCase(className);
289
+ }
290
+ function extractPlatformMetadata(classDecl) {
291
+ const apiName = extractApiName(classDecl);
292
+ const actions = {
293
+ instance: {}
294
+ };
295
+ for (const method of classDecl.getInstanceMethods()) {
296
+ const actionMetadata = extractActionMetadata(method);
297
+ if (actionMetadata) {
298
+ actions.instance[actionMetadata.methodName] = actionMetadata;
299
+ }
300
+ }
301
+ return {
302
+ apiName,
303
+ actions
304
+ };
305
+ }
306
+ function injectPlatformMetadata(classDecl, metadata) {
307
+ const existingProperty = classDecl.getStaticProperty("__platformMeta__");
308
+ if (existingProperty) {
309
+ existingProperty.remove();
310
+ }
311
+ const metadataJson = JSON.stringify(metadata, null, 2);
312
+ classDecl.insertProperty(0, {
313
+ isStatic: true,
314
+ name: "__platformMeta__",
315
+ initializer: metadataJson
316
+ });
317
+ }
318
+ function injectConstructorProxy(classDecl) {
319
+ let constructor = classDecl.getConstructors()[0];
320
+ const hasBaseClass = classDecl.getExtends() !== void 0;
321
+ if (!constructor) {
322
+ const statements = hasBaseClass ? ["super(id);"] : [];
323
+ constructor = classDecl.addConstructor({
324
+ parameters: [{ name: "id", type: "string", hasQuestionToken: true }],
325
+ statements
326
+ });
327
+ }
328
+ const returnStatements = constructor.getDescendantsOfKind(SyntaxKind.ReturnStatement);
329
+ if (returnStatements.length > 0) {
330
+ throw new Error(
331
+ `Custom object "${classDecl.getName() ?? "anonymous"}" has explicit return statements in its constructor. Custom object constructors cannot return values \u2014 the proxy is returned automatically.`
332
+ );
333
+ }
334
+ constructor.insertStatements(constructor.getStatements().length, [
335
+ "if (!this.fields) { (this as any).fields = {}; }",
336
+ "return _createInstanceProxy(this).proxy;"
337
+ ]);
338
+ }
339
+ function injectAutoSave(method) {
340
+ method.addStatements(["if (__hasPendingChanges(this)) {", " __queueSave(this);", "}"]);
341
+ }
342
+ var CUSTOM_OBJECTS_IMPORTS = [
343
+ "_createInstanceProxy",
344
+ "__hasPendingChanges",
345
+ "__queueSave",
346
+ "_registerClass",
347
+ "_getProxyState",
348
+ "_getSlatedUpdates",
349
+ "_clearSlatedUpdates",
350
+ "_buildMethodResponse"
351
+ ];
352
+ var STDLIB_IMPORTS = [
353
+ "_ShapeDescriptor",
354
+ "_apply",
355
+ "_ProtoWireToType",
356
+ "_TypeToProtoWire",
357
+ "_identity",
358
+ "_required",
359
+ "_translateDateTime",
360
+ "_translateDecimal",
361
+ "_translateShape",
362
+ "_translateArray",
363
+ "_translateMap",
364
+ "_isPromiseLike"
365
+ ];
366
+ function injectRuntimeImports(sourceFile, hasDescriptors = false) {
367
+ const existingRuntimeCoImport = sourceFile.getImportDeclarations().find(
368
+ (decl) => decl.getModuleSpecifierValue() === "@stripe/extensibility-custom-objects" && !decl.isTypeOnly()
369
+ );
370
+ if (existingRuntimeCoImport) {
371
+ const namedImports = existingRuntimeCoImport.getNamedImports().map((ni) => ni.getName());
372
+ for (const needed of CUSTOM_OBJECTS_IMPORTS) {
373
+ if (!namedImports.includes(needed)) {
374
+ existingRuntimeCoImport.addNamedImport(needed);
375
+ }
376
+ }
377
+ } else {
378
+ sourceFile.insertImportDeclaration(0, {
379
+ moduleSpecifier: "@stripe/extensibility-custom-objects",
380
+ namedImports: [...CUSTOM_OBJECTS_IMPORTS]
381
+ });
382
+ }
383
+ if (hasDescriptors) {
384
+ const existingRuntimeStdlibImport = sourceFile.getImportDeclarations().find(
385
+ (decl) => decl.getModuleSpecifierValue() === "@stripe/extensibility-sdk/stdlib" && !decl.isTypeOnly()
386
+ );
387
+ if (existingRuntimeStdlibImport) {
388
+ const namedImports = existingRuntimeStdlibImport.getNamedImports().map((ni) => ni.getName());
389
+ for (const needed of STDLIB_IMPORTS) {
390
+ if (!namedImports.includes(needed)) {
391
+ existingRuntimeStdlibImport.addNamedImport(needed);
392
+ }
393
+ }
394
+ } else {
395
+ sourceFile.insertImportDeclaration(0, {
396
+ moduleSpecifier: "@stripe/extensibility-sdk/stdlib",
397
+ namedImports: [...STDLIB_IMPORTS]
398
+ });
399
+ }
400
+ }
401
+ }
402
+ var KNOWN_DECORATOR_NAMES = /* @__PURE__ */ new Set(["CustomObject", "CustomFields", "Action"]);
403
+ function removeDecorators(classDecl) {
404
+ for (const decorator of classDecl.getDecorators()) {
405
+ const metadata = getDecoratorMetadata(decorator);
406
+ if (metadata?.name && KNOWN_DECORATOR_NAMES.has(metadata.name)) {
407
+ decorator.remove();
408
+ }
409
+ }
410
+ for (const method of classDecl.getMethods()) {
411
+ for (const decorator of method.getDecorators()) {
412
+ const metadata = getDecoratorMetadata(decorator);
413
+ if (metadata?.name && KNOWN_DECORATOR_NAMES.has(metadata.name)) {
414
+ decorator.remove();
415
+ }
416
+ }
417
+ }
418
+ for (const property of classDecl.getProperties()) {
419
+ for (const decorator of property.getDecorators()) {
420
+ const metadata = getDecoratorMetadata(decorator);
421
+ if (metadata?.name && KNOWN_DECORATOR_NAMES.has(metadata.name)) {
422
+ decorator.remove();
423
+ }
424
+ }
425
+ }
426
+ }
427
+ function removeDecoratorImports(sourceFile) {
428
+ const imports = sourceFile.getImportDeclarations();
429
+ for (const importDecl of imports) {
430
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
431
+ if (moduleSpecifier === "@stripe/extensibility-custom-objects") {
432
+ const namedImports = importDecl.getNamedImports();
433
+ for (const namedImport of namedImports) {
434
+ const importName = namedImport.getName();
435
+ if (KNOWN_DECORATOR_NAMES.has(importName)) {
436
+ namedImport.remove();
437
+ }
438
+ }
439
+ if (importDecl.getNamedImports().length === 0) {
440
+ importDecl.remove();
441
+ }
442
+ }
443
+ }
444
+ }
445
+ function generateInstanceMethodTypeMap(metadata) {
446
+ const entries = Object.entries(metadata.actions.instance);
447
+ if (entries.length === 0) {
448
+ return "type InstanceMethodTypeMap = Record<string, never>;";
449
+ }
450
+ const typeEntries = entries.map(([methodName, action]) => {
451
+ return ` ${methodName}: { argType: ${action.argType}; returnType: ${action.returnType} };`;
452
+ });
453
+ return `type InstanceMethodTypeMap = {
454
+ ${typeEntries.join("\n")}
455
+ };`;
456
+ }
457
+ function generateWrapperClass(metadata, className, schemaData) {
458
+ if (schemaData) {
459
+ return generateWireConvertingWrapperClass(metadata, className, schemaData);
460
+ }
461
+ return generateLegacyWrapperClass(metadata, className);
462
+ }
463
+ function generateWireConvertingWrapperClass(metadata, className, schemaData) {
464
+ const wrapperName = `${className}Logic`;
465
+ const descriptorInput = {
466
+ className,
467
+ fieldsSchema: schemaData.fieldsSchema,
468
+ fieldsPropertyNameMap: schemaData.fieldsPropertyNameMap,
469
+ actions: Object.fromEntries(
470
+ Object.entries(schemaData.actions).map(([apiName, actionSchema]) => [
471
+ apiName,
472
+ {
473
+ apiName,
474
+ inputSchema: actionSchema.inputSchema,
475
+ outputSchema: actionSchema.outputSchema,
476
+ inputPropertyNameMap: actionSchema.inputPropertyNameMap,
477
+ outputPropertyNameMap: actionSchema.outputPropertyNameMap
478
+ }
479
+ ])
480
+ )
481
+ };
482
+ const descriptorResult = generateDescriptors(descriptorInput);
483
+ const descriptorCode = descriptorResult.code.join("\n");
484
+ const switchCases = buildWireConvertingSwitchCases(metadata, descriptorResult);
485
+ const assignLine = descriptorResult.descriptorNames.fields ? [
486
+ ` const native = _apply(${descriptorResult.descriptorNames.fields}, _ProtoWireToType, wireRecord);`,
487
+ ` Object.assign(instance.fields, native);`
488
+ ].join("\n") : ` Object.assign(instance.fields, wireRecord);`;
489
+ const hydrateBody = assignLine;
490
+ const parts = [];
491
+ if (descriptorCode.trim().length > 0) {
492
+ parts.push("");
493
+ parts.push("// Wire conversion descriptors");
494
+ parts.push(descriptorCode);
495
+ }
496
+ const actionEntries = Object.values(metadata.actions.instance);
497
+ const methodNameUnion = actionEntries.map((a) => `"${a.apiName}"`).join(" | ") || "never";
498
+ parts.push(`
499
+ interface ${wrapperName}Request {
500
+ objectRecord?: Record<string, unknown>;
501
+ methodName: ${methodNameUnion};
502
+ methodArgs?: Record<string, unknown>;
503
+ }
504
+
505
+ class ${wrapperName} {
506
+ // Fresh instance per executeMethod call to prevent state leakage between invocations.
507
+ // The platform may call executeMethod for different object records on the same module.
508
+ private createInstance(): ${className} {
509
+ return new ${className}();
510
+ }
511
+
512
+ private hydrate(instance: ${className}, wireRecord: Record<string, unknown>): void {
513
+ ${hydrateBody}
514
+ }
515
+
516
+ private hydrateObjectRecord(instance: ${className}, objectRecord: Record<string, unknown>): void {
517
+ // Extract the inner fields map from the ObjectRecord proto.
518
+ // The proto shape is { id, object_definition_id, fields: {...}, created, updated, object }.
519
+ const fields = objectRecord.fields;
520
+ if (typeof fields !== 'object' || fields === null) {
521
+ throw new Error('objectRecord.fields must be a non-null object.');
522
+ }
523
+
524
+ // Set state.hydrating to suppress dirty tracking and allow platform field
525
+ // writes for the duration of hydration. Cleared in the finally block.
526
+ const proxyState = _getProxyState(instance);
527
+ if (proxyState) { proxyState.hydrating = true; }
528
+ try {
529
+ this.hydrate(instance, fields as Record<string, unknown>);
530
+
531
+ // Hydrate platform fields (id, object, created, updated, livemode)
532
+ const platformFields = ['id', 'object', 'created', 'updated', 'livemode'];
533
+ for (const key of platformFields) {
534
+ if (key in objectRecord) {
535
+ (instance as unknown as Record<string, unknown>)[key] = objectRecord[key];
536
+ }
537
+ }
538
+ } finally {
539
+ if (proxyState) { proxyState.hydrating = false; }
540
+ }
541
+ }
542
+
543
+ executeMethod(request: ${wrapperName}Request, config: unknown, context: unknown): unknown {
544
+ const instance = this.createInstance();
545
+
546
+ if (request.objectRecord) {
547
+ this.hydrateObjectRecord(instance, request.objectRecord);
548
+ }
549
+
550
+ // Assert: slated updates must be empty after hydration. If they're not,
551
+ // something has gone wrong \u2014 hydration should never produce slated updates.
552
+ const staleUpdates = _getSlatedUpdates(instance);
553
+ if (Object.keys(staleUpdates).length > 0) {
554
+ throw new Error('Invariant violation: slatedUpdates is non-empty after hydration. ' +
555
+ 'Fields: ' + Object.keys(staleUpdates).join(', '));
556
+ }
557
+ const methodArgs = request.methodArgs ?? {};
558
+
559
+ switch (request.methodName) {
560
+ ${switchCases}
561
+ }
562
+ throw new Error(\`Unknown method: \${String(request.methodName)}\`);
563
+ }
564
+ }
565
+
566
+ export default new ${wrapperName}();`);
567
+ return parts.join("\n");
568
+ }
569
+ function buildWireConvertingSwitchCases(metadata, descriptorResult) {
570
+ const cases = [];
571
+ for (const action of Object.values(metadata.actions.instance)) {
572
+ const { apiName, methodName } = action;
573
+ const actionDescriptors = descriptorResult.descriptorNames.actions[apiName];
574
+ const inputDescriptor = actionDescriptors?.input;
575
+ const outputDescriptor = actionDescriptors?.output;
576
+ const caseLines = [];
577
+ caseLines.push(` case "${apiName}": {`);
578
+ if (inputDescriptor) {
579
+ caseLines.push(
580
+ ` const nativeArgs = _apply(${inputDescriptor}, _ProtoWireToType, methodArgs);`
581
+ );
582
+ caseLines.push(` const result = instance.${methodName}(nativeArgs);`);
583
+ } else {
584
+ caseLines.push(` const result = instance.${methodName}(methodArgs);`);
585
+ }
586
+ if (outputDescriptor) {
587
+ caseLines.push(` if (_isPromiseLike(result)) {`);
588
+ caseLines.push(
589
+ ` return Promise.resolve(result).then((r) => _buildMethodResponse(instance, _apply(${outputDescriptor}, _TypeToProtoWire, r)));`
590
+ );
591
+ caseLines.push(` }`);
592
+ caseLines.push(
593
+ ` return _buildMethodResponse(instance, _apply(${outputDescriptor}, _TypeToProtoWire, result));`
594
+ );
595
+ } else {
596
+ caseLines.push(` if (_isPromiseLike(result)) {`);
597
+ caseLines.push(
598
+ ` return Promise.resolve(result).then((r) => _buildMethodResponse(instance, r));`
599
+ );
600
+ caseLines.push(` }`);
601
+ caseLines.push(` return _buildMethodResponse(instance, result);`);
602
+ }
603
+ caseLines.push(` }`);
604
+ cases.push(caseLines.join("\n"));
605
+ }
606
+ return cases.join("\n");
607
+ }
608
+ function generateLegacyWrapperClass(metadata, className) {
609
+ const wrapperName = `${className}Logic`;
610
+ const instanceCases = Object.keys(metadata.actions.instance).map(
611
+ (methodName) => ` case "${methodName}": return instance.${methodName}(args.args);`
612
+ ).join("\n");
613
+ const typeArgs = `
614
+ type ExecuteMethodArg<K extends keyof InstanceMethodTypeMap> = {
615
+ type: "instance_method";
616
+ methodName: K;
617
+ args: InstanceMethodTypeMap[K]["argType"];
618
+ };
619
+ `.trim();
620
+ return `
621
+ ${typeArgs}
622
+
623
+ class ${wrapperName} {
624
+ private instance: ${className};
625
+
626
+ constructor(instanceId?: string) {
627
+ this.instance = new ${className}(instanceId);
628
+ }
629
+
630
+ /**
631
+ * Hydrate the instance with data from the platform
632
+ */
633
+ hydrate(data: Record<string, unknown>): void {
634
+ Object.assign(this.instance, data);
635
+ }
636
+
637
+ executeMethod<K extends keyof InstanceMethodTypeMap>(args: ExecuteMethodArg<K>): InstanceMethodTypeMap[K]["returnType"] {
638
+ const instance = this.instance;
639
+ switch (args.methodName) {
640
+ ${instanceCases || " default: throw new Error(`Unknown method: ${String(args.methodName)}`);"}
641
+ }
642
+ throw new Error(\`Unknown method: \${String(args.methodName)}\`);
643
+ }
644
+ }
645
+ `.trim();
646
+ }
647
+
648
+ // src/transformer/transform.ts
649
+ function transform(sourceCode, fileName) {
650
+ const sourceContext = createInMemorySourceContext(sourceCode, fileName);
651
+ if ("errors" in sourceContext) {
652
+ return {
653
+ code: sourceCode,
654
+ metadata: [],
655
+ errors: sourceContext.errors
656
+ };
657
+ }
658
+ const discovery = discoverTransformPlanFromSourceFile(
659
+ sourceContext.sourceFile,
660
+ fileName
661
+ );
662
+ if (discovery.errors.length > 0) {
663
+ return {
664
+ code: sourceCode,
665
+ metadata: [],
666
+ errors: discovery.errors
667
+ };
668
+ }
669
+ return transformSourceFile(
670
+ sourceCode,
671
+ sourceContext.sourceFile,
672
+ discovery.plan,
673
+ fileName
674
+ );
675
+ }
676
+ function transformSourceFile(sourceCode, sourceFile, plan, fileName) {
677
+ const errors = [];
678
+ const metadata = [];
679
+ const transformedObjects = [];
680
+ try {
681
+ for (const plannedObject of plan.objects) {
682
+ const className = plannedObject.className;
683
+ try {
684
+ const classDecl = sourceFile.getClasses().find((candidate) => candidate.getName() === className);
685
+ if (!classDecl) {
686
+ throw new Error(`Could not find class "${className}" in source file.`);
687
+ }
688
+ const classMeta = plannedObject.platformMetadata;
689
+ injectPlatformMetadata(classDecl, classMeta);
690
+ injectConstructorProxy(classDecl);
691
+ for (const method of classDecl.getInstanceMethods()) {
692
+ if (classMeta.actions.instance[method.getName()]) {
693
+ injectAutoSave(method);
694
+ }
695
+ }
696
+ removeDecorators(classDecl);
697
+ const runtimeClassName = classDecl.getName();
698
+ if (runtimeClassName) {
699
+ const classIndex = classDecl.getChildIndex();
700
+ sourceFile.insertStatements(
701
+ classIndex + 1,
702
+ `_registerClass(${runtimeClassName});`
703
+ );
704
+ }
705
+ metadata.push(classMeta);
706
+ transformedObjects.push(plannedObject);
707
+ } catch (error) {
708
+ const errorMessage = error instanceof Error ? error.message : String(error);
709
+ errors.push({
710
+ message: `Failed to transform class ${className}: ${errorMessage}`,
711
+ ...fileName !== void 0 && { file: fileName }
712
+ });
713
+ }
714
+ }
715
+ if (transformedObjects.length === 0) {
716
+ return {
717
+ code: sourceCode,
718
+ metadata: [],
719
+ errors
720
+ };
721
+ }
722
+ removeDecoratorImports(sourceFile);
723
+ const hasDescriptors = transformedObjects.some((obj) => obj.schemaData !== void 0);
724
+ injectRuntimeImports(sourceFile, hasDescriptors);
725
+ for (const plannedObject of transformedObjects) {
726
+ const wrapperClass = generateWrapperClass(
727
+ plannedObject.platformMetadata,
728
+ plannedObject.className,
729
+ plannedObject.schemaData
730
+ );
731
+ if (!plannedObject.schemaData) {
732
+ const instanceTypeMap = generateInstanceMethodTypeMap(
733
+ plannedObject.platformMetadata
734
+ );
735
+ sourceFile.addStatements([
736
+ "",
737
+ `// Generated type map for ${plannedObject.platformMetadata.apiName}`,
738
+ instanceTypeMap,
739
+ "",
740
+ `// Generated wrapper class for ${plannedObject.platformMetadata.apiName}`,
741
+ wrapperClass
742
+ ]);
743
+ } else {
744
+ sourceFile.addStatements([
745
+ "",
746
+ `// Generated wrapper class for ${plannedObject.platformMetadata.apiName}`,
747
+ wrapperClass
748
+ ]);
749
+ }
750
+ }
751
+ return {
752
+ code: sourceFile.getFullText(),
753
+ metadata,
754
+ errors
755
+ };
756
+ } catch (error) {
757
+ const errorMessage = error instanceof Error ? error.message : String(error);
758
+ errors.push({
759
+ message: `Transform failed: ${errorMessage}`,
760
+ ...fileName !== void 0 && { file: fileName }
761
+ });
762
+ return {
763
+ code: sourceCode,
764
+ metadata: [],
765
+ errors
766
+ };
767
+ }
768
+ }
769
+ function discoverTransformPlanFromSourceFile(sourceFile, fileName) {
770
+ const errors = [];
771
+ try {
772
+ const objects = sourceFile.getClasses().filter(hasCustomObjectDecorator).map((classDecl) => ({
773
+ className: classDecl.getName() ?? "Anonymous",
774
+ platformMetadata: extractPlatformMetadata(classDecl)
775
+ }));
776
+ return {
777
+ plan: { objects },
778
+ errors
779
+ };
780
+ } catch (error) {
781
+ const errorMessage = error instanceof Error ? error.message : String(error);
782
+ errors.push({
783
+ message: `Transform discovery failed: ${errorMessage}`,
784
+ ...fileName !== void 0 && { file: fileName }
785
+ });
786
+ return {
787
+ plan: { objects: [] },
788
+ errors
789
+ };
790
+ }
791
+ }
792
+ function createInMemorySourceContext(sourceCode, fileName) {
793
+ try {
794
+ const project = new Project({
795
+ useInMemoryFileSystem: true,
796
+ compilerOptions: {
797
+ target: 1,
798
+ module: 199,
799
+ moduleResolution: 3,
800
+ strict: true,
801
+ skipLibCheck: true
802
+ }
803
+ });
804
+ return {
805
+ sourceFile: project.createSourceFile(fileName ?? "source.ts", sourceCode)
806
+ };
807
+ } catch (error) {
808
+ const errorMessage = error instanceof Error ? error.message : String(error);
809
+ return {
810
+ errors: [
811
+ {
812
+ message: `Transform failed: ${errorMessage}`,
813
+ ...fileName !== void 0 && { file: fileName }
814
+ }
815
+ ]
816
+ };
817
+ }
818
+ }
819
+
820
+ // src/build/build.ts
821
+ async function findTypeScriptFiles(dir, baseDir = dir) {
822
+ const files = [];
823
+ try {
824
+ const entries = await readdir(dir, { withFileTypes: true });
825
+ for (const entry of entries) {
826
+ const fullPath = join(dir, entry.name);
827
+ if (entry.isDirectory()) {
828
+ if (entry.name === "node_modules") {
829
+ continue;
830
+ }
831
+ const subFiles = await findTypeScriptFiles(fullPath, baseDir);
832
+ files.push(...subFiles);
833
+ } else if (entry.isFile()) {
834
+ if (entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts") && !entry.name.endsWith(".d.ts")) {
835
+ files.push(fullPath);
836
+ }
837
+ }
838
+ }
839
+ } catch {
840
+ return [];
841
+ }
842
+ return files;
843
+ }
844
+ async function ensureDir(dirPath) {
845
+ await mkdir(dirPath, { recursive: true });
846
+ }
847
+ async function build(options) {
848
+ const { inputDir, outputDir } = options;
849
+ const globalErrors = [];
850
+ const fileResults = [];
851
+ try {
852
+ const files = await findTypeScriptFiles(inputDir);
853
+ if (files.length === 0) {
854
+ return {
855
+ files: [],
856
+ errors: [
857
+ {
858
+ message: `No TypeScript files found in ${inputDir}`,
859
+ file: inputDir
860
+ }
861
+ ]
862
+ };
863
+ }
864
+ for (const inputPath of files) {
865
+ try {
866
+ const sourceCode = await readFile2(inputPath, "utf-8");
867
+ const result = transform(sourceCode, inputPath);
868
+ const relativePath = relative(inputDir, inputPath);
869
+ const outputPath = join(outputDir, relativePath);
870
+ await ensureDir(dirname(outputPath));
871
+ await writeFile(outputPath, result.code, "utf-8");
872
+ fileResults.push({
873
+ inputPath,
874
+ outputPath,
875
+ success: result.errors.length === 0,
876
+ errors: result.errors
877
+ });
878
+ if (result.errors.length > 0) {
879
+ globalErrors.push(...result.errors);
880
+ }
881
+ } catch (error) {
882
+ const errorMessage = error instanceof Error ? error.message : String(error);
883
+ const fileError = {
884
+ message: `Failed to process file: ${errorMessage}`,
885
+ file: inputPath
886
+ };
887
+ globalErrors.push(fileError);
888
+ fileResults.push({
889
+ inputPath,
890
+ outputPath: "",
891
+ success: false,
892
+ errors: [fileError]
893
+ });
894
+ }
895
+ }
896
+ return {
897
+ files: fileResults,
898
+ errors: globalErrors
899
+ };
900
+ } catch (error) {
901
+ const errorMessage = error instanceof Error ? error.message : String(error);
902
+ return {
903
+ files: fileResults,
904
+ errors: [
905
+ {
906
+ message: `Build failed: ${errorMessage}`
907
+ }
908
+ ]
909
+ };
910
+ }
911
+ }
912
+
913
+ // src/build/cli.ts
914
+ import { _createCliContext } from "@stripe/extensibility-tool-utils";
915
+ var logger = _createLogger({ name: "co-build" });
916
+ function parseArgs() {
917
+ const args = process.argv.slice(2);
918
+ let inputDir;
919
+ let outputDir;
920
+ for (let i = 0; i < args.length; i++) {
921
+ const arg = args[i];
922
+ if (!arg) continue;
923
+ if (arg === "--input" || arg === "-i") {
924
+ inputDir = args[i + 1];
925
+ i++;
926
+ } else if (arg === "--output" || arg === "-o") {
927
+ outputDir = args[i + 1];
928
+ i++;
929
+ } else if (arg === "--help" || arg === "-h") {
930
+ return null;
931
+ }
932
+ }
933
+ if (!inputDir || !outputDir) {
934
+ return null;
935
+ }
936
+ return { input: inputDir, output: outputDir };
937
+ }
938
+ function printUsage(ux) {
939
+ ux.print(`
940
+ Usage: custom-objects-build --input <dir> --output <dir>
941
+
942
+ Options:
943
+ --input, -i Input directory containing TypeScript source files
944
+ --output, -o Output directory for transformed files
945
+ --help, -h Show this help message
946
+
947
+ Example:
948
+ custom-objects-build --input src --output dist
949
+ `);
950
+ }
951
+ async function main() {
952
+ const ctx = _createCliContext();
953
+ const parsed = parseArgs();
954
+ if (!parsed) {
955
+ printUsage(ctx.ux);
956
+ process.exit(1);
957
+ }
958
+ ctx.ux.log(`Building custom objects...`);
959
+ ctx.ux.log(` Input: ${parsed.input}`);
960
+ ctx.ux.log(` Output: ${parsed.output}`);
961
+ const result = await build({
962
+ inputDir: parsed.input,
963
+ outputDir: parsed.output
964
+ });
965
+ ctx.ux.log(`
966
+ Processed ${String(result.files.length)} file(s)`);
967
+ const successCount = result.files.filter((f) => f.success).length;
968
+ const failureCount = result.files.filter((f) => !f.success).length;
969
+ ctx.ux.log(` Success: ${String(successCount)}`);
970
+ if (failureCount > 0) {
971
+ ctx.ux.log(` Failed: ${String(failureCount)}`);
972
+ }
973
+ if (result.errors.length > 0) {
974
+ ctx.ux.error(`
975
+ ${String(result.errors.length)} error(s) occurred:`);
976
+ for (const error of result.errors) {
977
+ if (error.file) {
978
+ ctx.ux.error(` [${error.file}] ${error.message}`);
979
+ } else {
980
+ ctx.ux.error(` ${error.message}`);
981
+ }
982
+ }
983
+ process.exit(1);
984
+ }
985
+ ctx.ux.log(`
986
+ Build complete!`);
987
+ }
988
+ main().catch((error) => {
989
+ logger.error({ err: error }, "Build failed");
990
+ process.exit(1);
991
+ });