@distilled.cloud/core 0.30.3 → 1.0.0-rc.10

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 (216) hide show
  1. package/LICENSE +201 -0
  2. package/lib/api.d.ts +165 -0
  3. package/lib/api.d.ts.map +1 -0
  4. package/lib/api.js +190 -0
  5. package/lib/api.js.map +1 -0
  6. package/lib/category.d.ts +4 -4
  7. package/lib/category.js +4 -4
  8. package/lib/codegen/boolean-string-enums.d.ts +36 -0
  9. package/lib/codegen/boolean-string-enums.d.ts.map +1 -0
  10. package/lib/codegen/boolean-string-enums.js +94 -0
  11. package/lib/codegen/boolean-string-enums.js.map +1 -0
  12. package/lib/codegen/boolean-string-enums.test.d.ts +2 -0
  13. package/lib/codegen/boolean-string-enums.test.d.ts.map +1 -0
  14. package/lib/codegen/boolean-string-enums.test.js +147 -0
  15. package/lib/codegen/boolean-string-enums.test.js.map +1 -0
  16. package/lib/codegen/cli.d.ts +79 -0
  17. package/lib/codegen/cli.d.ts.map +1 -0
  18. package/lib/codegen/cli.js +149 -0
  19. package/lib/codegen/cli.js.map +1 -0
  20. package/lib/codegen/emit.d.ts +125 -0
  21. package/lib/codegen/emit.d.ts.map +1 -0
  22. package/lib/codegen/emit.js +101 -0
  23. package/lib/codegen/emit.js.map +1 -0
  24. package/lib/codegen/format.d.ts +23 -0
  25. package/lib/codegen/format.d.ts.map +1 -0
  26. package/lib/codegen/format.js +28 -0
  27. package/lib/codegen/format.js.map +1 -0
  28. package/lib/codegen/generator.d.ts +334 -0
  29. package/lib/codegen/generator.d.ts.map +1 -0
  30. package/lib/codegen/generator.js +813 -0
  31. package/lib/codegen/generator.js.map +1 -0
  32. package/lib/codegen/graph.d.ts +36 -0
  33. package/lib/codegen/graph.d.ts.map +1 -0
  34. package/lib/codegen/graph.js +136 -0
  35. package/lib/codegen/graph.js.map +1 -0
  36. package/lib/codegen/graphql-client.d.ts +62 -0
  37. package/lib/codegen/graphql-client.d.ts.map +1 -0
  38. package/lib/codegen/graphql-client.js +294 -0
  39. package/lib/codegen/graphql-client.js.map +1 -0
  40. package/lib/codegen/graphql-client.test.d.ts +2 -0
  41. package/lib/codegen/graphql-client.test.d.ts.map +1 -0
  42. package/lib/codegen/graphql-client.test.js +311 -0
  43. package/lib/codegen/graphql-client.test.js.map +1 -0
  44. package/lib/codegen/graphql.d.ts +207 -0
  45. package/lib/codegen/graphql.d.ts.map +1 -0
  46. package/lib/codegen/graphql.js +799 -0
  47. package/lib/codegen/graphql.js.map +1 -0
  48. package/lib/codegen/members.d.ts +25 -0
  49. package/lib/codegen/members.d.ts.map +1 -0
  50. package/lib/codegen/members.js +55 -0
  51. package/lib/codegen/members.js.map +1 -0
  52. package/lib/codegen/naming.d.ts +29 -0
  53. package/lib/codegen/naming.d.ts.map +1 -0
  54. package/lib/codegen/naming.js +74 -0
  55. package/lib/codegen/naming.js.map +1 -0
  56. package/lib/codegen/openapi-cli.d.ts +52 -0
  57. package/lib/codegen/openapi-cli.d.ts.map +1 -0
  58. package/lib/codegen/openapi-cli.js +109 -0
  59. package/lib/codegen/openapi-cli.js.map +1 -0
  60. package/lib/codegen/openapi.d.ts +178 -0
  61. package/lib/codegen/openapi.d.ts.map +1 -0
  62. package/lib/codegen/openapi.js +1377 -0
  63. package/lib/codegen/openapi.js.map +1 -0
  64. package/lib/codegen/operations.d.ts +24 -0
  65. package/lib/codegen/operations.d.ts.map +1 -0
  66. package/lib/codegen/operations.js +56 -0
  67. package/lib/codegen/operations.js.map +1 -0
  68. package/lib/codegen/pagination.d.ts +39 -0
  69. package/lib/codegen/pagination.d.ts.map +1 -0
  70. package/lib/codegen/pagination.js +33 -0
  71. package/lib/codegen/pagination.js.map +1 -0
  72. package/lib/codegen/patches.d.ts +65 -0
  73. package/lib/codegen/patches.d.ts.map +1 -0
  74. package/lib/codegen/patches.js +236 -0
  75. package/lib/codegen/patches.js.map +1 -0
  76. package/lib/codegen/patches.test.d.ts +2 -0
  77. package/lib/codegen/patches.test.d.ts.map +1 -0
  78. package/lib/codegen/patches.test.js +105 -0
  79. package/lib/codegen/patches.test.js.map +1 -0
  80. package/lib/codegen/prelude.d.ts +15 -0
  81. package/lib/codegen/prelude.d.ts.map +1 -0
  82. package/lib/codegen/prelude.js +60 -0
  83. package/lib/codegen/prelude.js.map +1 -0
  84. package/lib/codegen/proto.d.ts +121 -0
  85. package/lib/codegen/proto.d.ts.map +1 -0
  86. package/lib/codegen/proto.js +962 -0
  87. package/lib/codegen/proto.js.map +1 -0
  88. package/lib/codegen/rewrite-operation-ids.d.ts +131 -0
  89. package/lib/codegen/rewrite-operation-ids.d.ts.map +1 -0
  90. package/lib/codegen/rewrite-operation-ids.js +1079 -0
  91. package/lib/codegen/rewrite-operation-ids.js.map +1 -0
  92. package/lib/codegen/rewrite-operation-ids.test.d.ts +2 -0
  93. package/lib/codegen/rewrite-operation-ids.test.d.ts.map +1 -0
  94. package/lib/codegen/rewrite-operation-ids.test.js +533 -0
  95. package/lib/codegen/rewrite-operation-ids.test.js.map +1 -0
  96. package/lib/codegen/spec-path.d.ts +16 -0
  97. package/lib/codegen/spec-path.d.ts.map +1 -0
  98. package/lib/codegen/spec-path.js +101 -0
  99. package/lib/codegen/spec-path.js.map +1 -0
  100. package/lib/error-category.d.ts +28 -0
  101. package/lib/error-category.d.ts.map +1 -0
  102. package/lib/error-category.js +46 -0
  103. package/lib/error-category.js.map +1 -0
  104. package/lib/errors.d.ts +1 -0
  105. package/lib/errors.d.ts.map +1 -1
  106. package/lib/errors.js +18 -13
  107. package/lib/errors.js.map +1 -1
  108. package/lib/graphql.d.ts +284 -0
  109. package/lib/graphql.d.ts.map +1 -0
  110. package/lib/graphql.fixture.d.ts +249 -0
  111. package/lib/graphql.fixture.d.ts.map +1 -0
  112. package/lib/graphql.fixture.js +240 -0
  113. package/lib/graphql.fixture.js.map +1 -0
  114. package/lib/graphql.js +718 -0
  115. package/lib/graphql.js.map +1 -0
  116. package/lib/graphql.test.d.ts +2 -0
  117. package/lib/graphql.test.d.ts.map +1 -0
  118. package/lib/graphql.test.js +780 -0
  119. package/lib/graphql.test.js.map +1 -0
  120. package/lib/graphql.types.d.ts +2 -0
  121. package/lib/graphql.types.d.ts.map +1 -0
  122. package/lib/graphql.types.js +45 -0
  123. package/lib/graphql.types.js.map +1 -0
  124. package/lib/json-patch.d.ts +30 -30
  125. package/lib/json-patch.d.ts.map +1 -1
  126. package/lib/json-patch.js +73 -107
  127. package/lib/json-patch.js.map +1 -1
  128. package/lib/pagination.d.ts +77 -51
  129. package/lib/pagination.d.ts.map +1 -1
  130. package/lib/pagination.js +162 -94
  131. package/lib/pagination.js.map +1 -1
  132. package/lib/protocol-http.d.ts +74 -0
  133. package/lib/protocol-http.d.ts.map +1 -0
  134. package/lib/protocol-http.js +590 -0
  135. package/lib/protocol-http.js.map +1 -0
  136. package/lib/protocol-http.test.d.ts +2 -0
  137. package/lib/protocol-http.test.d.ts.map +1 -0
  138. package/lib/protocol-http.test.js +88 -0
  139. package/lib/protocol-http.test.js.map +1 -0
  140. package/lib/protocol-rest.d.ts +134 -0
  141. package/lib/protocol-rest.d.ts.map +1 -0
  142. package/lib/protocol-rest.js +256 -0
  143. package/lib/protocol-rest.js.map +1 -0
  144. package/lib/retry.d.ts +8 -2
  145. package/lib/retry.d.ts.map +1 -1
  146. package/lib/retry.js +22 -16
  147. package/lib/retry.js.map +1 -1
  148. package/lib/schema.d.ts +8 -9
  149. package/lib/schema.d.ts.map +1 -1
  150. package/lib/schema.js +8 -9
  151. package/lib/schema.js.map +1 -1
  152. package/lib/trait.d.ts +174 -0
  153. package/lib/trait.d.ts.map +1 -0
  154. package/lib/trait.js +123 -0
  155. package/lib/trait.js.map +1 -0
  156. package/package.json +24 -78
  157. package/src/api.ts +460 -0
  158. package/src/category.ts +4 -4
  159. package/src/codegen/boolean-string-enums.test.ts +168 -0
  160. package/src/codegen/boolean-string-enums.ts +106 -0
  161. package/src/codegen/cli.ts +285 -0
  162. package/src/codegen/emit.ts +203 -0
  163. package/src/codegen/format.ts +47 -0
  164. package/src/codegen/generator.ts +1283 -0
  165. package/src/codegen/graph.ts +151 -0
  166. package/src/codegen/graphql-client.test.ts +386 -0
  167. package/src/codegen/graphql-client.ts +419 -0
  168. package/src/codegen/graphql.ts +1217 -0
  169. package/src/codegen/members.ts +71 -0
  170. package/src/codegen/naming.ts +86 -0
  171. package/src/codegen/openapi-cli.ts +182 -0
  172. package/src/codegen/openapi.ts +1689 -0
  173. package/src/codegen/operations.ts +76 -0
  174. package/src/codegen/pagination.ts +71 -0
  175. package/src/codegen/patches.test.ts +130 -0
  176. package/src/codegen/patches.ts +291 -0
  177. package/src/codegen/prelude.ts +70 -0
  178. package/src/codegen/proto.ts +1128 -0
  179. package/src/codegen/rewrite-operation-ids.test.ts +563 -0
  180. package/src/codegen/rewrite-operation-ids.ts +1206 -0
  181. package/src/codegen/spec-path.ts +115 -0
  182. package/src/error-category.ts +84 -0
  183. package/src/errors.ts +22 -25
  184. package/src/graphql.fixture.ts +371 -0
  185. package/src/graphql.test.ts +974 -0
  186. package/src/graphql.ts +1321 -0
  187. package/src/graphql.types.ts +185 -0
  188. package/src/json-patch.ts +95 -122
  189. package/src/pagination.ts +217 -146
  190. package/src/protocol-http.test.ts +107 -0
  191. package/src/protocol-http.ts +735 -0
  192. package/src/protocol-rest.ts +391 -0
  193. package/src/retry.ts +21 -22
  194. package/src/schema.ts +9 -10
  195. package/src/trait.ts +274 -0
  196. package/README.md +0 -30
  197. package/lib/client.d.ts +0 -167
  198. package/lib/client.d.ts.map +0 -1
  199. package/lib/client.js +0 -659
  200. package/lib/client.js.map +0 -1
  201. package/lib/schemas.d.ts +0 -60
  202. package/lib/schemas.d.ts.map +0 -1
  203. package/lib/schemas.js +0 -79
  204. package/lib/schemas.js.map +0 -1
  205. package/lib/sensitive.d.ts +0 -71
  206. package/lib/sensitive.d.ts.map +0 -1
  207. package/lib/sensitive.js +0 -96
  208. package/lib/sensitive.js.map +0 -1
  209. package/lib/traits.d.ts +0 -421
  210. package/lib/traits.d.ts.map +0 -1
  211. package/lib/traits.js +0 -737
  212. package/lib/traits.js.map +0 -1
  213. package/src/client.ts +0 -1177
  214. package/src/schemas.ts +0 -128
  215. package/src/sensitive.ts +0 -119
  216. package/src/traits.ts +0 -996
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Boolean-valued string enums on request members (dev-time only).
3
+ *
4
+ * Plenty of APIs document a flag as the string enum `"true" | "false"`
5
+ * rather than a JSON boolean — Cloudflare's `validation_enabled`, Clerk's
6
+ * `include_invalid`, GrowthBook's `deleteMissing`, and so on. Taken
7
+ * literally that surfaces as `"true" | "false" | (string & {})` and every
8
+ * caller writes the ternary itself.
9
+ *
10
+ * {@link generateService} runs this pass over the loaded model before
11
+ * emitting: REQUEST members of such an enum are retargeted to a real
12
+ * `smithy.api#Boolean` and stamped {@link STRING_ENCODED_TRAIT}, which the
13
+ * generator emits as `T.StringEncoded()` and the protocol's `buildRequest`
14
+ * honors by sending the value's string spelling. The TS surface becomes
15
+ * `boolean` with the wire unchanged. The model on disk keeps the string
16
+ * enum the description documents.
17
+ *
18
+ * Deliberately narrow:
19
+ *
20
+ * • request shapes only (`smithy.api#input`) — a response member would
21
+ * need the mirror-image decode, which the protocol does not do;
22
+ * • a list member is retargeted only when EVERY reference to that list
23
+ * comes from a request shape, so a shared list is never rewritten;
24
+ * • the enum must be exactly `{"true", "false"}` — a three-value enum
25
+ * that happens to include them is left alone.
26
+ */
27
+
28
+ /** Synthetic trait: send this member's value as its string spelling. */
29
+ export const STRING_ENCODED_TRAIT = "distilled.protocols#stringEncoded";
30
+
31
+ const BOOLEAN = "smithy.api#Boolean";
32
+
33
+ export interface BooleanStringEnumResult {
34
+ /** Request members retargeted to a real boolean. */
35
+ members: number;
36
+ /** List shapes whose element type was retargeted. */
37
+ lists: number;
38
+ }
39
+
40
+ /** Enum shapes whose values are exactly `"true"` and `"false"`. */
41
+ const booleanEnumIds = (shapes: Record<string, any>): Set<string> => {
42
+ const out = new Set<string>();
43
+ for (const [id, shape] of Object.entries(shapes)) {
44
+ if (shape?.type !== "enum") continue;
45
+ const values = Object.values(shape.members ?? {}).map(
46
+ (m: any) => m?.traits?.["smithy.api#enumValue"],
47
+ );
48
+ if (
49
+ values.length === 2 &&
50
+ values.includes("true") &&
51
+ values.includes("false")
52
+ ) {
53
+ out.add(id);
54
+ }
55
+ }
56
+ return out;
57
+ };
58
+
59
+ const isRequestShape = (shape: any): boolean =>
60
+ shape?.type === "structure" && "smithy.api#input" in (shape.traits ?? {});
61
+
62
+ export const booleanStringEnums = (model: any): BooleanStringEnumResult => {
63
+ const shapes: Record<string, any> = model?.shapes ?? {};
64
+ const boolEnums = booleanEnumIds(shapes);
65
+ if (boolEnums.size === 0) return { members: 0, lists: 0 };
66
+
67
+ // Lists of a boolean enum, and whether anything outside a request shape
68
+ // refers to them (a shared list keeps its documented string type).
69
+ const boolLists = new Set<string>();
70
+ for (const [id, shape] of Object.entries(shapes)) {
71
+ if (shape?.type === "list" && boolEnums.has(shape.member?.target)) {
72
+ boolLists.add(id);
73
+ }
74
+ }
75
+ const sharedLists = new Set<string>();
76
+ for (const shape of Object.values(shapes)) {
77
+ if (isRequestShape(shape)) continue;
78
+ for (const member of Object.values<any>(shape?.members ?? {})) {
79
+ if (boolLists.has(member?.target)) sharedLists.add(member.target);
80
+ }
81
+ if (boolLists.has(shape?.member?.target)) {
82
+ sharedLists.add(shape.member.target);
83
+ }
84
+ }
85
+
86
+ let members = 0;
87
+ const retargeted = new Set<string>();
88
+ for (const shape of Object.values<any>(shapes)) {
89
+ if (!isRequestShape(shape)) continue;
90
+ for (const member of Object.values<any>(shape.members ?? {})) {
91
+ const isList =
92
+ boolLists.has(member?.target) && !sharedLists.has(member.target);
93
+ if (!boolEnums.has(member?.target) && !isList) continue;
94
+ if (isList) {
95
+ retargeted.add(member.target);
96
+ } else {
97
+ member.target = BOOLEAN;
98
+ }
99
+ member.traits = { ...member.traits, [STRING_ENCODED_TRAIT]: {} };
100
+ members++;
101
+ }
102
+ }
103
+ for (const id of retargeted) shapes[id].member.target = BOOLEAN;
104
+
105
+ return { members, lists: retargeted.size };
106
+ };
@@ -0,0 +1,285 @@
1
+ /**
2
+ * The generic generator CLI harness (dev-time only).
3
+ *
4
+ * Owns the smithy→SDK pipeline every SDK package shares: scan the model
5
+ * directory (plus optional hand-authored manual specs), compile each model
6
+ * through {@link generateService} with the provider's {@link SdkSpec}, write
7
+ * the service modules and the namespaced barrel. RFC-6902 patches apply in
8
+ * convert so `.generated-specs` is already the patched model.
9
+ *
10
+ * A provider's `scripts/generate.ts` is: trait consts + an SdkSpec + a
11
+ * `runGeneratorCli` call.
12
+ */
13
+ import { BunRuntime, BunServices } from "@effect/platform-bun";
14
+ import { Console, Effect } from "effect";
15
+ import * as FileSystem from "effect/FileSystem";
16
+ import * as Path from "effect/Path";
17
+ import { Flag } from "effect/unstable/cli";
18
+ import { Command } from "effect/unstable/cli";
19
+ import { barrel } from "./emit.ts";
20
+ import { formatGenerated } from "./format.ts";
21
+ import { generateService, type SdkSpec } from "./generator.ts";
22
+
23
+ export interface GeneratorCliOptions {
24
+ /** Command description shown in --help. */
25
+ readonly description: string;
26
+ /** Absolute package root (usually `path.resolve(import.meta.dir, "..")`). */
27
+ readonly root: string;
28
+ /** Model directory default (relative to root). Default `.generated-specs`. */
29
+ readonly smithyDir?: string;
30
+ /** Output directory default (relative to root). Default `src/services`. */
31
+ readonly outDir?: string;
32
+ /** Model files to skip (e.g. a shared protocols model). */
33
+ readonly excludeModel?: (file: string) => boolean;
34
+ /**
35
+ * Where the models are, when they aren't a flat directory of `<resource>.json`.
36
+ * AWS vendors Amazon's own repo, whose models sit at
37
+ * `models/<service>/service/<version>/<service>-<version>.json`.
38
+ *
39
+ * Returns the model files to compile; manual specs are merged in after,
40
+ * as usual.
41
+ */
42
+ readonly discoverModels?: (ctx: {
43
+ readonly smithyDir: string;
44
+ }) => Effect.Effect<
45
+ ReadonlyArray<{ readonly file: string; readonly dir: string }>,
46
+ never,
47
+ FileSystem.FileSystem | Path.Path
48
+ >;
49
+ /**
50
+ * Side effects to run before generating — e.g. AWS copies `partitions.json`
51
+ * out of the smithy submodule, runtime data its endpoint resolver reads.
52
+ */
53
+ readonly prepare?: (ctx: {
54
+ readonly root: string;
55
+ readonly outDir: string;
56
+ }) => Effect.Effect<void, never, FileSystem.FileSystem | Path.Path>;
57
+ /**
58
+ * The output module's name, when it isn't the model's filename. AWS names
59
+ * modules after the service's `aws.api#service` sdkId (`amazon-s3.json` →
60
+ * `s3.ts`), so the public surface reads `AWS.S3.getObject`.
61
+ */
62
+ readonly resourceName?: (ctx: {
63
+ readonly model: any;
64
+ readonly file: string;
65
+ }) => string;
66
+ /** Barrel export name for a resource. Default: the resource name itself. */
67
+ readonly barrelExportName?: (resource: string) => string;
68
+ /**
69
+ * The closing pass over the output directory. Defaults to formatting;
70
+ * AWS lint-fixes first (see `codegen/format.ts`).
71
+ */
72
+ readonly finalize?: (dir: string) => Effect.Effect<void>;
73
+ /**
74
+ * Keep generating after a model fails, instead of stopping at the first.
75
+ * AWS compiles 430 services in one run, and one broken model shouldn't
76
+ * hide the state of the other 429. The run still FAILS at the end — the
77
+ * failures are reported together rather than swallowed.
78
+ */
79
+ readonly continueOnModelError?: boolean;
80
+ /** Directory of hand-authored models merged after the generated ones. */
81
+ readonly manualSpecsDir?: string;
82
+ /**
83
+ * RFC-6902 patches apply in convert, never here. Only `false` is accepted
84
+ * so a string is a type error rather than a silently ignored setting.
85
+ */
86
+ readonly patchesDir?: false;
87
+ /**
88
+ * Model transform applied before generation (e.g. AWS dropping
89
+ * unreachable foreign-namespace shapes from a vendored model). Does not
90
+ * write back to `.generated-specs` — convert owns the committed model.
91
+ */
92
+ readonly transformModel?: (model: any, resource: string) => string | void;
93
+ /** The provider spec — built per model (metadata may vary per model). */
94
+ readonly spec: (model: any) => SdkSpec;
95
+ }
96
+
97
+ /** Run the generator CLI (BunRuntime main — call at module top level). */
98
+ export const runGeneratorCli = (options: GeneratorCliOptions): void => {
99
+ const command = Command.make(
100
+ "generate",
101
+ {
102
+ smithy: Flag.String("smithy").pipe(
103
+ Flag.withDefault(options.smithyDir ?? ".generated-specs"),
104
+ Flag.withDescription("Directory of Smithy JSON models"),
105
+ ),
106
+ out: Flag.String("out").pipe(
107
+ Flag.withDefault(options.outDir ?? "src/services"),
108
+ Flag.withDescription("Output directory for generated service modules"),
109
+ ),
110
+ resource: Flag.String("resource").pipe(
111
+ Flag.withDefault(""),
112
+ Flag.withDescription("Only generate this resource (e.g. ai)"),
113
+ ),
114
+ },
115
+ (config) =>
116
+ Effect.gen(function* () {
117
+ const fs = yield* FileSystem.FileSystem;
118
+ const path = yield* Path.Path;
119
+ const root = options.root;
120
+ const smithyDir = path.resolve(root, config.smithy);
121
+ const outDir = path.resolve(root, config.out);
122
+
123
+ yield* Console.log("⚙️ generate");
124
+ yield* Console.log(` Smithy: ${smithyDir}`);
125
+ yield* Console.log(` Output: ${outDir}`);
126
+
127
+ yield* options.prepare?.({ root, outDir }) ?? Effect.void;
128
+
129
+ // Generated models plus optional manual-specs (hand-authored models
130
+ // for APIs the provider's spec source doesn't cover). A manual model
131
+ // must not shadow a generated one.
132
+ const generated = options.discoverModels
133
+ ? yield* options.discoverModels({ smithyDir })
134
+ : (yield* fs.readDirectory(smithyDir))
135
+ .filter(
136
+ (f) =>
137
+ f.endsWith(".json") && !(options.excludeModel?.(f) ?? false),
138
+ )
139
+ .map((f) => ({ file: f, dir: smithyDir }));
140
+ const manualDir = options.manualSpecsDir
141
+ ? path.resolve(root, options.manualSpecsDir)
142
+ : undefined;
143
+ const manual =
144
+ manualDir && (yield* fs.exists(manualDir))
145
+ ? (yield* fs.readDirectory(manualDir))
146
+ .filter((f) => f.endsWith(".json"))
147
+ .map((f) => ({ file: f, dir: manualDir }))
148
+ : [];
149
+ for (const m of manual) {
150
+ if (generated.some((g) => g.file === m.file)) {
151
+ return yield* Effect.die(
152
+ new Error(
153
+ `${options.manualSpecsDir}/${m.file} shadows a generated model — rename or delete it`,
154
+ ),
155
+ );
156
+ }
157
+ }
158
+ const entries = [...generated, ...manual].sort((a, b) =>
159
+ a.file.localeCompare(b.file),
160
+ );
161
+
162
+ yield* fs.makeDirectory(outDir, { recursive: true });
163
+
164
+ const written: string[] = [];
165
+ const failedModels: string[] = [];
166
+ let totalOps = 0;
167
+
168
+ for (const { file, dir } of entries) {
169
+ const model = JSON.parse(
170
+ yield* fs.readFileString(path.join(dir, file)),
171
+ );
172
+ // The module's name — the model's filename unless the provider
173
+ // derives it from the model itself (AWS: the service's sdkId).
174
+ const resource =
175
+ options.resourceName?.({ model, file }) ??
176
+ file.replace(/\.json$/, "");
177
+ if (config.resource && resource !== config.resource) continue;
178
+
179
+ if (options.transformModel) {
180
+ const note = options.transformModel(model, resource);
181
+ if (note) yield* Console.log(` ${note}`);
182
+ }
183
+
184
+ // `generateService` and the provider spec are synchronous and
185
+ // throw; with continueOnModelError the throw is recorded and the
186
+ // run moves on, so one broken model doesn't hide the state of
187
+ // every model after it. The run still fails at the end.
188
+ let generated;
189
+ try {
190
+ generated = generateService(model, options.spec(model));
191
+ } catch (e) {
192
+ if (!options.continueOnModelError) throw e;
193
+ failedModels.push(
194
+ `${resource}: ${e instanceof Error ? e.message : String(e)}`,
195
+ );
196
+ yield* Console.error(`❌ ${resource}`);
197
+ continue;
198
+ }
199
+ const { code, operations } = generated;
200
+ if (operations === 0) continue;
201
+
202
+ yield* fs.writeFileString(path.join(outDir, `${resource}.ts`), code);
203
+ written.push(resource);
204
+ totalOps += operations;
205
+ }
206
+
207
+ // Barrel — namespace per resource to avoid op-name collisions.
208
+ //
209
+ // A FULL run is authoritative: the barrel is exactly what was
210
+ // generated, so a resource whose model went away drops out of it.
211
+ //
212
+ // A `--resource` run only knows about the resource it generated, so
213
+ // rewriting the barrel from that would delete every other export —
214
+ // which is how #397 shipped a cloudflare barrel exporting `workers`
215
+ // and nothing else, silently dropping 119 services. Merge into the
216
+ // existing barrel instead, so `--resource` can still ADD a brand-new
217
+ // resource without removing anything.
218
+ const barrelPath = path.join(outDir, "index.ts");
219
+ const filtered = config.resource !== "";
220
+ // Sorted, not in generation order: a provider that discovers models
221
+ // in some other order (AWS walks Amazon's directory tree) would
222
+ // otherwise reshuffle the barrel on every run.
223
+ let resources = [...written].sort((a, b) =>
224
+ `${a}.json`.localeCompare(`${b}.json`),
225
+ );
226
+ if (filtered && (yield* fs.exists(barrelPath))) {
227
+ // Recover the RESOURCE from each export line's path, not its name:
228
+ // the two differ when barrelExportName renames (AWS exports
229
+ // `S3` from `./s3.ts`).
230
+ const existing = (yield* fs.readFileString(barrelPath))
231
+ .split("\n")
232
+ .map(
233
+ (line) =>
234
+ /^export \* as \S+ from "\.\/(.+)\.ts";/.exec(line)?.[1],
235
+ )
236
+ .filter((name): name is string => name !== undefined);
237
+ // Ordered exactly as a full run orders it: by MODEL FILENAME, so
238
+ // the `.json` takes part in the collation (`ai_gateway.json` sorts
239
+ // before `ai.json`, where the bare names sort the other way).
240
+ // Otherwise a filtered run reshuffles the barrel into diff noise.
241
+ resources = [...new Set([...existing, ...written])].sort((a, b) =>
242
+ `${a}.json`.localeCompare(`${b}.json`),
243
+ );
244
+ }
245
+
246
+ yield* fs.writeFileString(
247
+ barrelPath,
248
+ barrel(
249
+ `// AUTO-GENERATED by scripts/generate.ts. Do not edit.\n`,
250
+ resources.map((r) => ({
251
+ name: options.barrelExportName?.(r) ?? r,
252
+ path: `./${r}.ts`,
253
+ })),
254
+ ),
255
+ );
256
+
257
+ yield* (options.finalize ?? formatGenerated)(outDir);
258
+
259
+ yield* Console.log(
260
+ `\n✅ Generated ${totalOps} operations across ${written.length} resource modules.`,
261
+ );
262
+ yield* Console.log(` ${path.join(outDir, "index.ts")}`);
263
+
264
+ // Models that failed under continueOnModelError. Reported together,
265
+ // at the end, and the run FAILS — a generate that couldn't produce
266
+ // a module must not exit 0 with the failure buried in the log.
267
+ if (failedModels.length) {
268
+ yield* Console.error(
269
+ `\n❌ ${failedModels.length} model(s) failed to generate:`,
270
+ );
271
+ for (const f of failedModels) yield* Console.error(` ${f}`);
272
+ return yield* Effect.die(
273
+ new Error(`${failedModels.length} model(s) failed to generate`),
274
+ );
275
+ }
276
+ }),
277
+ ).pipe(Command.withDescription(options.description));
278
+
279
+ BunRuntime.runMain(
280
+ Effect.provide(
281
+ Command.run(command, { version: "1.0.0" }),
282
+ BunServices.layer,
283
+ ),
284
+ );
285
+ };
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Shared emission idioms for the SDK generators (dev-time only).
3
+ *
4
+ * Both SDK generators emit the same compile-time-performance pattern
5
+ * (ported from distilled PR #360):
6
+ *
7
+ * export interface X { … } // hand-emitted type
8
+ * export const X = S.suspend(() => S.Struct({…}))
9
+ * .annotate({ identifier: "X" })
10
+ * as any as S.Schema<X>; // no inference needed
11
+ *
12
+ * plus closed-alias string-union enums, `S.TaggedError` error classes, and
13
+ * `API.make(() => ({ … }))` operation consts. The helpers here own those
14
+ * shared skeletons; providers own the content strings (member pipes, trait
15
+ * calls, config fields). Emitted output is normalized by oxfmt afterwards,
16
+ * so helpers emit canonical token streams rather than matching historical
17
+ * whitespace.
18
+ */
19
+ import { q } from "./naming.ts";
20
+
21
+ /** PURE annotates calls, never property reads. Both @ and # forms are supported. */
22
+ export const PURE = "/*@__PURE__*/ ";
23
+
24
+ /** `export interface X { … }` (or the empty-body form). */
25
+ export const interfaceDecl = (name: string, fields: string[]): string =>
26
+ fields.length
27
+ ? `export interface ${name} {\n${fields.join("\n")}\n}`
28
+ : `export interface ${name} {}`;
29
+
30
+ export interface SuspendConstOptions {
31
+ readonly name: string;
32
+ /** The schema expression inside the suspend thunk. */
33
+ readonly expr: string;
34
+ /** PURE marker(s), e.g. `"/*@__PURE__*​/ "`. Emitted verbatim before the expression. */
35
+ readonly pure?: string;
36
+ /** When set, `.annotate({ identifier: <name> })` is appended after the suspend. */
37
+ readonly annotateIdentifier?: boolean;
38
+ /** Extra annotation object source to use instead of the identifier default. */
39
+ readonly annotation?: string;
40
+ /**
41
+ * Explicit thunk return type (`(): S.Schema<X> =>`) — used for shapes in
42
+ * dependency cycles to stop circular type inference.
43
+ */
44
+ readonly thunkType?: string;
45
+ /** CF-style multiline body (`S.suspend(() =>\n<expr>,\n)`). */
46
+ readonly multiline?: boolean;
47
+ /** The cast target; defaults to `S.Schema<name>`. */
48
+ readonly castTo?: string;
49
+ }
50
+
51
+ /**
52
+ * The `export const X = S.suspend(…) … as any as S.Schema<X>;` skeleton
53
+ * shared by both generators.
54
+ */
55
+ export const suspendConst = (o: SuspendConstOptions): string => {
56
+ const cast = o.castTo ?? `S.Schema<${o.name}>`;
57
+ const thunk = o.thunkType ? `(): ${o.thunkType} =>` : `() =>`;
58
+ const suspend = o.multiline
59
+ ? `S.suspend(${thunk}\n${o.expr},\n)`
60
+ : `S.suspend(${thunk} ${o.expr})`;
61
+ const annotate = o.annotation
62
+ ? `.annotate(${o.annotation})`
63
+ : o.annotateIdentifier
64
+ ? `.annotate({ identifier: ${q(o.name)} })`
65
+ : "";
66
+ return `export const ${o.name} = ${o.pure ?? ""}${suspend}${annotate} as any as ${cast};\n`;
67
+ };
68
+
69
+ /**
70
+ * Member-level lazy reference: `S.suspend(() => X).annotate({ identifier })`.
71
+ * The `typed` form adds an explicit `S.Schema<X>` thunk return type — used
72
+ * for references into dependency cycles to stop circular type inference.
73
+ */
74
+ export const suspendRef = (name: string, typed = false): string =>
75
+ typed
76
+ ? `S.suspend((): S.Schema<${name}> => ${name}).annotate({ identifier: ${q(name)} })`
77
+ : `S.suspend(() => ${name}).annotate({ identifier: ${q(name)} })`;
78
+
79
+ export interface EnumDeclOptions {
80
+ readonly name: string;
81
+ readonly values: readonly string[];
82
+ readonly pure?: string;
83
+ /** The schema const expression; both SDKs use `S.String` (open enums). */
84
+ readonly schemaExpr?: string;
85
+ }
86
+
87
+ /**
88
+ * String-union enum ALIAS: the spec's documented values as a CLOSED literal
89
+ * union (`type X = "a" | "b"`) — response readers match documented values
90
+ * exhaustively. INPUT references re-open the alias inline
91
+ * (`X | (string & {})`) so consumers can send tomorrow's values without an
92
+ * SDK update. The schema stays `S.String` in both directions (the
93
+ * protocols never validate enum membership, so undocumented wire values
94
+ * always pass through at runtime).
95
+ */
96
+ export const enumDecl = (o: EnumDeclOptions): string[] => {
97
+ const union = o.values.length ? o.values.map(q).join(" | ") : "string";
98
+ return [
99
+ `export type ${o.name} = ${union};`,
100
+ `export const ${o.name} = ${o.schemaExpr ? `${o.pure ?? ""}${o.schemaExpr}` : "S.String"};\n`,
101
+ ];
102
+ };
103
+
104
+ export interface ErrorClassOptions {
105
+ readonly name: string;
106
+ /** The error tag; defaults to `name`. */
107
+ readonly tag?: string;
108
+ /** Field lines (` key: S.String,`). */
109
+ readonly fields: readonly string[];
110
+ /** Optional extra argument(s) after the fields object (annotations). */
111
+ readonly annotations?: string;
112
+ /** `.pipe(…)` suffix (e.g. category decorators). */
113
+ readonly pipes?: string;
114
+ /** Wrap the class expression (e.g. `T.applyErrorMatchers(<cls>, …)`). */
115
+ readonly wrap?: (cls: string) => string;
116
+ }
117
+
118
+ /**
119
+ * `export class X extends /*@__PURE__*​/ S.TaggedError<X>()("X", { … }) {}`
120
+ *
121
+ * The PURE markers are what make an unused error class droppable. A class
122
+ * whose heritage clause is an unannotated call can never be tree-shaken —
123
+ * the bundler has to assume the call has side effects — so a consumer
124
+ * importing one operation would retain every error class in the module
125
+ * (distilled #191).
126
+ *
127
+ * `wrap` needs its own marker as well as the inner one: a pure call's
128
+ * ARGUMENTS are still evaluated, so annotating only
129
+ * `T.applyErrorMatchers(S.TaggedError…(…), […])` leaves the inner call
130
+ * holding the class alive. Verified against esbuild in both directions.
131
+ */
132
+ export const errorClass = (o: ErrorClassOptions): string => {
133
+ const annotations = o.annotations ? `,\n${o.annotations}` : "";
134
+ const cls = `${PURE}S.TaggedError<${o.name}>()(${q(o.tag ?? o.name)}, {\n${o.fields.join("\n")}\n}${annotations})${o.pipes ?? ""}`;
135
+ const body = o.wrap ? `${PURE}${o.wrap(cls)}` : cls;
136
+ return `export class ${o.name} extends ${body} {}\n`;
137
+ };
138
+
139
+ export interface OperationConstOptions {
140
+ /** The exported (usually lowerFirst) operation name. */
141
+ readonly exportName: string;
142
+ /** Full type annotation (e.g. `API.OperationMethod<A, B, E, R>`). */
143
+ readonly typeAnnotation: string;
144
+ /** `API.make` or `API.makePaginated` (with namespace prefix). */
145
+ readonly factory: string;
146
+ /** The config object source (including braces). */
147
+ readonly config: string;
148
+ /** Optional extra factory argument (e.g. a pagination strategy). */
149
+ readonly extraArg?: string;
150
+ readonly pure?: string;
151
+ /**
152
+ * Widen the factory result to `any`, for annotations the factory's
153
+ * generic signature can't prove. The one case today: a paginated
154
+ * operation's `items` element type comes from the pagination trait's
155
+ * `items` PATH — a runtime string — so the factory can only infer the
156
+ * structural fallback while the annotation names the real element type.
157
+ *
158
+ * Unlike the schema consts' `as any as S.Schema<X>`, the target doesn't
159
+ * need restating here: the const carries its own annotation, which IS
160
+ * the assignment target, so a bare `as any` lands in the same place.
161
+ * Restating it would double every paginated operation's declaration —
162
+ * ~40k lines across the SDKs — for no added checking.
163
+ */
164
+ readonly castToAnnotation?: boolean;
165
+ }
166
+
167
+ /** `export const op: T = API.make(() => ({ … }));` */
168
+ export const operationConst = (o: OperationConstOptions): string =>
169
+ `export const ${o.exportName}: ${o.typeAnnotation} = ${o.pure ?? ""}${o.factory}(() => (${o.config})${
170
+ o.extraArg ? `, ${o.extraArg}` : ""
171
+ })${o.castToAnnotation ? ` as any` : ""};\n`;
172
+
173
+ import { tsKey } from "./naming.ts";
174
+
175
+ export interface InterfaceFieldOptions {
176
+ readonly name: string;
177
+ readonly type: string;
178
+ readonly optional: boolean;
179
+ readonly doc?: string;
180
+ }
181
+
182
+ /** Interface field line(s): optional doc comment + ` name?: Type;`. */
183
+ export const interfaceField = (o: InterfaceFieldOptions): string[] => [
184
+ ...(o.doc ? [` /** ${o.doc} */`] : []),
185
+ ` ${tsKey(o.name)}${o.optional ? "?" : ""}: ${o.type};`,
186
+ ];
187
+
188
+ /** `export type <Op>Error = A | B | <CommonErrors>;` */
189
+ export const errorUnionAlias = (
190
+ opName: string,
191
+ errorNames: readonly string[],
192
+ commonRef: string,
193
+ ): string =>
194
+ `export type ${opName}Error = ${[...errorNames, commonRef].join(" | ")};`;
195
+
196
+ /** Namespaced barrel: `export * as name from "./file.ts";` per entry. */
197
+ export const barrel = (
198
+ header: string,
199
+ entries: ReadonlyArray<{ name: string; path: string }>,
200
+ ): string =>
201
+ header +
202
+ entries.map((e) => `export * as ${e.name} from ${q(e.path)};`).join("\n") +
203
+ "\n";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Post-generation formatting (dev-time only).
3
+ *
4
+ * The emitters produce a canonical token stream, not formatted source, so
5
+ * every generator formats what it wrote before finishing. Without it a
6
+ * generate run leaves the whole output directory dirty against the committed
7
+ * (formatted) files, and a real regression is indistinguishable from
8
+ * whitespace in the diff.
9
+ *
10
+ * Lives here so the shared {@link runGeneratorCli} and the providers with
11
+ * their own pipelines run the identical step.
12
+ */
13
+ import { Console, Effect } from "effect";
14
+
15
+ /** Run a dev-time tool, failing the generate run if it does. */
16
+ export const runTool = (
17
+ argv: readonly string[],
18
+ ): Effect.Effect<void, never, never> =>
19
+ Effect.tryPromise({
20
+ try: () =>
21
+ Bun.spawn([...argv], { stdout: "inherit", stderr: "inherit" }).exited,
22
+ catch: (cause) => new Error(`${argv[0]} failed to start: ${cause}`),
23
+ }).pipe(
24
+ Effect.flatMap((code) =>
25
+ code === 0
26
+ ? Effect.void
27
+ : Effect.die(new Error(`${argv.join(" ")} exited with ${code}`)),
28
+ ),
29
+ Effect.catchCause((cause) => Effect.die(cause)),
30
+ );
31
+
32
+ /** Format a generated directory in place. */
33
+ export const formatGenerated = (dir: string) =>
34
+ Effect.flatMap(Console.log(`\n🧹 Formatting ${dir}`), () =>
35
+ runTool(["bunx", "oxfmt", dir]),
36
+ );
37
+
38
+ /**
39
+ * Lint-fix then format. `oxlint --fix` can leave its rewrites unformatted,
40
+ * so the formatter has to run after it, not before.
41
+ */
42
+ export const lintAndFormatGenerated = (dir: string) =>
43
+ Effect.flatMap(Console.log(`\n🧹 Linting and formatting ${dir}`), () =>
44
+ Effect.flatMap(runTool(["bunx", "oxlint", "--fix", dir]), () =>
45
+ runTool(["bunx", "oxfmt", dir]),
46
+ ),
47
+ );