@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.
Files changed (132) hide show
  1. package/lib/api.d.ts +165 -0
  2. package/lib/api.d.ts.map +1 -0
  3. package/lib/api.js +178 -0
  4. package/lib/api.js.map +1 -0
  5. package/lib/codegen/cli.d.ts +29 -0
  6. package/lib/codegen/cli.d.ts.map +1 -0
  7. package/lib/codegen/cli.js +165 -0
  8. package/lib/codegen/cli.js.map +1 -0
  9. package/lib/codegen/emit.d.ts +129 -0
  10. package/lib/codegen/emit.d.ts.map +1 -0
  11. package/lib/codegen/emit.js +105 -0
  12. package/lib/codegen/emit.js.map +1 -0
  13. package/lib/codegen/format.d.ts +23 -0
  14. package/lib/codegen/format.d.ts.map +1 -0
  15. package/lib/codegen/format.js +28 -0
  16. package/lib/codegen/format.js.map +1 -0
  17. package/lib/codegen/generator.d.ts +334 -0
  18. package/lib/codegen/generator.d.ts.map +1 -0
  19. package/lib/codegen/generator.js +691 -0
  20. package/lib/codegen/generator.js.map +1 -0
  21. package/lib/codegen/graph.d.ts +36 -0
  22. package/lib/codegen/graph.d.ts.map +1 -0
  23. package/lib/codegen/graph.js +136 -0
  24. package/lib/codegen/graph.js.map +1 -0
  25. package/lib/codegen/members.d.ts +25 -0
  26. package/lib/codegen/members.d.ts.map +1 -0
  27. package/lib/codegen/members.js +55 -0
  28. package/lib/codegen/members.js.map +1 -0
  29. package/lib/codegen/naming.d.ts +29 -0
  30. package/lib/codegen/naming.d.ts.map +1 -0
  31. package/lib/codegen/naming.js +74 -0
  32. package/lib/codegen/naming.js.map +1 -0
  33. package/lib/codegen/openapi-cli.d.ts +38 -0
  34. package/lib/codegen/openapi-cli.d.ts.map +1 -0
  35. package/lib/codegen/openapi-cli.js +107 -0
  36. package/lib/codegen/openapi-cli.js.map +1 -0
  37. package/lib/codegen/openapi.d.ts +115 -0
  38. package/lib/codegen/openapi.d.ts.map +1 -0
  39. package/lib/codegen/openapi.js +1220 -0
  40. package/lib/codegen/openapi.js.map +1 -0
  41. package/lib/codegen/operations.d.ts +24 -0
  42. package/lib/codegen/operations.d.ts.map +1 -0
  43. package/lib/codegen/operations.js +56 -0
  44. package/lib/codegen/operations.js.map +1 -0
  45. package/lib/codegen/pagination.d.ts +39 -0
  46. package/lib/codegen/pagination.d.ts.map +1 -0
  47. package/lib/codegen/pagination.js +33 -0
  48. package/lib/codegen/pagination.js.map +1 -0
  49. package/lib/codegen/prelude.d.ts +15 -0
  50. package/lib/codegen/prelude.d.ts.map +1 -0
  51. package/lib/codegen/prelude.js +60 -0
  52. package/lib/codegen/prelude.js.map +1 -0
  53. package/lib/error-category.d.ts +28 -0
  54. package/lib/error-category.d.ts.map +1 -0
  55. package/lib/error-category.js +46 -0
  56. package/lib/error-category.js.map +1 -0
  57. package/lib/errors.d.ts +1 -0
  58. package/lib/errors.d.ts.map +1 -1
  59. package/lib/errors.js +1 -0
  60. package/lib/errors.js.map +1 -1
  61. package/lib/json-patch.d.ts +25 -32
  62. package/lib/json-patch.d.ts.map +1 -1
  63. package/lib/json-patch.js +23 -95
  64. package/lib/json-patch.js.map +1 -1
  65. package/lib/pagination.d.ts +37 -51
  66. package/lib/pagination.d.ts.map +1 -1
  67. package/lib/pagination.js +72 -90
  68. package/lib/pagination.js.map +1 -1
  69. package/lib/protocol-http.d.ts +74 -0
  70. package/lib/protocol-http.d.ts.map +1 -0
  71. package/lib/protocol-http.js +554 -0
  72. package/lib/protocol-http.js.map +1 -0
  73. package/lib/protocol-rest.d.ts +124 -0
  74. package/lib/protocol-rest.d.ts.map +1 -0
  75. package/lib/protocol-rest.js +242 -0
  76. package/lib/protocol-rest.js.map +1 -0
  77. package/lib/retry.d.ts +8 -2
  78. package/lib/retry.d.ts.map +1 -1
  79. package/lib/retry.js +21 -15
  80. package/lib/retry.js.map +1 -1
  81. package/lib/schema.d.ts +7 -8
  82. package/lib/schema.d.ts.map +1 -1
  83. package/lib/schema.js +7 -8
  84. package/lib/schema.js.map +1 -1
  85. package/lib/trait.d.ts +150 -0
  86. package/lib/trait.d.ts.map +1 -0
  87. package/lib/trait.js +107 -0
  88. package/lib/trait.js.map +1 -0
  89. package/package.json +18 -75
  90. package/src/api.ts +446 -0
  91. package/src/codegen/cli.ts +268 -0
  92. package/src/codegen/emit.ts +207 -0
  93. package/src/codegen/format.ts +47 -0
  94. package/src/codegen/generator.ts +1153 -0
  95. package/src/codegen/graph.ts +151 -0
  96. package/src/codegen/members.ts +71 -0
  97. package/src/codegen/naming.ts +86 -0
  98. package/src/codegen/openapi-cli.ts +166 -0
  99. package/src/codegen/openapi.ts +1450 -0
  100. package/src/codegen/operations.ts +76 -0
  101. package/src/codegen/pagination.ts +71 -0
  102. package/src/codegen/prelude.ts +70 -0
  103. package/src/error-category.ts +84 -0
  104. package/src/errors.ts +2 -0
  105. package/src/json-patch.ts +26 -110
  106. package/src/pagination.ts +86 -142
  107. package/src/protocol-http.ts +699 -0
  108. package/src/protocol-rest.ts +367 -0
  109. package/src/retry.ts +20 -21
  110. package/src/schema.ts +7 -8
  111. package/src/trait.ts +238 -0
  112. package/README.md +0 -30
  113. package/lib/client.d.ts +0 -167
  114. package/lib/client.d.ts.map +0 -1
  115. package/lib/client.js +0 -659
  116. package/lib/client.js.map +0 -1
  117. package/lib/schemas.d.ts +0 -60
  118. package/lib/schemas.d.ts.map +0 -1
  119. package/lib/schemas.js +0 -79
  120. package/lib/schemas.js.map +0 -1
  121. package/lib/sensitive.d.ts +0 -71
  122. package/lib/sensitive.d.ts.map +0 -1
  123. package/lib/sensitive.js +0 -96
  124. package/lib/sensitive.js.map +0 -1
  125. package/lib/traits.d.ts +0 -421
  126. package/lib/traits.d.ts.map +0 -1
  127. package/lib/traits.js +0 -737
  128. package/lib/traits.js.map +0 -1
  129. package/src/client.ts +0 -1177
  130. package/src/schemas.ts +0 -128
  131. package/src/sensitive.ts +0 -119
  132. package/src/traits.ts +0 -996
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Shared shape-graph utilities for the SDK generators (dev-time only).
3
+ *
4
+ * Works over plain Smithy-JSON shape maps (`{ "ns#Name": { type, members,
5
+ * … } }`). Providers supply the shape map and (optionally) a custom
6
+ * dependency extractor; reachability, topological ordering, and cycle
7
+ * analysis are generic.
8
+ */
9
+ import { isPrelude } from "./naming.ts";
10
+
11
+ export type ShapeMap = Record<string, any>;
12
+
13
+ /**
14
+ * Direct dependencies of a shape: structure/union member targets, list
15
+ * member, map key/value. Operations/services are not traversed here —
16
+ * providers seed reachability from operation I/O explicitly.
17
+ */
18
+ export const shapeDeps = (def: any): string[] => {
19
+ if (!def) return [];
20
+ if (def.type === "structure" || def.type === "union") {
21
+ return Object.values(def.members ?? {}).map((m: any) => m.target);
22
+ }
23
+ if (def.type === "list") return [def.member.target];
24
+ if (def.type === "map") {
25
+ return def.key ? [def.key.target, def.value.target] : [def.value.target];
26
+ }
27
+ return [];
28
+ };
29
+
30
+ /**
31
+ * Every shape reachable from `roots` through `deps`, excluding prelude
32
+ * shapes and ids missing from the map.
33
+ */
34
+ export const reachableFrom = (
35
+ shapes: ShapeMap,
36
+ roots: Iterable<string>,
37
+ deps: (def: any) => string[] = shapeDeps,
38
+ ): Set<string> => {
39
+ const reachable = new Set<string>();
40
+ const visit = (id: string) => {
41
+ if (isPrelude(id) || reachable.has(id) || !shapes[id]) return;
42
+ reachable.add(id);
43
+ for (const dep of deps(shapes[id])) visit(dep);
44
+ };
45
+ for (const r of roots) visit(r);
46
+ return reachable;
47
+ };
48
+
49
+ /**
50
+ * Dependencies-first topological order over `ids`. Cycles are tolerated:
51
+ * a back-edge is simply skipped, so mutually-recursive shapes come out in
52
+ * discovery order and emitters break the cycle with `S.suspend` at
53
+ * forward references (see {@link forwardRef}).
54
+ */
55
+ export const topoOrder = (
56
+ shapes: ShapeMap,
57
+ ids: Iterable<string>,
58
+ deps: (def: any) => string[] = shapeDeps,
59
+ ): string[] => {
60
+ const order: string[] = [];
61
+ const done = new Set<string>();
62
+ const stack = new Set<string>();
63
+ const walk = (id: string) => {
64
+ if (done.has(id) || stack.has(id) || isPrelude(id) || !shapes[id]) return;
65
+ stack.add(id);
66
+ for (const dep of deps(shapes[id])) walk(dep);
67
+ stack.delete(id);
68
+ done.add(id);
69
+ order.push(id);
70
+ };
71
+ for (const id of ids) walk(id);
72
+ return order;
73
+ };
74
+
75
+ /** Position index for {@link forwardRef} decisions. */
76
+ export const orderIndex = (order: readonly string[]): Map<string, number> => {
77
+ const indexOf = new Map<string, number>();
78
+ order.forEach((id, i) => indexOf.set(id, i));
79
+ return indexOf;
80
+ };
81
+
82
+ /**
83
+ * Whether a reference from the shape at `selfIdx` to `target` points
84
+ * forward in emission order (including into a cycle) and therefore needs
85
+ * `S.suspend(() => …)` to defer evaluation.
86
+ */
87
+ export const isForwardRef = (
88
+ indexOf: ReadonlyMap<string, number>,
89
+ target: string,
90
+ selfIdx: number,
91
+ ): boolean => {
92
+ const ti = indexOf.get(target);
93
+ return ti !== undefined && ti > selfIdx;
94
+ };
95
+
96
+ /**
97
+ * All shape ids that participate in a dependency cycle (Tarjan SCC —
98
+ * components of size > 1, plus direct self-loops). Used by generators
99
+ * that must know cyclicity independent of emission position (e.g. to add
100
+ * an explicit `S.Schema<T>` return type on suspended thunks and break
101
+ * circular type inference).
102
+ */
103
+ export const cyclicShapeIds = (
104
+ shapes: ShapeMap,
105
+ ids: Iterable<string>,
106
+ deps: (def: any) => string[] = shapeDeps,
107
+ ): Set<string> => {
108
+ const idSet = new Set(ids);
109
+ const index = new Map<string, number>();
110
+ const low = new Map<string, number>();
111
+ const onStack = new Set<string>();
112
+ const stack: string[] = [];
113
+ const cyclic = new Set<string>();
114
+ let counter = 0;
115
+
116
+ const strongConnect = (v: string) => {
117
+ index.set(v, counter);
118
+ low.set(v, counter);
119
+ counter++;
120
+ stack.push(v);
121
+ onStack.add(v);
122
+
123
+ for (const w of deps(shapes[v])) {
124
+ if (!idSet.has(w) || !shapes[w]) continue;
125
+ if (!index.has(w)) {
126
+ strongConnect(w);
127
+ low.set(v, Math.min(low.get(v)!, low.get(w)!));
128
+ } else if (onStack.has(w)) {
129
+ low.set(v, Math.min(low.get(v)!, index.get(w)!));
130
+ }
131
+ }
132
+
133
+ if (low.get(v) === index.get(v)) {
134
+ const component: string[] = [];
135
+ let w: string;
136
+ do {
137
+ w = stack.pop()!;
138
+ onStack.delete(w);
139
+ component.push(w);
140
+ } while (w !== v);
141
+ if (component.length > 1 || deps(shapes[v]).some((d) => d === v)) {
142
+ for (const c of component) cyclic.add(c);
143
+ }
144
+ }
145
+ };
146
+
147
+ for (const id of idSet) {
148
+ if (!index.has(id) && shapes[id]) strongConnect(id);
149
+ }
150
+ return cyclic;
151
+ };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Structure-member analysis for the SDK generators (dev-time only).
3
+ *
4
+ * The TS-facing name policy is the provider's (cloudflare camelCases wire
5
+ * names, AWS keeps them) — supplied as `nameFn`. Wire-name resolution from
6
+ * the smithy HTTP binding traits is generic smithy semantics.
7
+ */
8
+ import { oneLine } from "./naming.ts";
9
+
10
+ export interface MemberBase {
11
+ /** Original smithy member name. */
12
+ readonly name: string;
13
+ /** TS-facing name (per `nameFn`, deduplicated within the structure). */
14
+ readonly tsName: string;
15
+ readonly target: string;
16
+ readonly required: boolean;
17
+ readonly doc: string | undefined;
18
+ readonly traits: Record<string, any>;
19
+ }
20
+
21
+ /**
22
+ * Base analysis of a structure's members: TS naming (with collision
23
+ * dedup — a clashing name falls back to the original, then a numeric
24
+ * suffix), required-ness, and one-line docs. Binding classification and
25
+ * anything provider-specific layer on top via `traits`.
26
+ */
27
+ export const memberBases = (
28
+ def: any,
29
+ nameFn: (memberName: string) => string = (n) => n,
30
+ ): MemberBase[] => {
31
+ const used = new Set<string>();
32
+ return Object.entries(def.members ?? {}).map(([mn, m]: [string, any]) => {
33
+ const traits = m.traits ?? {};
34
+ let tsName = nameFn(mn);
35
+ if (used.has(tsName)) tsName = mn;
36
+ let k = 2;
37
+ while (used.has(tsName)) tsName = `${nameFn(mn)}${k++}`;
38
+ used.add(tsName);
39
+ return {
40
+ name: mn,
41
+ tsName,
42
+ target: m.target,
43
+ required: "smithy.api#required" in traits,
44
+ doc: oneLine(traits["smithy.api#documentation"]),
45
+ traits,
46
+ };
47
+ });
48
+ };
49
+
50
+ /**
51
+ * The wire name a bound member serializes under, per the smithy HTTP
52
+ * binding traits: labels use the member name (URI placeholders), query and
53
+ * header use the trait value when it's a non-empty string, everything else
54
+ * uses `jsonName` when present.
55
+ */
56
+ export const smithyWireName = (
57
+ traits: Record<string, any>,
58
+ memberName: string,
59
+ binding: "label" | "query" | "header" | "other",
60
+ ): string => {
61
+ if (binding === "label") return memberName;
62
+ if (binding === "query") {
63
+ const v = traits["smithy.api#httpQuery"];
64
+ return typeof v === "string" && v ? v : memberName;
65
+ }
66
+ if (binding === "header") {
67
+ const v = traits["smithy.api#httpHeader"];
68
+ return typeof v === "string" && v ? v : memberName;
69
+ }
70
+ return traits["smithy.api#jsonName"] ?? memberName;
71
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Shared naming utilities for the SDK generators (dev-time only — imported by
3
+ * `packages/<sdk>/scripts/generate.ts`, never by runtime code).
4
+ */
5
+
6
+ /** JSON-string-quote a value for emission into generated source. */
7
+ export const q = (s: string): string => JSON.stringify(s);
8
+
9
+ /** Smithy shape id → local name (`ns#Shape` → `Shape`). */
10
+ export const local = (id: string): string => id.split("#")[1] ?? id;
11
+
12
+ /** Whether a shape id is a smithy prelude shape (`smithy.api#…`). */
13
+ export const isPrelude = (id: string): boolean => id.startsWith("smithy.api#");
14
+
15
+ const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
16
+
17
+ /** Emit a TS object key: bare when a valid identifier, quoted otherwise. */
18
+ export const tsKey = (s: string): string => (IDENT.test(s) ? s : q(s));
19
+
20
+ /** snake_case / kebab-case wire name → camelCase TS-facing name. */
21
+ export const camel = (s: string): string =>
22
+ s.replace(/[_-]+([A-Za-z0-9])/g, (_, c: string) => c.toUpperCase());
23
+
24
+ /** Reserved words that can't be `const` names. */
25
+ export const RESERVED = new Set([
26
+ "break",
27
+ "case",
28
+ "catch",
29
+ "class",
30
+ "const",
31
+ "continue",
32
+ "debugger",
33
+ "default",
34
+ "delete",
35
+ "do",
36
+ "else",
37
+ "enum",
38
+ "export",
39
+ "extends",
40
+ "false",
41
+ "finally",
42
+ "for",
43
+ "function",
44
+ "if",
45
+ "import",
46
+ "in",
47
+ "instanceof",
48
+ "let",
49
+ "new",
50
+ "null",
51
+ "package",
52
+ "return",
53
+ "static",
54
+ "super",
55
+ "switch",
56
+ "this",
57
+ "throw",
58
+ "true",
59
+ "try",
60
+ "typeof",
61
+ "var",
62
+ "void",
63
+ "while",
64
+ "with",
65
+ "yield",
66
+ ]);
67
+
68
+ /**
69
+ * Lower-case the first character for an exported operation name — unless the
70
+ * result is a reserved word, in which case the original casing is kept.
71
+ */
72
+ export const lowerFirst = (s: string): string => {
73
+ const lowered = s.charAt(0).toLowerCase() + s.slice(1);
74
+ return RESERVED.has(lowered) ? s : lowered;
75
+ };
76
+
77
+ /** Upper-case the first character. */
78
+ export const upperFirst = (s: string): string =>
79
+ s.charAt(0).toUpperCase() + s.slice(1);
80
+
81
+ /**
82
+ * Collapse a smithy documentation string to a single safe line for a
83
+ * `/** … *​/` comment.
84
+ */
85
+ export const oneLine = (s: string | undefined): string | undefined =>
86
+ s ? s.replace(/\s+/g, " ").replace(/\*\//g, "*\\/").trim() : undefined;
@@ -0,0 +1,166 @@
1
+ /**
2
+ * OpenAPI → Smithy pipeline helper (dev-time only, provider-agnostic).
3
+ *
4
+ * Owns the spec-side pipeline every OpenAPI-sourced provider shares: read the
5
+ * spec file, apply the RFC-6902 patch chain to the OpenAPI document (v0
6
+ * semantics: `*.patch.json` files in sorted name order, `{ description,
7
+ * patches }` shape; stale targets warn+skip, malformed patches fail the run),
8
+ * run the optional preprocess hook, convert with
9
+ * {@link convertOpenApiToSmithy}, and write `.generated-specs/<name>.json`.
10
+ *
11
+ * A provider's `scripts/convert.ts` is: a `runOpenApiConvert` call. The
12
+ * separate `scripts/generate.ts` (an SdkSpec + `runGeneratorCli`) then
13
+ * compiles the written models. Note: providers whose OpenAPI patches live in
14
+ * `patches/` should pass `patchesDir: false` to `runGeneratorCli` — the
15
+ * patch chain applies HERE, to the OpenAPI document, not to the Smithy model.
16
+ */
17
+ import * as fs from "node:fs/promises";
18
+ import * as path from "node:path";
19
+ import {
20
+ applyOperation,
21
+ isStaleTargetError,
22
+ type PatchFile,
23
+ } from "../json-patch.ts";
24
+ import {
25
+ convertOpenApiToSmithy,
26
+ type OpenApiConvertOptions,
27
+ } from "./openapi.ts";
28
+
29
+ export interface OpenApiSpecEntry {
30
+ /** Output model name — written to `<outDir>/<name>.json`. */
31
+ readonly name: string;
32
+ /** Spec file path, relative to `root` (or absolute). */
33
+ readonly specPath: string;
34
+ /**
35
+ * Hook between patching and conversion (e.g. path prefixing, server
36
+ * rewrites). May mutate the spec in place or return a replacement.
37
+ */
38
+ readonly preprocess?: (spec: any) => unknown | void;
39
+ /** Per-spec converter option overrides (merged over the shared options). */
40
+ readonly options?: Partial<OpenApiConvertOptions>;
41
+ }
42
+
43
+ export interface RunOpenApiConvertOptions {
44
+ /** Absolute package root (usually `path.resolve(import.meta.dir, "..")`). */
45
+ readonly root: string;
46
+ readonly specs: readonly OpenApiSpecEntry[];
47
+ /**
48
+ * RFC-6902 patch chain root, relative to `root`. Default `"patches"`;
49
+ * `false` disables. Layout: `<patchesDir>/<name>/*.patch.json` when the
50
+ * per-spec directory exists; for single-spec providers a flat
51
+ * `<patchesDir>/*.patch.json` also works.
52
+ */
53
+ readonly patchesDir?: string | false;
54
+ /** Output directory, relative to `root`. Default `".generated-specs"`. */
55
+ readonly outDir?: string;
56
+ /**
57
+ * Spec text parser. Default `JSON.parse` — the seam for YAML specs
58
+ * (`parse: (text) => YAML.parse(text)`).
59
+ */
60
+ readonly parse?: (text: string, specPath: string) => unknown;
61
+ /** Shared converter options (per-spec `options` merge over these). */
62
+ readonly options: OpenApiConvertOptions;
63
+ }
64
+
65
+ const exists = async (p: string): Promise<boolean> => {
66
+ try {
67
+ await fs.access(p);
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ };
73
+
74
+ /** Run the OpenAPI→Smithy conversion pipeline (plain async — call from a script). */
75
+ export const runOpenApiConvert = async (
76
+ o: RunOpenApiConvertOptions,
77
+ ): Promise<void> => {
78
+ const outDir = path.resolve(o.root, o.outDir ?? ".generated-specs");
79
+ const patchRoot =
80
+ o.patchesDir === false
81
+ ? undefined
82
+ : path.resolve(o.root, o.patchesDir ?? "patches");
83
+ const parse = o.parse ?? ((text: string) => JSON.parse(text));
84
+
85
+ await fs.mkdir(outDir, { recursive: true });
86
+
87
+ console.log("🛠️ openapi → smithy");
88
+ console.log(` Output: ${outDir}`);
89
+
90
+ for (const entry of o.specs) {
91
+ const specPath = path.resolve(o.root, entry.specPath);
92
+ let spec: any = parse(await fs.readFile(specPath, "utf8"), specPath);
93
+
94
+ // ---- RFC-6902 patch chain (applies to the OpenAPI document) ----
95
+ let patchDir: string | undefined;
96
+ if (patchRoot && (await exists(patchRoot))) {
97
+ const perSpec = path.join(patchRoot, entry.name);
98
+ if (await exists(perSpec)) {
99
+ patchDir = perSpec;
100
+ } else if (o.specs.length === 1) {
101
+ patchDir = patchRoot;
102
+ }
103
+ }
104
+ let patchFileCount = 0;
105
+ let staleOps = 0;
106
+ const badPatches: string[] = [];
107
+ if (patchDir) {
108
+ const files = (await fs.readdir(patchDir))
109
+ .filter((f) => f.endsWith(".patch.json"))
110
+ .sort((a, b) => a.localeCompare(b));
111
+ for (const pf of files) {
112
+ const parsed = JSON.parse(
113
+ await fs.readFile(path.join(patchDir, pf), "utf8"),
114
+ ) as PatchFile;
115
+ for (const patchOp of parsed.patches ?? []) {
116
+ try {
117
+ applyOperation(spec, patchOp);
118
+ } catch (e) {
119
+ const msg = e instanceof Error ? e.message : String(e);
120
+ if (isStaleTargetError(msg)) {
121
+ // Spec drift — the patch target no longer exists upstream.
122
+ staleOps++;
123
+ console.warn(
124
+ ` ⚠️ stale: ${entry.name}/${pf} [${patchOp.op} ${patchOp.path}]`,
125
+ );
126
+ } else {
127
+ badPatches.push(
128
+ `${entry.name}/${pf} [${patchOp.op} ${patchOp.path}]: ${msg}`,
129
+ );
130
+ }
131
+ }
132
+ }
133
+ patchFileCount++;
134
+ }
135
+ }
136
+ if (badPatches.length) {
137
+ for (const b of badPatches) console.error(`❌ bad patch: ${b}`);
138
+ throw new Error(
139
+ `${badPatches.length} malformed patch operation(s) — fix or remove them`,
140
+ );
141
+ }
142
+
143
+ // ---- Preprocess hook ----
144
+ if (entry.preprocess) {
145
+ const replaced = entry.preprocess(spec);
146
+ if (replaced !== undefined) spec = replaced;
147
+ }
148
+
149
+ // ---- Convert + write ----
150
+ const model = convertOpenApiToSmithy(spec, {
151
+ ...o.options,
152
+ ...entry.options,
153
+ });
154
+ const opCount = Object.values(model.shapes).filter(
155
+ (s: any) => s.type === "operation",
156
+ ).length;
157
+ const outPath = path.join(outDir, `${entry.name}.json`);
158
+ await fs.writeFile(outPath, JSON.stringify(model, null, 2) + "\n");
159
+ console.log(
160
+ ` ✅ ${entry.name}: ${opCount} operations, ${Object.keys(model.shapes).length} shapes` +
161
+ (patchFileCount
162
+ ? ` (${patchFileCount} patch file(s) applied${staleOps ? `, ${staleOps} stale op(s) skipped` : ""})`
163
+ : ""),
164
+ );
165
+ }
166
+ };