@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/apifuse-check.ts +61 -0
  3. package/bin/apifuse-migrate-shape.ts +202 -0
  4. package/bin/apifuse-submit-check.ts +1773 -222
  5. package/dist/cli/commands.d.ts +1 -1
  6. package/dist/cli/commands.js +8 -0
  7. package/dist/cli/create.js +6 -0
  8. package/dist/cli/migrate-operation-shape.d.ts +44 -0
  9. package/dist/cli/migrate-operation-shape.js +113 -0
  10. package/dist/cli/migrate-provider-shape.d.ts +52 -0
  11. package/dist/cli/migrate-provider-shape.js +578 -0
  12. package/dist/cli/templates/provider/provider.json.tpl +6 -0
  13. package/dist/contract.js +1 -0
  14. package/dist/define.js +22 -1
  15. package/dist/error-observability.d.ts +7 -0
  16. package/dist/error-observability.js +61 -0
  17. package/dist/errors.d.ts +15 -0
  18. package/dist/fixture-sanitization.js +13 -3
  19. package/dist/index.d.ts +1 -1
  20. package/dist/provider.d.ts +1 -1
  21. package/dist/runtime/executor.js +11 -1
  22. package/dist/server/error-observability.d.ts +1 -0
  23. package/dist/server/error-observability.js +1 -0
  24. package/dist/server/index.d.ts +2 -1
  25. package/dist/server/self-test.js +3 -0
  26. package/dist/server/serve-implementation.d.ts +12 -0
  27. package/dist/server/serve-implementation.js +174 -66
  28. package/dist/types.d.ts +18 -10
  29. package/package.json +1 -1
  30. package/src/cli/commands.ts +10 -0
  31. package/src/cli/create.ts +6 -0
  32. package/src/cli/migrate-operation-shape.ts +184 -0
  33. package/src/cli/migrate-provider-shape.ts +772 -0
  34. package/src/cli/templates/provider/provider.json.tpl +6 -0
  35. package/src/contract.ts +1 -0
  36. package/src/define.ts +33 -1
  37. package/src/error-observability.ts +64 -0
  38. package/src/errors.ts +16 -0
  39. package/src/fixture-sanitization.ts +19 -3
  40. package/src/index.ts +1 -0
  41. package/src/provider.ts +1 -0
  42. package/src/runtime/executor.ts +13 -1
  43. package/src/server/error-observability.ts +1 -0
  44. package/src/server/index.ts +2 -0
  45. package/src/server/self-test.ts +5 -0
  46. package/src/server/serve-implementation.ts +214 -84
  47. package/src/types.ts +38 -27
