@apifuse/provider-sdk 2.2.0-beta.41 → 2.2.0-beta.43

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.43
4
+
5
+ - Release candidate for main commit 5b8b6048514fb500ead400495bd5e1ed8695ed6d.
6
+
7
+ ## 2.2.0-beta.42
8
+
9
+ - Release candidate for main commit efbdbb409c9400e6211b061705b634e659f298bd.
10
+
3
11
  ## 2.2.0-beta.41
4
12
 
5
13
  - Release candidate for main commit e1bde60e6a9e0424074a357afd9e8af56549080d.
@@ -551,7 +551,7 @@ function isProviderDefinition(value: unknown): value is ProviderDefinition {
551
551
  return (
552
552
  typeof value.id === "string" &&
553
553
  typeof value.version === "string" &&
554
- (value.runtime === "standard" || value.runtime === "browser") &&
554
+ (value.runtime === "standard" || value.runtime === "shared" || value.runtime === "browser") &&
555
555
  typeof value.meta.displayName === "string" &&
556
556
  typeof value.meta.category === "string"
557
557
  );
@@ -249,7 +249,7 @@ function isProviderDefinition(value: unknown): value is ProviderDefinition {
249
249
  return (
250
250
  typeof value.id === "string" &&
251
251
  typeof value.version === "string" &&
252
- (value.runtime === "standard" || value.runtime === "browser") &&
252
+ (value.runtime === "standard" || value.runtime === "shared" || value.runtime === "browser") &&
253
253
  typeof value.meta.displayName === "string" &&
254
254
  typeof value.meta.category === "string"
255
255
  );
@@ -3,24 +3,48 @@
3
3
  /**
4
4
  * `apifuse migrate-shape [path] [--check] [--json]`
5
5
  *
6
- * Applies the provider authoring-shape migration (single-phase
7
- * `defineProvider({...operations})` to the two-phase declaration builder) to
8
- * a provider's `index.ts`. This is the source half of a breaking SDK bump:
9
- * the pin bump and this transform must land in the same commit, because the
10
- * single-phase shape default-exports a builder function under 2.2.0-beta.37+
11
- * and the module stops loading.
6
+ * Applies the provider authoring-shape migration for the phase-separated SDK
7
+ * (2.2.0-beta.37+) to a provider repository. Two coordinated transforms:
12
8
  *
13
- * Exit codes: 0 migrated or already two-phase; 1 skipped (the transform
14
- * refuses to guess) or the file is missing. `--check` reports without
9
+ * 1. `index.ts`: single-phase `defineProvider({...operations})` becomes the
10
+ * two-phase declaration builder. The old shape default-exports a builder
11
+ * FUNCTION under the new SDK, so the module stops loading.
12
+ * 2. Every provider source file: legacy `defineOperation(config)` /
13
+ * `defineStreamOperation(config)` become the curried
14
+ * `defineOperation<ProviderContext>()(config)`. The legacy call returns
15
+ * the inner factory with the config swallowed, so every operation in the
16
+ * map turns into a function and finalizeProvider rejects the provider
17
+ * with a misleading health-check error.
18
+ *
19
+ * Both halves belong to the same SDK bump: this transform and the pin bump
20
+ * must land in one commit.
21
+ *
22
+ * Exit codes: 0 migrated or already migrated; 1 any skip (the transform
23
+ * refuses to guess) or a missing index.ts. `--check` reports without
15
24
  * writing. A skip is a hard stop for fan-out callers — never pair a pin bump
16
25
  * with a skipped migration.
17
26
  */
18
27
 
19
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
20
- import { resolve } from "node:path";
28
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
29
+ import { dirname, join, relative, resolve } from "node:path";
21
30
 
31
+ import { migrateOperationShape } from "../src/cli/migrate-operation-shape.js";
22
32
  import { migrateProviderShape } from "../src/cli/migrate-provider-shape.js";
23
33
 
34
+ const SOURCE_SKIP_DIRECTORIES = new Set([
35
+ "node_modules",
36
+ "__tests__",
37
+ "__fixtures__",
38
+ ".git",
39
+ "dist",
40
+ ]);
41
+
42
+ type FileOutcome = {
43
+ readonly path: string;
44
+ readonly status: string;
45
+ readonly detail?: string;
46
+ };
47
+
24
48
  export async function main(): Promise<void> {
25
49
  const args = process.argv.slice(3);
26
50
  const check = args.includes("--check");
@@ -31,7 +55,9 @@ export async function main(): Promise<void> {
31
55
 
32
56
  const report = (payload: Record<string, unknown>, humanText: string): void => {
33
57
  if (json) {
34
- console.log(JSON.stringify({ schemaVersion: 1, indexPath, ...payload }));
58
+ console.log(
59
+ JSON.stringify({ schemaVersion: 2, providerRoot, ...payload }),
60
+ );
35
61
  } else {
36
62
  console.log(humanText);
37
63
  }
@@ -45,40 +71,132 @@ export async function main(): Promise<void> {
45
71
  process.exit(1);
46
72
  }
47
73
 
48
- const sourceText = readFileSync(indexPath, "utf8");
49
- const result = migrateProviderShape(sourceText, indexPath);
74
+ // ── Pass 1: provider two-phase shape on index.ts
75
+ const indexSource = readFileSync(indexPath, "utf8");
76
+ const providerResult = migrateProviderShape(indexSource, indexPath);
50
77
 
51
- if (result.status === "skipped") {
78
+ if (providerResult.status === "skipped") {
52
79
  report(
53
- { status: "skipped", reason: result.reason },
54
- `migrate-shape: skipped — ${result.reason}\nThis provider needs a manual migration; do not bump its SDK pin without one.`,
80
+ { status: "skipped", stage: "provider-shape", reason: providerResult.reason },
81
+ `migrate-shape: skipped — ${providerResult.reason}\nThis provider needs a manual migration; do not bump its SDK pin without one.`,
55
82
  );
56
83
  process.exit(1);
57
84
  }
58
85
 
59
- if (result.status === "unchanged") {
86
+ // ── Pass 2: operation currying across the provider's source tree.
87
+ // The index.ts input for this pass is pass 1's OUTPUT, so a provider
88
+ // whose index both holds the declaration and defines operations gets
89
+ // both transforms in one write.
90
+ const indexAfterProvider = providerResult.code;
91
+ const outcomes: FileOutcome[] = [];
92
+ const pendingWrites = new Map<string, string>();
93
+ let operationRewrites = 0;
94
+
95
+ for (const sourcePath of collectSourceFiles(providerRoot)) {
96
+ const isIndex = sourcePath === indexPath;
97
+ const input = isIndex
98
+ ? indexAfterProvider
99
+ : readFileSync(sourcePath, "utf8");
100
+ // Operation modules import the alias from the provider entry; the
101
+ // entry file declares it itself.
102
+ const contextSpecifier = isIndex
103
+ ? "./index"
104
+ : relativeImportToIndex(sourcePath, providerRoot);
105
+ const result = migrateOperationShape(input, sourcePath, contextSpecifier);
106
+
107
+ if (result.status === "skipped") {
108
+ report(
109
+ {
110
+ status: "skipped",
111
+ stage: "operation-shape",
112
+ file: relative(providerRoot, sourcePath),
113
+ reason: result.reason,
114
+ },
115
+ `migrate-shape: skipped at ${relative(providerRoot, sourcePath)} — ${result.reason}\nThis provider needs a manual migration; do not bump its SDK pin without one.`,
116
+ );
117
+ process.exit(1);
118
+ }
119
+ if (result.status === "migrated") {
120
+ operationRewrites += result.rewrites;
121
+ pendingWrites.set(sourcePath, result.code);
122
+ outcomes.push({
123
+ path: relative(providerRoot, sourcePath),
124
+ status: "curried",
125
+ detail: `${result.rewrites} call site(s)`,
126
+ });
127
+ } else if (isIndex && providerResult.status === "migrated") {
128
+ // Provider shape changed even though no operation calls did.
129
+ pendingWrites.set(sourcePath, result.code);
130
+ }
131
+ }
132
+
133
+ const providerChanged = providerResult.status === "migrated";
134
+ const anythingChanged = providerChanged || operationRewrites > 0;
135
+
136
+ if (!anythingChanged) {
60
137
  report(
61
138
  { status: "unchanged" },
62
- "migrate-shape: already two-phase; nothing to do.",
139
+ "migrate-shape: already migrated; nothing to do.",
63
140
  );
64
141
  return;
65
142
  }
66
143
 
67
144
  if (check) {
68
145
  report(
69
- { status: "would-migrate", kind: result.kind },
70
- `migrate-shape: would migrate (${result.kind}). Run without --check to write.`,
146
+ {
147
+ status: "would-migrate",
148
+ providerShape: providerChanged ? providerResult.kind : "unchanged",
149
+ operationCallSites: operationRewrites,
150
+ files: outcomes,
151
+ },
152
+ `migrate-shape: would migrate (provider: ${providerChanged ? providerResult.kind : "unchanged"}, operation call sites: ${operationRewrites}). Run without --check to write.`,
71
153
  );
72
154
  return;
73
155
  }
74
156
 
75
- writeFileSync(indexPath, result.code, "utf8");
157
+ for (const [path, code] of pendingWrites) {
158
+ writeFileSync(path, code, "utf8");
159
+ }
76
160
  report(
77
- { status: "migrated", kind: result.kind },
78
- `migrate-shape: migrated (${result.kind}). Review the diff, then run \`apifuse check\` and \`bun test\`.`,
161
+ {
162
+ status: "migrated",
163
+ providerShape: providerChanged ? providerResult.kind : "unchanged",
164
+ operationCallSites: operationRewrites,
165
+ files: outcomes,
166
+ },
167
+ `migrate-shape: migrated (provider: ${providerChanged ? providerResult.kind : "unchanged"}, operation call sites: ${operationRewrites} across ${pendingWrites.size} file(s)). Review the diff, then run \`apifuse check\` and \`bun test\`.`,
79
168
  );
80
169
  }
81
170
 
171
+ /** Provider-authored .ts sources, excluding tests, fixtures, and build output. */
172
+ function collectSourceFiles(root: string): string[] {
173
+ const files: string[] = [];
174
+ const walk = (directory: string): void => {
175
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
176
+ if (entry.isDirectory()) {
177
+ if (SOURCE_SKIP_DIRECTORIES.has(entry.name)) continue;
178
+ walk(join(directory, entry.name));
179
+ continue;
180
+ }
181
+ if (!entry.name.endsWith(".ts")) continue;
182
+ if (entry.name.endsWith(".d.ts")) continue;
183
+ if (entry.name.endsWith(".test.ts")) continue;
184
+ files.push(join(directory, entry.name));
185
+ }
186
+ };
187
+ walk(root);
188
+ return files.sort();
189
+ }
190
+
191
+ /** `operations/foo.ts` -> `../index`; `operations/a/b.ts` -> `../../index`. */
192
+ function relativeImportToIndex(sourcePath: string, providerRoot: string): string {
193
+ const fromDirectory = dirname(sourcePath);
194
+ let specifier = relative(fromDirectory, join(providerRoot, "index"));
195
+ specifier = specifier.split("\\").join("/");
196
+ if (!specifier.startsWith(".")) specifier = `./${specifier}`;
197
+ return specifier;
198
+ }
199
+
82
200
  if (import.meta.main) {
83
201
  await main();
84
202
  }