@distilled.cloud/core 1.0.0-rc.1 → 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.
- package/LICENSE +201 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +16 -4
- package/lib/api.js.map +1 -1
- package/lib/category.d.ts +4 -4
- package/lib/category.js +4 -4
- package/lib/codegen/boolean-string-enums.d.ts +36 -0
- package/lib/codegen/boolean-string-enums.d.ts.map +1 -0
- package/lib/codegen/boolean-string-enums.js +94 -0
- package/lib/codegen/boolean-string-enums.js.map +1 -0
- package/lib/codegen/boolean-string-enums.test.d.ts +2 -0
- package/lib/codegen/boolean-string-enums.test.d.ts.map +1 -0
- package/lib/codegen/boolean-string-enums.test.js +147 -0
- package/lib/codegen/boolean-string-enums.test.js.map +1 -0
- package/lib/codegen/cli.d.ts +56 -6
- package/lib/codegen/cli.d.ts.map +1 -1
- package/lib/codegen/cli.js +59 -75
- package/lib/codegen/cli.js.map +1 -1
- package/lib/codegen/emit.d.ts +3 -7
- package/lib/codegen/emit.d.ts.map +1 -1
- package/lib/codegen/emit.js +6 -10
- package/lib/codegen/emit.js.map +1 -1
- package/lib/codegen/generator.d.ts.map +1 -1
- package/lib/codegen/generator.js +142 -20
- package/lib/codegen/generator.js.map +1 -1
- package/lib/codegen/graphql-client.d.ts +62 -0
- package/lib/codegen/graphql-client.d.ts.map +1 -0
- package/lib/codegen/graphql-client.js +294 -0
- package/lib/codegen/graphql-client.js.map +1 -0
- package/lib/codegen/graphql-client.test.d.ts +2 -0
- package/lib/codegen/graphql-client.test.d.ts.map +1 -0
- package/lib/codegen/graphql-client.test.js +311 -0
- package/lib/codegen/graphql-client.test.js.map +1 -0
- package/lib/codegen/graphql.d.ts +207 -0
- package/lib/codegen/graphql.d.ts.map +1 -0
- package/lib/codegen/graphql.js +799 -0
- package/lib/codegen/graphql.js.map +1 -0
- package/lib/codegen/openapi-cli.d.ts +17 -3
- package/lib/codegen/openapi-cli.d.ts.map +1 -1
- package/lib/codegen/openapi-cli.js +50 -48
- package/lib/codegen/openapi-cli.js.map +1 -1
- package/lib/codegen/openapi.d.ts +69 -6
- package/lib/codegen/openapi.d.ts.map +1 -1
- package/lib/codegen/openapi.js +173 -16
- package/lib/codegen/openapi.js.map +1 -1
- package/lib/codegen/patches.d.ts +65 -0
- package/lib/codegen/patches.d.ts.map +1 -0
- package/lib/codegen/patches.js +236 -0
- package/lib/codegen/patches.js.map +1 -0
- package/lib/codegen/patches.test.d.ts +2 -0
- package/lib/codegen/patches.test.d.ts.map +1 -0
- package/lib/codegen/patches.test.js +105 -0
- package/lib/codegen/patches.test.js.map +1 -0
- package/lib/codegen/proto.d.ts +121 -0
- package/lib/codegen/proto.d.ts.map +1 -0
- package/lib/codegen/proto.js +962 -0
- package/lib/codegen/proto.js.map +1 -0
- package/lib/codegen/rewrite-operation-ids.d.ts +131 -0
- package/lib/codegen/rewrite-operation-ids.d.ts.map +1 -0
- package/lib/codegen/rewrite-operation-ids.js +1079 -0
- package/lib/codegen/rewrite-operation-ids.js.map +1 -0
- package/lib/codegen/rewrite-operation-ids.test.d.ts +2 -0
- package/lib/codegen/rewrite-operation-ids.test.d.ts.map +1 -0
- package/lib/codegen/rewrite-operation-ids.test.js +533 -0
- package/lib/codegen/rewrite-operation-ids.test.js.map +1 -0
- package/lib/codegen/spec-path.d.ts +16 -0
- package/lib/codegen/spec-path.d.ts.map +1 -0
- package/lib/codegen/spec-path.js +101 -0
- package/lib/codegen/spec-path.js.map +1 -0
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +17 -13
- package/lib/errors.js.map +1 -1
- package/lib/graphql.d.ts +284 -0
- package/lib/graphql.d.ts.map +1 -0
- package/lib/graphql.fixture.d.ts +249 -0
- package/lib/graphql.fixture.d.ts.map +1 -0
- package/lib/graphql.fixture.js +240 -0
- package/lib/graphql.fixture.js.map +1 -0
- package/lib/graphql.js +718 -0
- package/lib/graphql.js.map +1 -0
- package/lib/graphql.test.d.ts +2 -0
- package/lib/graphql.test.d.ts.map +1 -0
- package/lib/graphql.test.js +780 -0
- package/lib/graphql.test.js.map +1 -0
- package/lib/graphql.types.d.ts +2 -0
- package/lib/graphql.types.d.ts.map +1 -0
- package/lib/graphql.types.js +45 -0
- package/lib/graphql.types.js.map +1 -0
- package/lib/json-patch.d.ts +18 -11
- package/lib/json-patch.d.ts.map +1 -1
- package/lib/json-patch.js +63 -25
- package/lib/json-patch.js.map +1 -1
- package/lib/pagination.d.ts +43 -3
- package/lib/pagination.d.ts.map +1 -1
- package/lib/pagination.js +91 -5
- package/lib/pagination.js.map +1 -1
- package/lib/protocol-http.d.ts.map +1 -1
- package/lib/protocol-http.js +62 -26
- package/lib/protocol-http.js.map +1 -1
- package/lib/protocol-http.test.d.ts +2 -0
- package/lib/protocol-http.test.d.ts.map +1 -0
- package/lib/protocol-http.test.js +88 -0
- package/lib/protocol-http.test.js.map +1 -0
- package/lib/protocol-rest.d.ts +12 -2
- package/lib/protocol-rest.d.ts.map +1 -1
- package/lib/protocol-rest.js +17 -3
- package/lib/protocol-rest.js.map +1 -1
- package/lib/retry.js +1 -1
- package/lib/schema.d.ts +1 -1
- package/lib/schema.d.ts.map +1 -1
- package/lib/schema.js +1 -1
- package/lib/schema.js.map +1 -1
- package/lib/trait.d.ts +27 -3
- package/lib/trait.d.ts.map +1 -1
- package/lib/trait.js +17 -1
- package/lib/trait.js.map +1 -1
- package/package.json +13 -10
- package/src/api.ts +18 -4
- package/src/category.ts +4 -4
- package/src/codegen/boolean-string-enums.test.ts +168 -0
- package/src/codegen/boolean-string-enums.ts +106 -0
- package/src/codegen/cli.ts +127 -110
- package/src/codegen/emit.ts +6 -10
- package/src/codegen/generator.ts +151 -21
- package/src/codegen/graphql-client.test.ts +386 -0
- package/src/codegen/graphql-client.ts +419 -0
- package/src/codegen/graphql.ts +1217 -0
- package/src/codegen/openapi-cli.ts +75 -59
- package/src/codegen/openapi.ts +255 -16
- package/src/codegen/patches.test.ts +130 -0
- package/src/codegen/patches.ts +291 -0
- package/src/codegen/proto.ts +1128 -0
- package/src/codegen/rewrite-operation-ids.test.ts +563 -0
- package/src/codegen/rewrite-operation-ids.ts +1206 -0
- package/src/codegen/spec-path.ts +115 -0
- package/src/errors.ts +20 -25
- package/src/graphql.fixture.ts +371 -0
- package/src/graphql.test.ts +974 -0
- package/src/graphql.ts +1321 -0
- package/src/graphql.types.ts +185 -0
- package/src/json-patch.ts +82 -25
- package/src/pagination.ts +134 -7
- package/src/protocol-http.test.ts +107 -0
- package/src/protocol-http.ts +67 -31
- package/src/protocol-rest.ts +28 -4
- package/src/retry.ts +1 -1
- package/src/schema.ts +2 -2
- package/src/trait.ts +39 -3
package/src/codegen/cli.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The generic generator CLI harness (dev-time only).
|
|
3
3
|
*
|
|
4
|
-
* Owns the smithy
|
|
5
|
-
* directory (plus optional hand-authored manual specs),
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* service modules and the namespaced barrel.
|
|
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.
|
|
10
9
|
*
|
|
11
10
|
* A provider's `scripts/generate.ts` is: trait consts + an SdkSpec + a
|
|
12
11
|
* `runGeneratorCli` call.
|
|
@@ -17,11 +16,6 @@ import * as FileSystem from "effect/FileSystem";
|
|
|
17
16
|
import * as Path from "effect/Path";
|
|
18
17
|
import { Flag } from "effect/unstable/cli";
|
|
19
18
|
import { Command } from "effect/unstable/cli";
|
|
20
|
-
import {
|
|
21
|
-
applyOperation,
|
|
22
|
-
isStaleTargetError,
|
|
23
|
-
type PatchFile,
|
|
24
|
-
} from "../json-patch.ts";
|
|
25
19
|
import { barrel } from "./emit.ts";
|
|
26
20
|
import { formatGenerated } from "./format.ts";
|
|
27
21
|
import { generateService, type SdkSpec } from "./generator.ts";
|
|
@@ -37,15 +31,63 @@ export interface GeneratorCliOptions {
|
|
|
37
31
|
readonly outDir?: string;
|
|
38
32
|
/** Model files to skip (e.g. a shared protocols model). */
|
|
39
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;
|
|
40
80
|
/** Directory of hand-authored models merged after the generated ones. */
|
|
41
81
|
readonly manualSpecsDir?: string;
|
|
42
|
-
/** RFC-6902 patch chain root. Default `patches`; `false` disables. */
|
|
43
|
-
readonly patchesDir?: string | false;
|
|
44
82
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
|
|
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.
|
|
49
91
|
*/
|
|
50
92
|
readonly transformModel?: (model: any, resource: string) => string | void;
|
|
51
93
|
/** The provider spec — built per model (metadata may vary per model). */
|
|
@@ -57,15 +99,15 @@ export const runGeneratorCli = (options: GeneratorCliOptions): void => {
|
|
|
57
99
|
const command = Command.make(
|
|
58
100
|
"generate",
|
|
59
101
|
{
|
|
60
|
-
smithy: Flag.
|
|
102
|
+
smithy: Flag.String("smithy").pipe(
|
|
61
103
|
Flag.withDefault(options.smithyDir ?? ".generated-specs"),
|
|
62
104
|
Flag.withDescription("Directory of Smithy JSON models"),
|
|
63
105
|
),
|
|
64
|
-
out: Flag.
|
|
106
|
+
out: Flag.String("out").pipe(
|
|
65
107
|
Flag.withDefault(options.outDir ?? "src/services"),
|
|
66
108
|
Flag.withDescription("Output directory for generated service modules"),
|
|
67
109
|
),
|
|
68
|
-
resource: Flag.
|
|
110
|
+
resource: Flag.String("resource").pipe(
|
|
69
111
|
Flag.withDefault(""),
|
|
70
112
|
Flag.withDescription("Only generate this resource (e.g. ai)"),
|
|
71
113
|
),
|
|
@@ -82,14 +124,19 @@ export const runGeneratorCli = (options: GeneratorCliOptions): void => {
|
|
|
82
124
|
yield* Console.log(` Smithy: ${smithyDir}`);
|
|
83
125
|
yield* Console.log(` Output: ${outDir}`);
|
|
84
126
|
|
|
127
|
+
yield* options.prepare?.({ root, outDir }) ?? Effect.void;
|
|
128
|
+
|
|
85
129
|
// Generated models plus optional manual-specs (hand-authored models
|
|
86
130
|
// for APIs the provider's spec source doesn't cover). A manual model
|
|
87
131
|
// must not shadow a generated one.
|
|
88
|
-
const generated =
|
|
89
|
-
.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 }));
|
|
93
140
|
const manualDir = options.manualSpecsDir
|
|
94
141
|
? path.resolve(root, options.manualSpecsDir)
|
|
95
142
|
: undefined;
|
|
@@ -115,84 +162,41 @@ export const runGeneratorCli = (options: GeneratorCliOptions): void => {
|
|
|
115
162
|
yield* fs.makeDirectory(outDir, { recursive: true });
|
|
116
163
|
|
|
117
164
|
const written: string[] = [];
|
|
165
|
+
const failedModels: string[] = [];
|
|
118
166
|
let totalOps = 0;
|
|
119
|
-
let totalPatches = 0;
|
|
120
|
-
let staleOps = 0;
|
|
121
|
-
const badPatches: string[] = [];
|
|
122
|
-
|
|
123
|
-
// Orphan check: a patch directory matching no smithy model would be
|
|
124
|
-
// silently dropped — flag it instead.
|
|
125
|
-
const patchRoot =
|
|
126
|
-
options.patchesDir === false
|
|
127
|
-
? undefined
|
|
128
|
-
: path.join(root, options.patchesDir ?? "patches");
|
|
129
|
-
if (patchRoot && (yield* fs.exists(patchRoot))) {
|
|
130
|
-
const resources = new Set(
|
|
131
|
-
entries.map((e) => e.file.replace(/\.json$/, "")),
|
|
132
|
-
);
|
|
133
|
-
for (const dir of yield* fs.readDirectory(patchRoot)) {
|
|
134
|
-
if (!resources.has(dir)) {
|
|
135
|
-
yield* Console.warn(
|
|
136
|
-
`⚠️ patches/${dir}/ matches no smithy model — orphaned?`,
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
167
|
|
|
142
168
|
for (const { file, dir } of entries) {
|
|
143
|
-
const resource = file.replace(/\.json$/, "");
|
|
144
|
-
if (config.resource && resource !== config.resource) continue;
|
|
145
|
-
|
|
146
169
|
const model = JSON.parse(
|
|
147
170
|
yield* fs.readFileString(path.join(dir, file)),
|
|
148
171
|
);
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
const patchFiles = (yield* fs.readDirectory(patchDir))
|
|
156
|
-
.filter((f) => f.endsWith(".json"))
|
|
157
|
-
.sort(
|
|
158
|
-
(a, b) =>
|
|
159
|
-
Number(a.endsWith(".manual.json")) -
|
|
160
|
-
Number(b.endsWith(".manual.json")) || a.localeCompare(b),
|
|
161
|
-
);
|
|
162
|
-
for (const pf of patchFiles) {
|
|
163
|
-
const parsed = JSON.parse(
|
|
164
|
-
yield* fs.readFileString(path.join(patchDir, pf)),
|
|
165
|
-
) as PatchFile;
|
|
166
|
-
for (const patchOp of parsed.patches ?? []) {
|
|
167
|
-
try {
|
|
168
|
-
applyOperation(model, patchOp);
|
|
169
|
-
} catch (e) {
|
|
170
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
171
|
-
if (isStaleTargetError(msg)) {
|
|
172
|
-
staleOps++;
|
|
173
|
-
yield* Console.warn(
|
|
174
|
-
` ⚠️ stale: ${resource}/${pf} [${patchOp.op} ${patchOp.path}]`,
|
|
175
|
-
);
|
|
176
|
-
} else {
|
|
177
|
-
badPatches.push(
|
|
178
|
-
`${resource}/${pf} [${patchOp.op} ${patchOp.path}]: ${msg}`,
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
totalPatches++;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
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;
|
|
186
178
|
|
|
187
179
|
if (options.transformModel) {
|
|
188
180
|
const note = options.transformModel(model, resource);
|
|
189
181
|
if (note) yield* Console.log(` ${note}`);
|
|
190
182
|
}
|
|
191
183
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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;
|
|
196
200
|
if (operations === 0) continue;
|
|
197
201
|
|
|
198
202
|
yield* fs.writeFileString(path.join(outDir, `${resource}.ts`), code);
|
|
@@ -200,17 +204,6 @@ export const runGeneratorCli = (options: GeneratorCliOptions): void => {
|
|
|
200
204
|
totalOps += operations;
|
|
201
205
|
}
|
|
202
206
|
|
|
203
|
-
if (badPatches.length) {
|
|
204
|
-
for (const b of badPatches) {
|
|
205
|
-
yield* Console.error(`❌ bad patch: ${b}`);
|
|
206
|
-
}
|
|
207
|
-
return yield* Effect.die(
|
|
208
|
-
new Error(
|
|
209
|
-
`${badPatches.length} malformed patch operation(s) — fix or remove them`,
|
|
210
|
-
),
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
207
|
// Barrel — namespace per resource to avoid op-name collisions.
|
|
215
208
|
//
|
|
216
209
|
// A FULL run is authoritative: the barrel is exactly what was
|
|
@@ -224,11 +217,22 @@ export const runGeneratorCli = (options: GeneratorCliOptions): void => {
|
|
|
224
217
|
// resource without removing anything.
|
|
225
218
|
const barrelPath = path.join(outDir, "index.ts");
|
|
226
219
|
const filtered = config.resource !== "";
|
|
227
|
-
|
|
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
|
+
);
|
|
228
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`).
|
|
229
230
|
const existing = (yield* fs.readFileString(barrelPath))
|
|
230
231
|
.split("\n")
|
|
231
|
-
.map(
|
|
232
|
+
.map(
|
|
233
|
+
(line) =>
|
|
234
|
+
/^export \* as \S+ from "\.\/(.+)\.ts";/.exec(line)?.[1],
|
|
235
|
+
)
|
|
232
236
|
.filter((name): name is string => name !== undefined);
|
|
233
237
|
// Ordered exactly as a full run orders it: by MODEL FILENAME, so
|
|
234
238
|
// the `.json` takes part in the collation (`ai_gateway.json` sorts
|
|
@@ -243,19 +247,32 @@ export const runGeneratorCli = (options: GeneratorCliOptions): void => {
|
|
|
243
247
|
barrelPath,
|
|
244
248
|
barrel(
|
|
245
249
|
`// AUTO-GENERATED by scripts/generate.ts. Do not edit.\n`,
|
|
246
|
-
resources.map((r) => ({
|
|
250
|
+
resources.map((r) => ({
|
|
251
|
+
name: options.barrelExportName?.(r) ?? r,
|
|
252
|
+
path: `./${r}.ts`,
|
|
253
|
+
})),
|
|
247
254
|
),
|
|
248
255
|
);
|
|
249
256
|
|
|
250
|
-
yield* formatGenerated(outDir);
|
|
257
|
+
yield* (options.finalize ?? formatGenerated)(outDir);
|
|
251
258
|
|
|
252
259
|
yield* Console.log(
|
|
253
|
-
`\n✅ Generated ${totalOps} operations across ${written.length} resource modules
|
|
254
|
-
(totalPatches
|
|
255
|
-
? ` (${totalPatches} patch files applied${staleOps ? `, ${staleOps} stale op(s) skipped` : ""}).`
|
|
256
|
-
: "."),
|
|
260
|
+
`\n✅ Generated ${totalOps} operations across ${written.length} resource modules.`,
|
|
257
261
|
);
|
|
258
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
|
+
}
|
|
259
276
|
}),
|
|
260
277
|
).pipe(Command.withDescription(options.description));
|
|
261
278
|
|
package/src/codegen/emit.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* .annotate({ identifier: "X" })
|
|
10
10
|
* as any as S.Schema<X>; // no inference needed
|
|
11
11
|
*
|
|
12
|
-
* plus closed-alias string-union enums, `S.
|
|
12
|
+
* plus closed-alias string-union enums, `S.TaggedError` error classes, and
|
|
13
13
|
* `API.make(() => ({ … }))` operation consts. The helpers here own those
|
|
14
14
|
* shared skeletons; providers own the content strings (member pipes, trait
|
|
15
15
|
* calls, config fields). Emitted output is normalized by oxfmt afterwards,
|
|
@@ -18,11 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { q } from "./naming.ts";
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* The PURE annotation emitted before generated schema consts. A single
|
|
23
|
-
* `/*@__PURE__*/` — Rolldown 1.1+ warns on the `/*#__PURE__*/` form
|
|
24
|
-
* (distilled #374).
|
|
25
|
-
*/
|
|
21
|
+
/** PURE annotates calls, never property reads. Both @ and # forms are supported. */
|
|
26
22
|
export const PURE = "/*@__PURE__*/ ";
|
|
27
23
|
|
|
28
24
|
/** `export interface X { … }` (or the empty-body form). */
|
|
@@ -101,7 +97,7 @@ export const enumDecl = (o: EnumDeclOptions): string[] => {
|
|
|
101
97
|
const union = o.values.length ? o.values.map(q).join(" | ") : "string";
|
|
102
98
|
return [
|
|
103
99
|
`export type ${o.name} = ${union};`,
|
|
104
|
-
`export const ${o.name} = ${o.pure ?? ""}${o.schemaExpr
|
|
100
|
+
`export const ${o.name} = ${o.schemaExpr ? `${o.pure ?? ""}${o.schemaExpr}` : "S.String"};\n`,
|
|
105
101
|
];
|
|
106
102
|
};
|
|
107
103
|
|
|
@@ -120,7 +116,7 @@ export interface ErrorClassOptions {
|
|
|
120
116
|
}
|
|
121
117
|
|
|
122
118
|
/**
|
|
123
|
-
* `export class X extends /*@__PURE__*/ S.
|
|
119
|
+
* `export class X extends /*@__PURE__*/ S.TaggedError<X>()("X", { … }) {}`
|
|
124
120
|
*
|
|
125
121
|
* The PURE markers are what make an unused error class droppable. A class
|
|
126
122
|
* whose heritage clause is an unannotated call can never be tree-shaken —
|
|
@@ -130,12 +126,12 @@ export interface ErrorClassOptions {
|
|
|
130
126
|
*
|
|
131
127
|
* `wrap` needs its own marker as well as the inner one: a pure call's
|
|
132
128
|
* ARGUMENTS are still evaluated, so annotating only
|
|
133
|
-
* `T.applyErrorMatchers(S.
|
|
129
|
+
* `T.applyErrorMatchers(S.TaggedError…(…), […])` leaves the inner call
|
|
134
130
|
* holding the class alive. Verified against esbuild in both directions.
|
|
135
131
|
*/
|
|
136
132
|
export const errorClass = (o: ErrorClassOptions): string => {
|
|
137
133
|
const annotations = o.annotations ? `,\n${o.annotations}` : "";
|
|
138
|
-
const cls = `${PURE}S.
|
|
134
|
+
const cls = `${PURE}S.TaggedError<${o.name}>()(${q(o.tag ?? o.name)}, {\n${o.fields.join("\n")}\n}${annotations})${o.pipes ?? ""}`;
|
|
139
135
|
const body = o.wrap ? `${PURE}${o.wrap(cls)}` : cls;
|
|
140
136
|
return `export class ${o.name} extends ${body} {}\n`;
|
|
141
137
|
};
|
package/src/codegen/generator.ts
CHANGED
|
@@ -55,6 +55,10 @@ import {
|
|
|
55
55
|
} from "./operations.ts";
|
|
56
56
|
import { memberBases, smithyWireName } from "./members.ts";
|
|
57
57
|
import { validatePaginated } from "./pagination.ts";
|
|
58
|
+
import {
|
|
59
|
+
booleanStringEnums,
|
|
60
|
+
STRING_ENCODED_TRAIT,
|
|
61
|
+
} from "./boolean-string-enums.ts";
|
|
58
62
|
|
|
59
63
|
const PAGINATED_TRAIT = "smithy.api#paginated";
|
|
60
64
|
|
|
@@ -447,6 +451,9 @@ export const generateService = (
|
|
|
447
451
|
model: any,
|
|
448
452
|
spec: SdkSpec,
|
|
449
453
|
): GeneratedService => {
|
|
454
|
+
// `"true" | "false"` request members become real booleans that travel as
|
|
455
|
+
// their string spelling (see boolean-string-enums.ts).
|
|
456
|
+
booleanStringEnums(model);
|
|
450
457
|
const shapes: ShapeMap = model.shapes;
|
|
451
458
|
const pure = spec.pure ?? PURE;
|
|
452
459
|
const prelude = spec.prelude ?? JSON_PRELUDE;
|
|
@@ -487,8 +494,27 @@ export const generateService = (
|
|
|
487
494
|
const order = topoOrder(shapes, reachable, shapeDeps);
|
|
488
495
|
const indexOf = orderIndex(order);
|
|
489
496
|
|
|
490
|
-
|
|
491
|
-
|
|
497
|
+
/**
|
|
498
|
+
* Shape id → the id whose emitted body it shares.
|
|
499
|
+
*
|
|
500
|
+
* Structural aliasing (see the struct emission below) has to cascade: a
|
|
501
|
+
* parent only matches another parent if their MEMBERS render identically,
|
|
502
|
+
* and a member renders as its target's name. So every reference resolves
|
|
503
|
+
* through this map first — once `…ItemGroupRuleGroup` is aliased onto a
|
|
504
|
+
* canonical shape, the parents referencing it start rendering the same
|
|
505
|
+
* text and collapse in turn.
|
|
506
|
+
*
|
|
507
|
+
* Filled in topological order, so a canonical target is always a shape
|
|
508
|
+
* that was already emitted — a back-reference, never a forward one.
|
|
509
|
+
*/
|
|
510
|
+
const canonicalId = new Map<string, string>();
|
|
511
|
+
const canon = (target: string): string => canonicalId.get(target) ?? target;
|
|
512
|
+
|
|
513
|
+
const rawRef = makeSchemaRef(prelude, indexOf);
|
|
514
|
+
const rawTsRef = makeTsRef(tsPrelude);
|
|
515
|
+
const ref = (target: string, selfIdx: number) =>
|
|
516
|
+
rawRef(canon(target), selfIdx);
|
|
517
|
+
const tsRef = (target: string) => rawTsRef(canon(target));
|
|
492
518
|
|
|
493
519
|
// Direction classification for enum openness. Enum ALIASES are emitted
|
|
494
520
|
// CLOSED (exhaustively matchable on reads); request-reachable shapes
|
|
@@ -529,7 +555,8 @@ export const generateService = (
|
|
|
529
555
|
* pure response/error shapes stay the plain closed alias, and so do
|
|
530
556
|
* union-arm discriminant literals.
|
|
531
557
|
*/
|
|
532
|
-
const tsRefAt = (
|
|
558
|
+
const tsRefAt = (rawTarget: string, ownerId: string): string => {
|
|
559
|
+
const target = canon(rawTarget);
|
|
533
560
|
const base = tsRef(target);
|
|
534
561
|
if (!requestReachable.has(ownerId)) return base;
|
|
535
562
|
if (discriminantEnums.has(target)) return base;
|
|
@@ -605,6 +632,50 @@ export const generateService = (
|
|
|
605
632
|
};
|
|
606
633
|
});
|
|
607
634
|
|
|
635
|
+
/** The single value a one-value enum shape fixes, if `target` is one. */
|
|
636
|
+
const soleEnumValue = (target: string): string | undefined => {
|
|
637
|
+
const d = shapes[target];
|
|
638
|
+
if (d?.type !== "enum") return undefined;
|
|
639
|
+
const values = Object.values(d.members ?? {}).map(
|
|
640
|
+
(m: any) => m.traits?.["smithy.api#enumValue"],
|
|
641
|
+
);
|
|
642
|
+
return values.length === 1 && typeof values[0] === "string"
|
|
643
|
+
? values[0]
|
|
644
|
+
: undefined;
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* The member every case of a union pins to a different literal — the tag
|
|
649
|
+
* the API itself discriminates by (`type: "zone"`, `id: "browser_check"`).
|
|
650
|
+
* Undefined unless every case is a structure carrying that member and no
|
|
651
|
+
* two cases claim the same value.
|
|
652
|
+
*/
|
|
653
|
+
const unionDiscriminator = (
|
|
654
|
+
caseTargets: readonly string[],
|
|
655
|
+
): { key: string; values: string[] } | undefined => {
|
|
656
|
+
if (caseTargets.length < 2) return undefined;
|
|
657
|
+
const perCase = caseTargets.map((t) => {
|
|
658
|
+
const cd = shapes[t];
|
|
659
|
+
if (cd?.type !== "structure") return undefined;
|
|
660
|
+
const tags = new Map<string, string>();
|
|
661
|
+
for (const mi of memberInfos(cd)) {
|
|
662
|
+
const value = soleEnumValue(mi.target);
|
|
663
|
+
if (value !== undefined) tags.set(mi.tsName, value);
|
|
664
|
+
}
|
|
665
|
+
return tags;
|
|
666
|
+
});
|
|
667
|
+
if (perCase.some((tags) => tags === undefined || tags.size === 0)) {
|
|
668
|
+
return undefined;
|
|
669
|
+
}
|
|
670
|
+
for (const key of perCase[0]!.keys()) {
|
|
671
|
+
const values = perCase.map((tags) => tags!.get(key));
|
|
672
|
+
if (values.some((v) => v === undefined)) continue;
|
|
673
|
+
if (new Set(values).size !== values.length) continue;
|
|
674
|
+
return { key, values: values as string[] };
|
|
675
|
+
}
|
|
676
|
+
return undefined;
|
|
677
|
+
};
|
|
678
|
+
|
|
608
679
|
// Generic pipes for the smithy bindings (the SDK's traits module exports
|
|
609
680
|
// core's Label/Query/Header/HttpBody/Body builders under these names);
|
|
610
681
|
// provider bindings and member traits append theirs via memberExtraPipes.
|
|
@@ -648,6 +719,7 @@ export const generateService = (
|
|
|
648
719
|
([trait, builder]) =>
|
|
649
720
|
`${builder}(${JSON.stringify(info.traits[trait])})`,
|
|
650
721
|
),
|
|
722
|
+
...(STRING_ENCODED_TRAIT in info.traits ? ["T.StringEncoded()"] : []),
|
|
651
723
|
...(spec.memberExtraPipes?.(info) ?? []),
|
|
652
724
|
]);
|
|
653
725
|
|
|
@@ -731,6 +803,13 @@ export const generateService = (
|
|
|
731
803
|
|
|
732
804
|
// 4. Error classes from the operations' errors lists.
|
|
733
805
|
const out: string[] = [];
|
|
806
|
+
// Emitted struct body → the name that owns it, for structural aliasing.
|
|
807
|
+
// Keyed on the emitted TEXT, so two shapes collapse only when what they
|
|
808
|
+
// would emit is byte-identical — including member names, targets, docs and
|
|
809
|
+
// pipes. References are already resolved to names at this point, so a
|
|
810
|
+
// difference anywhere in the tree shows up as different text.
|
|
811
|
+
const structBodies = new Map<string, { id: string; name: string }>();
|
|
812
|
+
let aliased = 0;
|
|
734
813
|
// Set when any error carries CATEGORY_TRAIT, so the header only imports
|
|
735
814
|
// the category module when something actually uses it.
|
|
736
815
|
let usesCategories = false;
|
|
@@ -866,7 +945,6 @@ export const generateService = (
|
|
|
866
945
|
fields.push(...inject.interfaceLines);
|
|
867
946
|
members.push(inject.structLine);
|
|
868
947
|
}
|
|
869
|
-
out.push(interfaceDecl(name, fields));
|
|
870
948
|
const struct = members.length
|
|
871
949
|
? `S.Struct({\n${members.join("\n")}\n})`
|
|
872
950
|
: `S.Struct({})`;
|
|
@@ -884,26 +962,65 @@ export const generateService = (
|
|
|
884
962
|
: []),
|
|
885
963
|
];
|
|
886
964
|
const tail = pipes.map((p) => `.pipe(${p})`).join("");
|
|
965
|
+
|
|
966
|
+
// Shapes whose emitted body is byte-identical are one shape wearing
|
|
967
|
+
// many names. The docs pipelines generate a fresh copy of every nested
|
|
968
|
+
// shape per operation, so cloudflare's zero_trust carries 374 copies of
|
|
969
|
+
// `{ group?: unknown }` — one per operation x application type x
|
|
970
|
+
// include/exclude/require — and 72% of its 16,800 structures are
|
|
971
|
+
// redundant that way.
|
|
972
|
+
//
|
|
973
|
+
// Emit the body once and alias the rest to it. The public name is kept,
|
|
974
|
+
// so nothing about the surface changes; only the duplicate bodies go.
|
|
975
|
+
// Operation I/O is excluded: those carry the operation's Http trait and
|
|
976
|
+
// must never be merged onto one another.
|
|
977
|
+
const bodyKey = `${JSON.stringify(fields)}|${struct}${tail}`;
|
|
978
|
+
const canonical = structCtx.isOpIo
|
|
979
|
+
? undefined
|
|
980
|
+
: structBodies.get(bodyKey);
|
|
981
|
+
if (canonical !== undefined) {
|
|
982
|
+
out.push(`export type ${name} = ${canonical.name};`);
|
|
983
|
+
out.push(`export const ${name} = ${canonical.name};\n`);
|
|
984
|
+
// Later shapes referencing this one now render the canonical name,
|
|
985
|
+
// which is what lets their bodies collapse too.
|
|
986
|
+
canonicalId.set(id, canonical.id);
|
|
987
|
+
aliased++;
|
|
988
|
+
} else {
|
|
989
|
+
if (!structCtx.isOpIo) {
|
|
990
|
+
structBodies.set(bodyKey, { id, name });
|
|
991
|
+
}
|
|
992
|
+
out.push(interfaceDecl(name, fields));
|
|
993
|
+
out.push(
|
|
994
|
+
suspendConst({
|
|
995
|
+
name,
|
|
996
|
+
pure,
|
|
997
|
+
multiline: true,
|
|
998
|
+
annotateIdentifier: true,
|
|
999
|
+
expr: `${struct}${tail}`,
|
|
1000
|
+
}),
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
} else if (d.type === "list") {
|
|
1004
|
+
const nullable =
|
|
1005
|
+
spec.nullableTrait !== undefined &&
|
|
1006
|
+
spec.nullableTrait in (d.member.traits ?? {});
|
|
1007
|
+
const item = ref(d.member.target, i);
|
|
887
1008
|
out.push(
|
|
888
|
-
|
|
889
|
-
name,
|
|
890
|
-
pure,
|
|
891
|
-
multiline: true,
|
|
892
|
-
annotateIdentifier: true,
|
|
893
|
-
expr: `${struct}${tail}`,
|
|
894
|
-
}),
|
|
1009
|
+
`export type ${name} = Array<${tsRefAt(d.member.target, id)}${nullable ? " | null" : ""}>;`,
|
|
895
1010
|
);
|
|
896
|
-
} else if (d.type === "list") {
|
|
897
|
-
out.push(`export type ${name} = Array<${tsRefAt(d.member.target, id)}>;`);
|
|
898
1011
|
out.push(
|
|
899
|
-
`export const ${name} = ${pure}S.Array(${
|
|
1012
|
+
`export const ${name} = ${pure}S.Array(${nullable ? `S.NullOr(${item})` : item}) as any as S.Schema<${name}>;\n`,
|
|
900
1013
|
);
|
|
901
1014
|
} else if (d.type === "map") {
|
|
1015
|
+
const nullable =
|
|
1016
|
+
spec.nullableTrait !== undefined &&
|
|
1017
|
+
spec.nullableTrait in (d.value.traits ?? {});
|
|
1018
|
+
const value = ref(d.value.target, i);
|
|
902
1019
|
out.push(
|
|
903
|
-
`export type ${name} = { [key: string]: ${tsRefAt(d.value.target, id)} | undefined };`,
|
|
1020
|
+
`export type ${name} = { [key: string]: ${tsRefAt(d.value.target, id)}${nullable ? " | null" : ""} | undefined };`,
|
|
904
1021
|
);
|
|
905
1022
|
out.push(
|
|
906
|
-
`export const ${name} = ${pure}S.Record(S.String, ${
|
|
1023
|
+
`export const ${name} = ${pure}S.Record(S.String, ${nullable ? `S.NullOr(${value})` : value}) as any as S.Schema<${name}>;\n`,
|
|
907
1024
|
);
|
|
908
1025
|
} else if (d.type === "union") {
|
|
909
1026
|
// A union arm targeting the union itself carries no information
|
|
@@ -923,9 +1040,10 @@ export const generateService = (
|
|
|
923
1040
|
if (spec.union) {
|
|
924
1041
|
out.push(...spec.union({ name, caseTargets, caseKeys, tsRef }));
|
|
925
1042
|
} else if (spec.unionStyle === "opaque-cases") {
|
|
1043
|
+
const disc = unionDiscriminator(caseTargets);
|
|
926
1044
|
out.push(
|
|
927
1045
|
`export type ${name} = ${caseTargets.map((t) => tsRefAt(t, id)).join(" | ") || "unknown"};`,
|
|
928
|
-
`export const ${name} = ${pure}S.Unknown.pipe(T.UnionCases(${JSON.stringify(caseKeys)}));\n`,
|
|
1046
|
+
`export const ${name} = ${pure}S.Unknown.pipe(T.UnionCases(${JSON.stringify(caseKeys)}${disc ? `, ${JSON.stringify(disc)}` : ""}));\n`,
|
|
929
1047
|
);
|
|
930
1048
|
} else {
|
|
931
1049
|
throw new Error(
|
|
@@ -947,7 +1065,7 @@ export const generateService = (
|
|
|
947
1065
|
const union = values.length ? values.join(" | ") : "number";
|
|
948
1066
|
out.push(
|
|
949
1067
|
`export type ${name} = ${union};`,
|
|
950
|
-
`export const ${name} =
|
|
1068
|
+
`export const ${name} = S.Number;\n`,
|
|
951
1069
|
);
|
|
952
1070
|
}
|
|
953
1071
|
});
|
|
@@ -975,14 +1093,26 @@ export const generateService = (
|
|
|
975
1093
|
// No items path: `.items()` is a page passthrough at runtime, so an
|
|
976
1094
|
// item IS a whole response.
|
|
977
1095
|
if (!itemsPath) return tsRef(outputId);
|
|
978
|
-
let
|
|
1096
|
+
let id: string = outputId;
|
|
1097
|
+
// Whether the path crossed a list on the way down (`"edges.node"` on a
|
|
1098
|
+
// Relay connection). When it did, the path itself is what fans out, so
|
|
1099
|
+
// the value it lands on IS one item — it needn't be a list again.
|
|
1100
|
+
let fannedOut = false;
|
|
979
1101
|
for (const segment of itemsPath.split(".")) {
|
|
1102
|
+
let def: any = shapes[id];
|
|
1103
|
+
if (def?.type === "list") {
|
|
1104
|
+
id = def.member.target as string;
|
|
1105
|
+
def = shapes[id];
|
|
1106
|
+
fannedOut = true;
|
|
1107
|
+
}
|
|
980
1108
|
if (def?.type !== "structure") return undefined;
|
|
981
1109
|
const info = memberInfos(def).find((m) => m.tsName === segment);
|
|
982
1110
|
if (!info) return undefined;
|
|
983
|
-
|
|
1111
|
+
id = info.target;
|
|
984
1112
|
}
|
|
985
|
-
|
|
1113
|
+
const last: any = shapes[id];
|
|
1114
|
+
if (last?.type === "list") return tsRef(last.member.target);
|
|
1115
|
+
return fannedOut ? tsRef(id) : undefined;
|
|
986
1116
|
};
|
|
987
1117
|
|
|
988
1118
|
const emitOperation =
|