@@ -1,4 +1,4 @@
1
- export type ApifuseCommandName = "create" | "dev" | "check" | "sync-assets" | "submit-check" | "bounty-check" | "record" | "test" | "perf";
1
+ export type ApifuseCommandName = "create" | "dev" | "check" | "sync-assets" | "migrate-shape" | "submit-check" | "bounty-check" | "record" | "test" | "perf";
2
2
  export type ApifuseCommandManifest = {
3
3
  name: ApifuseCommandName;
4
4
  summary: string;
@@ -30,6 +30,13 @@ export const COMMAND_MANIFEST = {
30
30
  examples: ["apifuse sync-assets .", "apifuse sync-assets . --check"],
31
31
  modulePath: "./apifuse-sync-assets",
32
32
  },
33
+ "migrate-shape": {
34
+ name: "migrate-shape",
35
+ summary: "Migrate a provider to the phase-separated SDK: two-phase defineProvider in index.ts and curried defineOperation across sources.",
36
+ usage: "apifuse migrate-shape [path] [--check] [--json]",
37
+ examples: ["apifuse migrate-shape .", "apifuse migrate-shape . --check"],
38
+ modulePath: "./apifuse-migrate-shape",
39
+ },
33
40
  "submit-check": {
34
41
  name: "submit-check",
35
42
  summary: "Score provider bounty submission readiness and emit checklist evidence.",
@@ -81,6 +88,7 @@ export const COMMAND_ORDER = [
81
88
  "dev",
82
89
  "check",
83
90
  "sync-assets",
91
+ "migrate-shape",
84
92
  "submit-check",
85
93
  "record",
86
94
  "test",
@@ -379,6 +379,12 @@ export async function buildProviderCreatePlan(options, cwd) {
379
379
  sdkSpecifier,
380
380
  }),
381
381
  },
382
+ {
383
+ path: resolve(providerRoot, "provider.json"),
384
+ content: await renderTemplate("provider.json.tpl", {
385
+ PROVIDER_ID: options.name,
386
+ }),
387
+ },
382
388
  {
383
389
  path: resolve(providerRoot, "Dockerfile"),
384
390
  content: await renderTemplate("Dockerfile.tpl", {}),
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Operation authoring-shape migration.
3
+ *
4
+ * `defineOperation` / `defineStreamOperation` changed alongside the provider
5
+ * builder in 2.2.0-beta.37: the identity helper became a curried,
6
+ * context-typed factory.
7
+ *
8
+ * before: defineOperation({ ...config })
9
+ * after: defineOperation<ProviderContext>()({ ...config })
10
+ *
11
+ * The old call still type-checks against an older pin, but under the new SDK
12
+ * it returns the INNER FACTORY FUNCTION with the config swallowed as an
13
+ * ignored argument, so every operation in the map becomes a function and the
14
+ * provider fails `finalizeProvider` with a misleading "declares neither
15
+ * healthCheck nor healthCheckUnsupported" error. Measured fleet blast radius
16
+ * (2026-08-29): 53 of 84 repositories, 450 legacy call sites.
17
+ *
18
+ * Same contract as the provider-shape transform: rewrite only what is fully
19
+ * understood, report a reasoned skip otherwise, never emit a partial file.
20
+ */
21
+ export type OperationShapeMigration = {
22
+ readonly status: "migrated";
23
+ readonly code: string;
24
+ /** Number of call sites rewritten in this file. */
25
+ readonly rewrites: number;
26
+ /** True when a `ProviderContext` type import was added. */
27
+ readonly importAdded: boolean;
28
+ } | {
29
+ readonly status: "unchanged";
30
+ readonly code: string;
31
+ } | {
32
+ readonly status: "skipped";
33
+ readonly reason: string;
34
+ };
35
+ /**
36
+ * Migrate legacy `defineOperation(config)` calls in one source file to the
37
+ * curried `defineOperation<ProviderContext>()(config)` form.
38
+ *
39
+ * @param contextImportSpecifier module specifier the `ProviderContext` type
40
+ * import should come from when one has to be added — `"../index"` for
41
+ * operation modules, `"./index"` is never needed because index.ts declares
42
+ * the alias itself. Callers pass the correct relative path per file.
43
+ */
44
+ export declare function migrateOperationShape(sourceText: string, fileName: string, contextImportSpecifier: string): OperationShapeMigration;
@@ -0,0 +1,113 @@
1
+ const ts = await loadTypeScript();
2
+ async function loadTypeScript() {
3
+ try {
4
+ return await import("typescript");
5
+ }
6
+ catch {
7
+ console.error("apifuse migrate-shape requires typescript; install it in the workspace running the CLI (bun add -d typescript)");
8
+ process.exit(1);
9
+ }
10
+ }
11
+ const OPERATION_HELPERS = new Set(["defineOperation", "defineStreamOperation"]);
12
+ const PROVIDER_CONTEXT_TYPE_NAME = "ProviderContext";
13
+ const PROVIDER_SDK_PROVIDER_SUBPATH = "@apifuse/provider-sdk/provider";
14
+ /**
15
+ * Migrate legacy `defineOperation(config)` calls in one source file to the
16
+ * curried `defineOperation<ProviderContext>()(config)` form.
17
+ *
18
+ * @param contextImportSpecifier module specifier the `ProviderContext` type
19
+ * import should come from when one has to be added — `"../index"` for
20
+ * operation modules, `"./index"` is never needed because index.ts declares
21
+ * the alias itself. Callers pass the correct relative path per file.
22
+ */
23
+ export function migrateOperationShape(sourceText, fileName, contextImportSpecifier) {
24
+ const source = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
25
+ const parseError = firstSyntaxError(source);
26
+ if (parseError !== undefined) {
27
+ return { status: "skipped", reason: parseError };
28
+ }
29
+ // Every legacy call site: a direct call to the bare helper identifier
30
+ // whose single argument is the config (i.e. NOT the curried form, whose
31
+ // outer call has zero arguments and optional type arguments).
32
+ const legacyCalls = [];
33
+ let sawCurried = false;
34
+ const visit = (node) => {
35
+ if (ts.isCallExpression(node) &&
36
+ ts.isIdentifier(node.expression) &&
37
+ OPERATION_HELPERS.has(node.expression.text)) {
38
+ if (node.arguments.length === 0) {
39
+ sawCurried = true; // already-migrated outer call
40
+ }
41
+ else {
42
+ legacyCalls.push(node);
43
+ }
44
+ }
45
+ ts.forEachChild(node, visit);
46
+ };
47
+ visit(source);
48
+ if (legacyCalls.length === 0) {
49
+ return { status: "unchanged", code: sourceText };
50
+ }
51
+ // A file mixing both forms almost certainly had a partial hand-migration;
52
+ // refuse rather than guess which convention the author wants.
53
+ if (sawCurried) {
54
+ return {
55
+ status: "skipped",
56
+ reason: "File mixes legacy defineOperation(config) with curried defineOperation<...>()(config); migrate it by hand.",
57
+ };
58
+ }
59
+ const declaresContextAlias = source.statements.some((statement) => ts.isTypeAliasDeclaration(statement) &&
60
+ statement.name.text === PROVIDER_CONTEXT_TYPE_NAME);
61
+ const importsContext = sourceText.includes(PROVIDER_CONTEXT_TYPE_NAME);
62
+ const needsImport = !declaresContextAlias && !importsContext;
63
+ const edits = [];
64
+ for (const call of legacyCalls) {
65
+ // `defineOperation(` -> `defineOperation<ProviderContext>()(`
66
+ const callee = call.expression;
67
+ edits.push({
68
+ start: callee.getEnd(),
69
+ end: callee.getEnd(),
70
+ text: `<${PROVIDER_CONTEXT_TYPE_NAME}>()`,
71
+ });
72
+ }
73
+ let importAdded = false;
74
+ if (needsImport) {
75
+ const lastImport = [...source.statements]
76
+ .reverse()
77
+ .find((statement) => ts.isImportDeclaration(statement));
78
+ const insertAt = lastImport ? lastImport.getEnd() : 0;
79
+ const importText = `${lastImport ? "\n" : ""}import type { ${PROVIDER_CONTEXT_TYPE_NAME} } from "${contextImportSpecifier}";${lastImport ? "" : "\n"}`;
80
+ edits.push({ start: insertAt, end: insertAt, text: importText });
81
+ importAdded = true;
82
+ }
83
+ const ordered = edits.sort((a, b) => b.start - a.start);
84
+ let code = sourceText;
85
+ for (const edit of ordered) {
86
+ code = code.slice(0, edit.start) + edit.text + code.slice(edit.end);
87
+ }
88
+ const verified = ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
89
+ const outputError = firstSyntaxError(verified);
90
+ if (outputError !== undefined) {
91
+ return {
92
+ status: "skipped",
93
+ reason: `Transform produced source that does not parse (${outputError}); refusing to emit a partial migration.`,
94
+ };
95
+ }
96
+ return {
97
+ status: "migrated",
98
+ code,
99
+ rewrites: legacyCalls.length,
100
+ importAdded,
101
+ };
102
+ }
103
+ function firstSyntaxError(source) {
104
+ const diagnostics = source.parseDiagnostics;
105
+ if (diagnostics === undefined || diagnostics.length === 0)
106
+ return undefined;
107
+ const first = diagnostics[0];
108
+ if (first === undefined)
109
+ return undefined;
110
+ const message = ts.flattenDiagnosticMessageText(first.messageText, " ");
111
+ const { line } = source.getLineAndCharacterOfPosition(first.start);
112
+ return `${message} (line ${line + 1})`;
113
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Provider authoring shape migration.
3
+ *
4
+ * `defineProvider` changed in 2.2.0-beta.37 from returning a finished provider
5
+ * to returning a builder, splitting authoring into two phases:
6
+ *
7
+ * const buildProvider = defineProvider(<declaration>);
8
+ * export type ProviderContext = ProviderContextOf<typeof buildProvider>;
9
+ * export default buildProvider({ operations });
10
+ *
11
+ * Source written against the single-phase shape still type-checks against an
12
+ * older pin but default-exports a builder function once the pin moves, so
13
+ * `loadProviderDefinition` rejects it. Bumping the pin without migrating the
14
+ * source therefore breaks the module. This transform performs the source half
15
+ * so the SDK bump fan-out can ship both in one commit.
16
+ *
17
+ * The transform is deliberately conservative: it rewrites only the shapes it
18
+ * can fully account for and reports `skipped` with a reason for anything else,
19
+ * rather than emitting a partial migration a reviewer would have to audit.
20
+ */
21
+ /** Every source shape this transform recognizes. */
22
+ export type ProviderShapeKind =
23
+ /** `export default defineProvider({ ..., operations })` */
24
+ "single-phase-default-export"
25
+ /** `const p = defineProvider({ ..., operations }); export default p;` */
26
+ | "single-phase-variable-export"
27
+ /** `const p = defineProvider({ ..., operations }); export default { ...p, deployment };` */
28
+ | "single-phase-variable-spread-export"
29
+ /** Already `const b = defineProvider(...); export default b({ operations })` */
30
+ | "two-phase";
31
+ export type ProviderShapeMigration = {
32
+ readonly status: "migrated";
33
+ readonly kind: ProviderShapeKind;
34
+ readonly code: string;
35
+ /** Source text the operations map was supplied as, for reporting. */
36
+ readonly operationsExpression: string;
37
+ } | {
38
+ readonly status: "unchanged";
39
+ readonly kind: "two-phase";
40
+ readonly code: string;
41
+ } | {
42
+ readonly status: "skipped";
43
+ readonly reason: string;
44
+ };
45
+ /**
46
+ * Migrate one provider `index.ts` to the two-phase authoring shape.
47
+ *
48
+ * Returns the rewritten source on success. Callers MUST treat `skipped` as a
49
+ * hard stop for that provider — a skipped provider needs a human, and pairing
50
+ * a pin bump with a skipped migration produces an unloadable module.
51
+ */
52
+ export declare function migrateProviderShape(sourceText: string, fileName?: string): ProviderShapeMigration;