@distilled.cloud/core 0.30.2 → 1.0.0-rc.1
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/lib/api.d.ts +165 -0
- package/lib/api.d.ts.map +1 -0
- package/lib/api.js +178 -0
- package/lib/api.js.map +1 -0
- package/lib/codegen/cli.d.ts +29 -0
- package/lib/codegen/cli.d.ts.map +1 -0
- package/lib/codegen/cli.js +165 -0
- package/lib/codegen/cli.js.map +1 -0
- package/lib/codegen/emit.d.ts +129 -0
- package/lib/codegen/emit.d.ts.map +1 -0
- package/lib/codegen/emit.js +105 -0
- package/lib/codegen/emit.js.map +1 -0
- package/lib/codegen/format.d.ts +23 -0
- package/lib/codegen/format.d.ts.map +1 -0
- package/lib/codegen/format.js +28 -0
- package/lib/codegen/format.js.map +1 -0
- package/lib/codegen/generator.d.ts +334 -0
- package/lib/codegen/generator.d.ts.map +1 -0
- package/lib/codegen/generator.js +691 -0
- package/lib/codegen/generator.js.map +1 -0
- package/lib/codegen/graph.d.ts +36 -0
- package/lib/codegen/graph.d.ts.map +1 -0
- package/lib/codegen/graph.js +136 -0
- package/lib/codegen/graph.js.map +1 -0
- package/lib/codegen/members.d.ts +25 -0
- package/lib/codegen/members.d.ts.map +1 -0
- package/lib/codegen/members.js +55 -0
- package/lib/codegen/members.js.map +1 -0
- package/lib/codegen/naming.d.ts +29 -0
- package/lib/codegen/naming.d.ts.map +1 -0
- package/lib/codegen/naming.js +74 -0
- package/lib/codegen/naming.js.map +1 -0
- package/lib/codegen/openapi-cli.d.ts +38 -0
- package/lib/codegen/openapi-cli.d.ts.map +1 -0
- package/lib/codegen/openapi-cli.js +107 -0
- package/lib/codegen/openapi-cli.js.map +1 -0
- package/lib/codegen/openapi.d.ts +115 -0
- package/lib/codegen/openapi.d.ts.map +1 -0
- package/lib/codegen/openapi.js +1220 -0
- package/lib/codegen/openapi.js.map +1 -0
- package/lib/codegen/operations.d.ts +24 -0
- package/lib/codegen/operations.d.ts.map +1 -0
- package/lib/codegen/operations.js +56 -0
- package/lib/codegen/operations.js.map +1 -0
- package/lib/codegen/pagination.d.ts +39 -0
- package/lib/codegen/pagination.d.ts.map +1 -0
- package/lib/codegen/pagination.js +33 -0
- package/lib/codegen/pagination.js.map +1 -0
- package/lib/codegen/prelude.d.ts +15 -0
- package/lib/codegen/prelude.d.ts.map +1 -0
- package/lib/codegen/prelude.js +60 -0
- package/lib/codegen/prelude.js.map +1 -0
- package/lib/error-category.d.ts +28 -0
- package/lib/error-category.d.ts.map +1 -0
- package/lib/error-category.js +46 -0
- package/lib/error-category.js.map +1 -0
- package/lib/errors.d.ts +1 -0
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +1 -0
- package/lib/errors.js.map +1 -1
- package/lib/json-patch.d.ts +25 -32
- package/lib/json-patch.d.ts.map +1 -1
- package/lib/json-patch.js +23 -95
- package/lib/json-patch.js.map +1 -1
- package/lib/pagination.d.ts +37 -51
- package/lib/pagination.d.ts.map +1 -1
- package/lib/pagination.js +72 -90
- package/lib/pagination.js.map +1 -1
- package/lib/protocol-http.d.ts +74 -0
- package/lib/protocol-http.d.ts.map +1 -0
- package/lib/protocol-http.js +554 -0
- package/lib/protocol-http.js.map +1 -0
- package/lib/protocol-rest.d.ts +124 -0
- package/lib/protocol-rest.d.ts.map +1 -0
- package/lib/protocol-rest.js +242 -0
- package/lib/protocol-rest.js.map +1 -0
- package/lib/retry.d.ts +8 -2
- package/lib/retry.d.ts.map +1 -1
- package/lib/retry.js +21 -15
- package/lib/retry.js.map +1 -1
- package/lib/schema.d.ts +7 -8
- package/lib/schema.d.ts.map +1 -1
- package/lib/schema.js +7 -8
- package/lib/schema.js.map +1 -1
- package/lib/trait.d.ts +150 -0
- package/lib/trait.d.ts.map +1 -0
- package/lib/trait.js +107 -0
- package/lib/trait.js.map +1 -0
- package/package.json +18 -75
- package/src/api.ts +446 -0
- package/src/codegen/cli.ts +268 -0
- package/src/codegen/emit.ts +207 -0
- package/src/codegen/format.ts +47 -0
- package/src/codegen/generator.ts +1153 -0
- package/src/codegen/graph.ts +151 -0
- package/src/codegen/members.ts +71 -0
- package/src/codegen/naming.ts +86 -0
- package/src/codegen/openapi-cli.ts +166 -0
- package/src/codegen/openapi.ts +1450 -0
- package/src/codegen/operations.ts +76 -0
- package/src/codegen/pagination.ts +71 -0
- package/src/codegen/prelude.ts +70 -0
- package/src/error-category.ts +84 -0
- package/src/errors.ts +2 -0
- package/src/json-patch.ts +26 -110
- package/src/pagination.ts +86 -142
- package/src/protocol-http.ts +699 -0
- package/src/protocol-rest.ts +367 -0
- package/src/retry.ts +20 -21
- package/src/schema.ts +7 -8
- package/src/trait.ts +238 -0
- package/README.md +0 -30
- package/lib/client.d.ts +0 -167
- package/lib/client.d.ts.map +0 -1
- package/lib/client.js +0 -659
- package/lib/client.js.map +0 -1
- package/lib/schemas.d.ts +0 -60
- package/lib/schemas.d.ts.map +0 -1
- package/lib/schemas.js +0 -79
- package/lib/schemas.js.map +0 -1
- package/lib/sensitive.d.ts +0 -71
- package/lib/sensitive.d.ts.map +0 -1
- package/lib/sensitive.js +0 -96
- package/lib/sensitive.js.map +0 -1
- package/lib/traits.d.ts +0 -421
- package/lib/traits.d.ts.map +0 -1
- package/lib/traits.js +0 -737
- package/lib/traits.js.map +0 -1
- package/src/client.ts +0 -1177
- package/src/schemas.ts +0 -128
- package/src/sensitive.ts +0 -119
- package/src/traits.ts +0 -996
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generic generator CLI harness (dev-time only).
|
|
3
|
+
*
|
|
4
|
+
* Owns the smithy-model pipeline every SDK package shares: scan the model
|
|
5
|
+
* directory (plus optional hand-authored manual specs), apply the RFC-6902
|
|
6
|
+
* patch chain (`patches/<resource>/*.json`, `*.manual.json` last; stale
|
|
7
|
+
* targets warn, malformed patches fail the run), compile each model through
|
|
8
|
+
* {@link generateService} with the provider's {@link SdkSpec}, write the
|
|
9
|
+
* service modules and the namespaced barrel.
|
|
10
|
+
*
|
|
11
|
+
* A provider's `scripts/generate.ts` is: trait consts + an SdkSpec + a
|
|
12
|
+
* `runGeneratorCli` call.
|
|
13
|
+
*/
|
|
14
|
+
import { BunRuntime, BunServices } from "@effect/platform-bun";
|
|
15
|
+
import { Console, Effect } from "effect";
|
|
16
|
+
import * as FileSystem from "effect/FileSystem";
|
|
17
|
+
import * as Path from "effect/Path";
|
|
18
|
+
import { Flag } from "effect/unstable/cli";
|
|
19
|
+
import { Command } from "effect/unstable/cli";
|
|
20
|
+
import {
|
|
21
|
+
applyOperation,
|
|
22
|
+
isStaleTargetError,
|
|
23
|
+
type PatchFile,
|
|
24
|
+
} from "../json-patch.ts";
|
|
25
|
+
import { barrel } from "./emit.ts";
|
|
26
|
+
import { formatGenerated } from "./format.ts";
|
|
27
|
+
import { generateService, type SdkSpec } from "./generator.ts";
|
|
28
|
+
|
|
29
|
+
export interface GeneratorCliOptions {
|
|
30
|
+
/** Command description shown in --help. */
|
|
31
|
+
readonly description: string;
|
|
32
|
+
/** Absolute package root (usually `path.resolve(import.meta.dir, "..")`). */
|
|
33
|
+
readonly root: string;
|
|
34
|
+
/** Model directory default (relative to root). Default `.generated-specs`. */
|
|
35
|
+
readonly smithyDir?: string;
|
|
36
|
+
/** Output directory default (relative to root). Default `src/services`. */
|
|
37
|
+
readonly outDir?: string;
|
|
38
|
+
/** Model files to skip (e.g. a shared protocols model). */
|
|
39
|
+
readonly excludeModel?: (file: string) => boolean;
|
|
40
|
+
/** Directory of hand-authored models merged after the generated ones. */
|
|
41
|
+
readonly manualSpecsDir?: string;
|
|
42
|
+
/** RFC-6902 patch chain root. Default `patches`; `false` disables. */
|
|
43
|
+
readonly patchesDir?: string | false;
|
|
44
|
+
/**
|
|
45
|
+
* Model transform applied AFTER the patch chain, before generation —
|
|
46
|
+
* for whole-model rewrites that must see post-patch shape names (e.g.
|
|
47
|
+
* cloudflare's scope-twin structural dedup). May mutate the model;
|
|
48
|
+
* a returned log line is printed.
|
|
49
|
+
*/
|
|
50
|
+
readonly transformModel?: (model: any, resource: string) => string | void;
|
|
51
|
+
/** The provider spec — built per model (metadata may vary per model). */
|
|
52
|
+
readonly spec: (model: any) => SdkSpec;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Run the generator CLI (BunRuntime main — call at module top level). */
|
|
56
|
+
export const runGeneratorCli = (options: GeneratorCliOptions): void => {
|
|
57
|
+
const command = Command.make(
|
|
58
|
+
"generate",
|
|
59
|
+
{
|
|
60
|
+
smithy: Flag.string("smithy").pipe(
|
|
61
|
+
Flag.withDefault(options.smithyDir ?? ".generated-specs"),
|
|
62
|
+
Flag.withDescription("Directory of Smithy JSON models"),
|
|
63
|
+
),
|
|
64
|
+
out: Flag.string("out").pipe(
|
|
65
|
+
Flag.withDefault(options.outDir ?? "src/services"),
|
|
66
|
+
Flag.withDescription("Output directory for generated service modules"),
|
|
67
|
+
),
|
|
68
|
+
resource: Flag.string("resource").pipe(
|
|
69
|
+
Flag.withDefault(""),
|
|
70
|
+
Flag.withDescription("Only generate this resource (e.g. ai)"),
|
|
71
|
+
),
|
|
72
|
+
},
|
|
73
|
+
(config) =>
|
|
74
|
+
Effect.gen(function* () {
|
|
75
|
+
const fs = yield* FileSystem.FileSystem;
|
|
76
|
+
const path = yield* Path.Path;
|
|
77
|
+
const root = options.root;
|
|
78
|
+
const smithyDir = path.resolve(root, config.smithy);
|
|
79
|
+
const outDir = path.resolve(root, config.out);
|
|
80
|
+
|
|
81
|
+
yield* Console.log("⚙️ generate");
|
|
82
|
+
yield* Console.log(` Smithy: ${smithyDir}`);
|
|
83
|
+
yield* Console.log(` Output: ${outDir}`);
|
|
84
|
+
|
|
85
|
+
// Generated models plus optional manual-specs (hand-authored models
|
|
86
|
+
// for APIs the provider's spec source doesn't cover). A manual model
|
|
87
|
+
// must not shadow a generated one.
|
|
88
|
+
const generated = (yield* fs.readDirectory(smithyDir))
|
|
89
|
+
.filter(
|
|
90
|
+
(f) => f.endsWith(".json") && !(options.excludeModel?.(f) ?? false),
|
|
91
|
+
)
|
|
92
|
+
.map((f) => ({ file: f, dir: smithyDir }));
|
|
93
|
+
const manualDir = options.manualSpecsDir
|
|
94
|
+
? path.resolve(root, options.manualSpecsDir)
|
|
95
|
+
: undefined;
|
|
96
|
+
const manual =
|
|
97
|
+
manualDir && (yield* fs.exists(manualDir))
|
|
98
|
+
? (yield* fs.readDirectory(manualDir))
|
|
99
|
+
.filter((f) => f.endsWith(".json"))
|
|
100
|
+
.map((f) => ({ file: f, dir: manualDir }))
|
|
101
|
+
: [];
|
|
102
|
+
for (const m of manual) {
|
|
103
|
+
if (generated.some((g) => g.file === m.file)) {
|
|
104
|
+
return yield* Effect.die(
|
|
105
|
+
new Error(
|
|
106
|
+
`${options.manualSpecsDir}/${m.file} shadows a generated model — rename or delete it`,
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const entries = [...generated, ...manual].sort((a, b) =>
|
|
112
|
+
a.file.localeCompare(b.file),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
yield* fs.makeDirectory(outDir, { recursive: true });
|
|
116
|
+
|
|
117
|
+
const written: string[] = [];
|
|
118
|
+
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
|
+
|
|
142
|
+
for (const { file, dir } of entries) {
|
|
143
|
+
const resource = file.replace(/\.json$/, "");
|
|
144
|
+
if (config.resource && resource !== config.resource) continue;
|
|
145
|
+
|
|
146
|
+
const model = JSON.parse(
|
|
147
|
+
yield* fs.readFileString(path.join(dir, file)),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Apply the RFC-6902 patch chain before generating. Hand-written
|
|
151
|
+
// *.manual.json patches apply after the generated ones — they
|
|
152
|
+
// usually target post-rename shape names.
|
|
153
|
+
const patchDir = patchRoot && path.join(patchRoot, resource);
|
|
154
|
+
if (patchDir && (yield* fs.exists(patchDir))) {
|
|
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
|
+
}
|
|
186
|
+
|
|
187
|
+
if (options.transformModel) {
|
|
188
|
+
const note = options.transformModel(model, resource);
|
|
189
|
+
if (note) yield* Console.log(` ${note}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const { code, operations } = generateService(
|
|
193
|
+
model,
|
|
194
|
+
options.spec(model),
|
|
195
|
+
);
|
|
196
|
+
if (operations === 0) continue;
|
|
197
|
+
|
|
198
|
+
yield* fs.writeFileString(path.join(outDir, `${resource}.ts`), code);
|
|
199
|
+
written.push(resource);
|
|
200
|
+
totalOps += operations;
|
|
201
|
+
}
|
|
202
|
+
|
|
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
|
+
// Barrel — namespace per resource to avoid op-name collisions.
|
|
215
|
+
//
|
|
216
|
+
// A FULL run is authoritative: the barrel is exactly what was
|
|
217
|
+
// generated, so a resource whose model went away drops out of it.
|
|
218
|
+
//
|
|
219
|
+
// A `--resource` run only knows about the resource it generated, so
|
|
220
|
+
// rewriting the barrel from that would delete every other export —
|
|
221
|
+
// which is how #397 shipped a cloudflare barrel exporting `workers`
|
|
222
|
+
// and nothing else, silently dropping 119 services. Merge into the
|
|
223
|
+
// existing barrel instead, so `--resource` can still ADD a brand-new
|
|
224
|
+
// resource without removing anything.
|
|
225
|
+
const barrelPath = path.join(outDir, "index.ts");
|
|
226
|
+
const filtered = config.resource !== "";
|
|
227
|
+
let resources = written;
|
|
228
|
+
if (filtered && (yield* fs.exists(barrelPath))) {
|
|
229
|
+
const existing = (yield* fs.readFileString(barrelPath))
|
|
230
|
+
.split("\n")
|
|
231
|
+
.map((line) => /^export \* as (\S+) from/.exec(line)?.[1])
|
|
232
|
+
.filter((name): name is string => name !== undefined);
|
|
233
|
+
// Ordered exactly as a full run orders it: by MODEL FILENAME, so
|
|
234
|
+
// the `.json` takes part in the collation (`ai_gateway.json` sorts
|
|
235
|
+
// before `ai.json`, where the bare names sort the other way).
|
|
236
|
+
// Otherwise a filtered run reshuffles the barrel into diff noise.
|
|
237
|
+
resources = [...new Set([...existing, ...written])].sort((a, b) =>
|
|
238
|
+
`${a}.json`.localeCompare(`${b}.json`),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
yield* fs.writeFileString(
|
|
243
|
+
barrelPath,
|
|
244
|
+
barrel(
|
|
245
|
+
`// AUTO-GENERATED by scripts/generate.ts. Do not edit.\n`,
|
|
246
|
+
resources.map((r) => ({ name: r, path: `./${r}.ts` })),
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
yield* formatGenerated(outDir);
|
|
251
|
+
|
|
252
|
+
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
|
+
: "."),
|
|
257
|
+
);
|
|
258
|
+
yield* Console.log(` ${path.join(outDir, "index.ts")}`);
|
|
259
|
+
}),
|
|
260
|
+
).pipe(Command.withDescription(options.description));
|
|
261
|
+
|
|
262
|
+
BunRuntime.runMain(
|
|
263
|
+
Effect.provide(
|
|
264
|
+
Command.run(command, { version: "1.0.0" }),
|
|
265
|
+
BunServices.layer,
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
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.TaggedErrorClass` 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
|
+
/**
|
|
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
|
+
*/
|
|
26
|
+
export const PURE = "/*@__PURE__*/ ";
|
|
27
|
+
|
|
28
|
+
/** `export interface X { … }` (or the empty-body form). */
|
|
29
|
+
export const interfaceDecl = (name: string, fields: string[]): string =>
|
|
30
|
+
fields.length
|
|
31
|
+
? `export interface ${name} {\n${fields.join("\n")}\n}`
|
|
32
|
+
: `export interface ${name} {}`;
|
|
33
|
+
|
|
34
|
+
export interface SuspendConstOptions {
|
|
35
|
+
readonly name: string;
|
|
36
|
+
/** The schema expression inside the suspend thunk. */
|
|
37
|
+
readonly expr: string;
|
|
38
|
+
/** PURE marker(s), e.g. `"/*@__PURE__*/ "`. Emitted verbatim before the expression. */
|
|
39
|
+
readonly pure?: string;
|
|
40
|
+
/** When set, `.annotate({ identifier: <name> })` is appended after the suspend. */
|
|
41
|
+
readonly annotateIdentifier?: boolean;
|
|
42
|
+
/** Extra annotation object source to use instead of the identifier default. */
|
|
43
|
+
readonly annotation?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Explicit thunk return type (`(): S.Schema<X> =>`) — used for shapes in
|
|
46
|
+
* dependency cycles to stop circular type inference.
|
|
47
|
+
*/
|
|
48
|
+
readonly thunkType?: string;
|
|
49
|
+
/** CF-style multiline body (`S.suspend(() =>\n<expr>,\n)`). */
|
|
50
|
+
readonly multiline?: boolean;
|
|
51
|
+
/** The cast target; defaults to `S.Schema<name>`. */
|
|
52
|
+
readonly castTo?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The `export const X = S.suspend(…) … as any as S.Schema<X>;` skeleton
|
|
57
|
+
* shared by both generators.
|
|
58
|
+
*/
|
|
59
|
+
export const suspendConst = (o: SuspendConstOptions): string => {
|
|
60
|
+
const cast = o.castTo ?? `S.Schema<${o.name}>`;
|
|
61
|
+
const thunk = o.thunkType ? `(): ${o.thunkType} =>` : `() =>`;
|
|
62
|
+
const suspend = o.multiline
|
|
63
|
+
? `S.suspend(${thunk}\n${o.expr},\n)`
|
|
64
|
+
: `S.suspend(${thunk} ${o.expr})`;
|
|
65
|
+
const annotate = o.annotation
|
|
66
|
+
? `.annotate(${o.annotation})`
|
|
67
|
+
: o.annotateIdentifier
|
|
68
|
+
? `.annotate({ identifier: ${q(o.name)} })`
|
|
69
|
+
: "";
|
|
70
|
+
return `export const ${o.name} = ${o.pure ?? ""}${suspend}${annotate} as any as ${cast};\n`;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Member-level lazy reference: `S.suspend(() => X).annotate({ identifier })`.
|
|
75
|
+
* The `typed` form adds an explicit `S.Schema<X>` thunk return type — used
|
|
76
|
+
* for references into dependency cycles to stop circular type inference.
|
|
77
|
+
*/
|
|
78
|
+
export const suspendRef = (name: string, typed = false): string =>
|
|
79
|
+
typed
|
|
80
|
+
? `S.suspend((): S.Schema<${name}> => ${name}).annotate({ identifier: ${q(name)} })`
|
|
81
|
+
: `S.suspend(() => ${name}).annotate({ identifier: ${q(name)} })`;
|
|
82
|
+
|
|
83
|
+
export interface EnumDeclOptions {
|
|
84
|
+
readonly name: string;
|
|
85
|
+
readonly values: readonly string[];
|
|
86
|
+
readonly pure?: string;
|
|
87
|
+
/** The schema const expression; both SDKs use `S.String` (open enums). */
|
|
88
|
+
readonly schemaExpr?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* String-union enum ALIAS: the spec's documented values as a CLOSED literal
|
|
93
|
+
* union (`type X = "a" | "b"`) — response readers match documented values
|
|
94
|
+
* exhaustively. INPUT references re-open the alias inline
|
|
95
|
+
* (`X | (string & {})`) so consumers can send tomorrow's values without an
|
|
96
|
+
* SDK update. The schema stays `S.String` in both directions (the
|
|
97
|
+
* protocols never validate enum membership, so undocumented wire values
|
|
98
|
+
* always pass through at runtime).
|
|
99
|
+
*/
|
|
100
|
+
export const enumDecl = (o: EnumDeclOptions): string[] => {
|
|
101
|
+
const union = o.values.length ? o.values.map(q).join(" | ") : "string";
|
|
102
|
+
return [
|
|
103
|
+
`export type ${o.name} = ${union};`,
|
|
104
|
+
`export const ${o.name} = ${o.pure ?? ""}${o.schemaExpr ?? "S.String"};\n`,
|
|
105
|
+
];
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export interface ErrorClassOptions {
|
|
109
|
+
readonly name: string;
|
|
110
|
+
/** The error tag; defaults to `name`. */
|
|
111
|
+
readonly tag?: string;
|
|
112
|
+
/** Field lines (` key: S.String,`). */
|
|
113
|
+
readonly fields: readonly string[];
|
|
114
|
+
/** Optional extra argument(s) after the fields object (annotations). */
|
|
115
|
+
readonly annotations?: string;
|
|
116
|
+
/** `.pipe(…)` suffix (e.g. category decorators). */
|
|
117
|
+
readonly pipes?: string;
|
|
118
|
+
/** Wrap the class expression (e.g. `T.applyErrorMatchers(<cls>, …)`). */
|
|
119
|
+
readonly wrap?: (cls: string) => string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `export class X extends /*@__PURE__*/ S.TaggedErrorClass<X>()("X", { … }) {}`
|
|
124
|
+
*
|
|
125
|
+
* The PURE markers are what make an unused error class droppable. A class
|
|
126
|
+
* whose heritage clause is an unannotated call can never be tree-shaken —
|
|
127
|
+
* the bundler has to assume the call has side effects — so a consumer
|
|
128
|
+
* importing one operation would retain every error class in the module
|
|
129
|
+
* (distilled #191).
|
|
130
|
+
*
|
|
131
|
+
* `wrap` needs its own marker as well as the inner one: a pure call's
|
|
132
|
+
* ARGUMENTS are still evaluated, so annotating only
|
|
133
|
+
* `T.applyErrorMatchers(S.TaggedErrorClass…(…), […])` leaves the inner call
|
|
134
|
+
* holding the class alive. Verified against esbuild in both directions.
|
|
135
|
+
*/
|
|
136
|
+
export const errorClass = (o: ErrorClassOptions): string => {
|
|
137
|
+
const annotations = o.annotations ? `,\n${o.annotations}` : "";
|
|
138
|
+
const cls = `${PURE}S.TaggedErrorClass<${o.name}>()(${q(o.tag ?? o.name)}, {\n${o.fields.join("\n")}\n}${annotations})${o.pipes ?? ""}`;
|
|
139
|
+
const body = o.wrap ? `${PURE}${o.wrap(cls)}` : cls;
|
|
140
|
+
return `export class ${o.name} extends ${body} {}\n`;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export interface OperationConstOptions {
|
|
144
|
+
/** The exported (usually lowerFirst) operation name. */
|
|
145
|
+
readonly exportName: string;
|
|
146
|
+
/** Full type annotation (e.g. `API.OperationMethod<A, B, E, R>`). */
|
|
147
|
+
readonly typeAnnotation: string;
|
|
148
|
+
/** `API.make` or `API.makePaginated` (with namespace prefix). */
|
|
149
|
+
readonly factory: string;
|
|
150
|
+
/** The config object source (including braces). */
|
|
151
|
+
readonly config: string;
|
|
152
|
+
/** Optional extra factory argument (e.g. a pagination strategy). */
|
|
153
|
+
readonly extraArg?: string;
|
|
154
|
+
readonly pure?: string;
|
|
155
|
+
/**
|
|
156
|
+
* Widen the factory result to `any`, for annotations the factory's
|
|
157
|
+
* generic signature can't prove. The one case today: a paginated
|
|
158
|
+
* operation's `items` element type comes from the pagination trait's
|
|
159
|
+
* `items` PATH — a runtime string — so the factory can only infer the
|
|
160
|
+
* structural fallback while the annotation names the real element type.
|
|
161
|
+
*
|
|
162
|
+
* Unlike the schema consts' `as any as S.Schema<X>`, the target doesn't
|
|
163
|
+
* need restating here: the const carries its own annotation, which IS
|
|
164
|
+
* the assignment target, so a bare `as any` lands in the same place.
|
|
165
|
+
* Restating it would double every paginated operation's declaration —
|
|
166
|
+
* ~40k lines across the SDKs — for no added checking.
|
|
167
|
+
*/
|
|
168
|
+
readonly castToAnnotation?: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** `export const op: T = API.make(() => ({ … }));` */
|
|
172
|
+
export const operationConst = (o: OperationConstOptions): string =>
|
|
173
|
+
`export const ${o.exportName}: ${o.typeAnnotation} = ${o.pure ?? ""}${o.factory}(() => (${o.config})${
|
|
174
|
+
o.extraArg ? `, ${o.extraArg}` : ""
|
|
175
|
+
})${o.castToAnnotation ? ` as any` : ""};\n`;
|
|
176
|
+
|
|
177
|
+
import { tsKey } from "./naming.ts";
|
|
178
|
+
|
|
179
|
+
export interface InterfaceFieldOptions {
|
|
180
|
+
readonly name: string;
|
|
181
|
+
readonly type: string;
|
|
182
|
+
readonly optional: boolean;
|
|
183
|
+
readonly doc?: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Interface field line(s): optional doc comment + ` name?: Type;`. */
|
|
187
|
+
export const interfaceField = (o: InterfaceFieldOptions): string[] => [
|
|
188
|
+
...(o.doc ? [` /** ${o.doc} */`] : []),
|
|
189
|
+
` ${tsKey(o.name)}${o.optional ? "?" : ""}: ${o.type};`,
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
/** `export type <Op>Error = A | B | <CommonErrors>;` */
|
|
193
|
+
export const errorUnionAlias = (
|
|
194
|
+
opName: string,
|
|
195
|
+
errorNames: readonly string[],
|
|
196
|
+
commonRef: string,
|
|
197
|
+
): string =>
|
|
198
|
+
`export type ${opName}Error = ${[...errorNames, commonRef].join(" | ")};`;
|
|
199
|
+
|
|
200
|
+
/** Namespaced barrel: `export * as name from "./file.ts";` per entry. */
|
|
201
|
+
export const barrel = (
|
|
202
|
+
header: string,
|
|
203
|
+
entries: ReadonlyArray<{ name: string; path: string }>,
|
|
204
|
+
): string =>
|
|
205
|
+
header +
|
|
206
|
+
entries.map((e) => `export * as ${e.name} from ${q(e.path)};`).join("\n") +
|
|
207
|
+
"\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
|
+
);
|