@apifuse/provider-sdk 2.2.0-beta.40 → 2.2.0-beta.42
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 +8 -0
- package/bin/apifuse-check.ts +61 -0
- package/bin/apifuse-migrate-shape.ts +202 -0
- package/bin/apifuse-submit-check.ts +1773 -222
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.js +6 -0
- package/dist/cli/migrate-operation-shape.d.ts +44 -0
- package/dist/cli/migrate-operation-shape.js +113 -0
- package/dist/cli/migrate-provider-shape.d.ts +52 -0
- package/dist/cli/migrate-provider-shape.js +578 -0
- package/dist/cli/templates/provider/provider.json.tpl +6 -0
- package/dist/contract.js +1 -0
- package/dist/define.js +22 -1
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/errors.d.ts +15 -0
- package/dist/fixture-sanitization.js +13 -3
- package/dist/index.d.ts +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/runtime/executor.js +11 -1
- package/dist/server/error-observability.d.ts +1 -0
- package/dist/server/error-observability.js +1 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/self-test.js +3 -0
- package/dist/server/serve-implementation.d.ts +12 -0
- package/dist/server/serve-implementation.js +174 -66
- package/dist/types.d.ts +18 -10
- package/package.json +1 -1
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +6 -0
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +772 -0
- package/src/cli/templates/provider/provider.json.tpl +6 -0
- package/src/contract.ts +1 -0
- package/src/define.ts +33 -1
- package/src/error-observability.ts +64 -0
- package/src/errors.ts +16 -0
- package/src/fixture-sanitization.ts +19 -3
- package/src/index.ts +1 -0
- package/src/provider.ts +1 -0
- package/src/runtime/executor.ts +13 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/index.ts +2 -0
- package/src/server/self-test.ts +5 -0
- package/src/server/serve-implementation.ts +214 -84
- package/src/types.ts +38 -27
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0-beta.42
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit efbdbb409c9400e6211b061705b634e659f298bd.
|
|
6
|
+
|
|
7
|
+
## 2.2.0-beta.41
|
|
8
|
+
|
|
9
|
+
- Release candidate for main commit e1bde60e6a9e0424074a357afd9e8af56549080d.
|
|
10
|
+
|
|
3
11
|
## 2.2.0-beta.40
|
|
4
12
|
|
|
5
13
|
- Release candidate for main commit 7e2f05a954c23593a77e89e463a9bcbc2cdc7695.
|
package/bin/apifuse-check.ts
CHANGED
|
@@ -123,6 +123,7 @@ export async function runChecks(
|
|
|
123
123
|
const indexPath = resolve(providerRoot, "index.ts");
|
|
124
124
|
const dockerfilePath = resolve(providerRoot, "Dockerfile");
|
|
125
125
|
const packageJsonPath = resolve(providerRoot, "package.json");
|
|
126
|
+
const providerJsonPath = resolve(providerRoot, "provider.json");
|
|
126
127
|
|
|
127
128
|
let providerModule: Record<string, unknown> | undefined;
|
|
128
129
|
let providerImportError: unknown;
|
|
@@ -150,6 +151,7 @@ export async function runChecks(
|
|
|
150
151
|
checkProviderMetadata(provider),
|
|
151
152
|
checkDockerfile(dockerfilePath),
|
|
152
153
|
checkPackageJson(packageJsonPath),
|
|
154
|
+
checkProviderJson(providerJsonPath, packageJsonPath),
|
|
153
155
|
checkPromptAssets(providerRoot),
|
|
154
156
|
];
|
|
155
157
|
}
|
|
@@ -478,6 +480,65 @@ function checkPackageJson(packageJsonPath: string): CheckResult {
|
|
|
478
480
|
}
|
|
479
481
|
}
|
|
480
482
|
|
|
483
|
+
export const PROVIDER_JSON_CHECK_MESSAGE = "provider.json exists with a valid declaration";
|
|
484
|
+
|
|
485
|
+
const providerDeclarationSchema = z
|
|
486
|
+
.object({
|
|
487
|
+
schemaVersion: z.literal(1),
|
|
488
|
+
providerId: z.string(),
|
|
489
|
+
owner: z.string(),
|
|
490
|
+
lifecycle: z.enum(["draft", "ready", "live", "retired"]),
|
|
491
|
+
})
|
|
492
|
+
.strict();
|
|
493
|
+
|
|
494
|
+
function checkProviderJson(providerJsonPath: string, packageJsonPath: string): CheckResult {
|
|
495
|
+
if (!existsSync(providerJsonPath)) {
|
|
496
|
+
return {
|
|
497
|
+
message: PROVIDER_JSON_CHECK_MESSAGE,
|
|
498
|
+
passed: false,
|
|
499
|
+
details: ["Missing provider.json at the provider root"],
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const declaration = providerDeclarationSchema.parse(
|
|
505
|
+
JSON.parse(readFileSync(providerJsonPath, "utf-8")) as unknown,
|
|
506
|
+
);
|
|
507
|
+
const expectedProviderId = readProviderIdFromPackageName(packageJsonPath);
|
|
508
|
+
if (expectedProviderId !== undefined && declaration.providerId !== expectedProviderId) {
|
|
509
|
+
return {
|
|
510
|
+
message: PROVIDER_JSON_CHECK_MESSAGE,
|
|
511
|
+
passed: false,
|
|
512
|
+
details: [
|
|
513
|
+
`provider.json providerId "${declaration.providerId}" does not match package.json name (expected "${expectedProviderId}")`,
|
|
514
|
+
],
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
message: PROVIDER_JSON_CHECK_MESSAGE,
|
|
520
|
+
passed: true,
|
|
521
|
+
details: [`providerId: ${declaration.providerId}`, `lifecycle: ${declaration.lifecycle}`],
|
|
522
|
+
};
|
|
523
|
+
} catch (error) {
|
|
524
|
+
return {
|
|
525
|
+
message: PROVIDER_JSON_CHECK_MESSAGE,
|
|
526
|
+
passed: false,
|
|
527
|
+
details: [error instanceof Error ? error.message : String(error)],
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function readProviderIdFromPackageName(packageJsonPath: string): string | undefined {
|
|
533
|
+
try {
|
|
534
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as unknown;
|
|
535
|
+
if (!isRecord(packageJson) || typeof packageJson.name !== "string") return undefined;
|
|
536
|
+
return /^(?:apifuse-provider-|@apifuse\/provider-)(.+)$/.exec(packageJson.name)?.[1];
|
|
537
|
+
} catch {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
481
542
|
function assertProviderDefinition(value: unknown): ProviderDefinition | undefined {
|
|
482
543
|
return isProviderDefinition(value) ? value : undefined;
|
|
483
544
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `apifuse migrate-shape [path] [--check] [--json]`
|
|
5
|
+
*
|
|
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:
|
|
8
|
+
*
|
|
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
|
|
24
|
+
* writing. A skip is a hard stop for fan-out callers — never pair a pin bump
|
|
25
|
+
* with a skipped migration.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
30
|
+
|
|
31
|
+
import { migrateOperationShape } from "../src/cli/migrate-operation-shape.js";
|
|
32
|
+
import { migrateProviderShape } from "../src/cli/migrate-provider-shape.js";
|
|
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
|
+
|
|
48
|
+
export async function main(): Promise<void> {
|
|
49
|
+
const args = process.argv.slice(3);
|
|
50
|
+
const check = args.includes("--check");
|
|
51
|
+
const json = args.includes("--json");
|
|
52
|
+
const positional = args.filter((argument) => !argument.startsWith("--"));
|
|
53
|
+
const providerRoot = resolve(positional[0] ?? ".");
|
|
54
|
+
const indexPath = resolve(providerRoot, "index.ts");
|
|
55
|
+
|
|
56
|
+
const report = (payload: Record<string, unknown>, humanText: string): void => {
|
|
57
|
+
if (json) {
|
|
58
|
+
console.log(
|
|
59
|
+
JSON.stringify({ schemaVersion: 2, providerRoot, ...payload }),
|
|
60
|
+
);
|
|
61
|
+
} else {
|
|
62
|
+
console.log(humanText);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (!existsSync(indexPath)) {
|
|
67
|
+
report(
|
|
68
|
+
{ status: "skipped", reason: "index.ts not found" },
|
|
69
|
+
`migrate-shape: ${indexPath} not found.`,
|
|
70
|
+
);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Pass 1: provider two-phase shape on index.ts
|
|
75
|
+
const indexSource = readFileSync(indexPath, "utf8");
|
|
76
|
+
const providerResult = migrateProviderShape(indexSource, indexPath);
|
|
77
|
+
|
|
78
|
+
if (providerResult.status === "skipped") {
|
|
79
|
+
report(
|
|
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.`,
|
|
82
|
+
);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
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) {
|
|
137
|
+
report(
|
|
138
|
+
{ status: "unchanged" },
|
|
139
|
+
"migrate-shape: already migrated; nothing to do.",
|
|
140
|
+
);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (check) {
|
|
145
|
+
report(
|
|
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.`,
|
|
153
|
+
);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const [path, code] of pendingWrites) {
|
|
158
|
+
writeFileSync(path, code, "utf8");
|
|
159
|
+
}
|
|
160
|
+
report(
|
|
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\`.`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
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
|
+
|
|
200
|
+
if (import.meta.main) {
|
|
201
|
+
await main();
|
|
202
|
+
}
|