@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
|
@@ -0,0 +1,578 @@
|
|
|
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 DECLARATION_BUILDER_NAME = "buildProvider";
|
|
12
|
+
const PROVIDER_CONTEXT_TYPE_NAME = "ProviderContext";
|
|
13
|
+
const PROVIDER_CONTEXT_OF_TYPE_NAME = "ProviderContextOf";
|
|
14
|
+
const PROVIDER_SDK_PROVIDER_SUBPATH = "@apifuse/provider-sdk/provider";
|
|
15
|
+
/**
|
|
16
|
+
* Migrate one provider `index.ts` to the two-phase authoring shape.
|
|
17
|
+
*
|
|
18
|
+
* Returns the rewritten source on success. Callers MUST treat `skipped` as a
|
|
19
|
+
* hard stop for that provider — a skipped provider needs a human, and pairing
|
|
20
|
+
* a pin bump with a skipped migration produces an unloadable module.
|
|
21
|
+
*/
|
|
22
|
+
export function migrateProviderShape(sourceText, fileName = "index.ts") {
|
|
23
|
+
const source = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest,
|
|
24
|
+
/* setParentNodes */ true, ts.ScriptKind.TS);
|
|
25
|
+
const syntaxError = firstSyntaxError(source);
|
|
26
|
+
if (syntaxError !== undefined) {
|
|
27
|
+
return { status: "skipped", reason: syntaxError };
|
|
28
|
+
}
|
|
29
|
+
const calls = collectDefineProviderCalls(source);
|
|
30
|
+
if (calls.length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
status: "skipped",
|
|
33
|
+
reason: "No defineProvider(...) call found; this file does not declare a provider.",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (calls.length > 1) {
|
|
37
|
+
return {
|
|
38
|
+
status: "skipped",
|
|
39
|
+
reason: `Found ${calls.length} defineProvider(...) calls; the transform rewrites exactly one declaration.`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const call = calls[0];
|
|
43
|
+
if (call === undefined) {
|
|
44
|
+
return { status: "skipped", reason: "Internal: defineProvider call vanished." };
|
|
45
|
+
}
|
|
46
|
+
const exportAssignment = findDefaultExport(source);
|
|
47
|
+
if (exportAssignment === undefined) {
|
|
48
|
+
return {
|
|
49
|
+
status: "skipped",
|
|
50
|
+
reason: "No `export default` found; the provider module must default-export its provider.",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const declaration = call.arguments[0];
|
|
54
|
+
if (declaration === undefined || !ts.isObjectLiteralExpression(declaration)) {
|
|
55
|
+
return {
|
|
56
|
+
status: "skipped",
|
|
57
|
+
reason: "defineProvider(...) is called with a non-literal argument, so the declaration's operations key cannot be located.",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const operationsProperty = findOperationsProperty(declaration);
|
|
61
|
+
// Already migrated: the declaration carries no operations key and the
|
|
62
|
+
// default export path calls a builder variable rather than defineProvider.
|
|
63
|
+
if (operationsProperty === undefined &&
|
|
64
|
+
isAlreadyTwoPhase(source, exportAssignment, call)) {
|
|
65
|
+
return { status: "unchanged", kind: "two-phase", code: sourceText };
|
|
66
|
+
}
|
|
67
|
+
if (operationsProperty === undefined) {
|
|
68
|
+
return {
|
|
69
|
+
status: "skipped",
|
|
70
|
+
reason: "The declaration has no `operations` key and the default export does not call a declaration builder, so the intended shape is ambiguous.",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const operationsText = operationsPropertyValueText(operationsProperty, source);
|
|
74
|
+
if (operationsText === undefined) {
|
|
75
|
+
return {
|
|
76
|
+
status: "skipped",
|
|
77
|
+
reason: "The `operations` property uses a form the transform cannot relocate (getter, setter, method, spread, or computed name).",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const shape = classifyShape(source, call, exportAssignment);
|
|
81
|
+
if (shape.status === "skipped") {
|
|
82
|
+
return shape;
|
|
83
|
+
}
|
|
84
|
+
const edits = [];
|
|
85
|
+
const builderName = pickBuilderName(source);
|
|
86
|
+
// 1. Remove `operations` from the declaration literal.
|
|
87
|
+
edits.push(...removeOperationsProperty(operationsProperty, declaration, source));
|
|
88
|
+
// 2. Bind the declaration to `const buildProvider = defineProvider({...})`.
|
|
89
|
+
edits.push(...introduceBuilder(source, call, shape, builderName));
|
|
90
|
+
// 3. Route the default export through `buildProvider({ operations })`.
|
|
91
|
+
edits.push(...rewriteDefaultExport(source, exportAssignment, shape, builderName, operationsText));
|
|
92
|
+
// 4. `export type ProviderContext = ProviderContextOf<typeof buildProvider>;`
|
|
93
|
+
// plus the type-only import, when neither is already present.
|
|
94
|
+
edits.push(...ensureProviderContextType(source, call, shape, builderName));
|
|
95
|
+
const code = applyEdits(sourceText, edits);
|
|
96
|
+
// Re-parse the output: a transform that emits unparseable source is worse
|
|
97
|
+
// than one that skips, because the pin bump would ship alongside it.
|
|
98
|
+
const verified = ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
99
|
+
const outputError = firstSyntaxError(verified);
|
|
100
|
+
if (outputError !== undefined) {
|
|
101
|
+
return {
|
|
102
|
+
status: "skipped",
|
|
103
|
+
reason: `Transform produced source that does not parse (${outputError}); refusing to emit a partial migration.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
status: "migrated",
|
|
108
|
+
kind: shape.kind,
|
|
109
|
+
code,
|
|
110
|
+
operationsExpression: operationsText,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function classifyShape(source, call, exportAssignment) {
|
|
114
|
+
const variableStatement = enclosingVariableStatement(call);
|
|
115
|
+
if (variableStatement === undefined) {
|
|
116
|
+
// `export default defineProvider({...})`
|
|
117
|
+
if (exportAssignment.expression === call) {
|
|
118
|
+
return { status: "ok", kind: "single-phase-default-export" };
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
status: "skipped",
|
|
122
|
+
reason: "defineProvider(...) is neither bound to a variable nor the default-export expression, so the transform cannot place the builder.",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const declarations = variableStatement.declarationList.declarations;
|
|
126
|
+
if (declarations.length !== 1) {
|
|
127
|
+
return {
|
|
128
|
+
status: "skipped",
|
|
129
|
+
reason: "The defineProvider(...) result is declared alongside other bindings in one statement; split the declaration first.",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const declaration = declarations[0];
|
|
133
|
+
if (declaration === undefined || !ts.isIdentifier(declaration.name)) {
|
|
134
|
+
return {
|
|
135
|
+
status: "skipped",
|
|
136
|
+
reason: "The defineProvider(...) result is bound to a destructuring pattern.",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (declaration.initializer !== call) {
|
|
140
|
+
return {
|
|
141
|
+
status: "skipped",
|
|
142
|
+
reason: "defineProvider(...) is nested inside a larger initializer expression the transform cannot rewrite.",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const variableName = declaration.name.text;
|
|
146
|
+
const exported = exportAssignment.expression;
|
|
147
|
+
if (ts.isIdentifier(exported) && exported.text === variableName) {
|
|
148
|
+
return {
|
|
149
|
+
status: "ok",
|
|
150
|
+
kind: "single-phase-variable-export",
|
|
151
|
+
variableName,
|
|
152
|
+
variableStatement,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (ts.isObjectLiteralExpression(exported)) {
|
|
156
|
+
const spreads = exported.properties.filter(ts.isSpreadAssignment);
|
|
157
|
+
const spreadsProvider = spreads.some((property) => ts.isIdentifier(property.expression) &&
|
|
158
|
+
property.expression.text === variableName);
|
|
159
|
+
if (!spreadsProvider) {
|
|
160
|
+
return {
|
|
161
|
+
status: "skipped",
|
|
162
|
+
reason: `The default export is an object literal that does not spread \`${variableName}\`, so the provider value it exports is unclear.`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (spreads.length > 1) {
|
|
166
|
+
return {
|
|
167
|
+
status: "skipped",
|
|
168
|
+
reason: "The default export spreads more than one value; the transform cannot tell which carries the provider.",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const extras = exported.properties.filter((property) => !ts.isSpreadAssignment(property));
|
|
172
|
+
return {
|
|
173
|
+
status: "ok",
|
|
174
|
+
kind: "single-phase-variable-spread-export",
|
|
175
|
+
variableName,
|
|
176
|
+
variableStatement,
|
|
177
|
+
spreadExportExtras: extras
|
|
178
|
+
.map((property) => property.getText(source))
|
|
179
|
+
.join(",\n "),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
status: "skipped",
|
|
184
|
+
reason: `The default export is neither \`${variableName}\` nor an object literal spreading it.`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function collectDefineProviderCalls(source) {
|
|
188
|
+
const calls = [];
|
|
189
|
+
const visit = (node) => {
|
|
190
|
+
if (ts.isCallExpression(node) &&
|
|
191
|
+
ts.isIdentifier(node.expression) &&
|
|
192
|
+
node.expression.text === "defineProvider") {
|
|
193
|
+
calls.push(node);
|
|
194
|
+
}
|
|
195
|
+
ts.forEachChild(node, visit);
|
|
196
|
+
};
|
|
197
|
+
visit(source);
|
|
198
|
+
return calls;
|
|
199
|
+
}
|
|
200
|
+
function findDefaultExport(source) {
|
|
201
|
+
for (const statement of source.statements) {
|
|
202
|
+
if (ts.isExportAssignment(statement) && statement.isExportEquals !== true) {
|
|
203
|
+
return statement;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
function defaultExportCallsBuilder(exportAssignment, declarationCall) {
|
|
209
|
+
const expression = exportAssignment.expression;
|
|
210
|
+
if (!ts.isCallExpression(expression))
|
|
211
|
+
return false;
|
|
212
|
+
if (expression === declarationCall)
|
|
213
|
+
return false;
|
|
214
|
+
return ts.isIdentifier(expression.expression);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* True when the module is already in the two-phase shape: the default export
|
|
218
|
+
* resolves to a builder-call result. Covers the three migrated layouts —
|
|
219
|
+
* `export default buildProvider({...})`, an intermediate
|
|
220
|
+
* `const provider = buildProvider({...}); export default provider;`, and the
|
|
221
|
+
* spread export `export default { ...provider, deployment }` over such a
|
|
222
|
+
* binding. Without the latter two, re-running the transform on its own
|
|
223
|
+
* spread-shape output reports "ambiguous" instead of "unchanged", which
|
|
224
|
+
* breaks idempotency for repeated fan-out runs.
|
|
225
|
+
*/
|
|
226
|
+
function isAlreadyTwoPhase(source, exportAssignment, declarationCall) {
|
|
227
|
+
if (defaultExportCallsBuilder(exportAssignment, declarationCall))
|
|
228
|
+
return true;
|
|
229
|
+
const exported = exportAssignment.expression;
|
|
230
|
+
const candidateNames = [];
|
|
231
|
+
if (ts.isIdentifier(exported)) {
|
|
232
|
+
candidateNames.push(exported.text);
|
|
233
|
+
}
|
|
234
|
+
else if (ts.isObjectLiteralExpression(exported)) {
|
|
235
|
+
for (const property of exported.properties) {
|
|
236
|
+
if (ts.isSpreadAssignment(property) && ts.isIdentifier(property.expression)) {
|
|
237
|
+
candidateNames.push(property.expression.text);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (candidateNames.length === 0)
|
|
242
|
+
return false;
|
|
243
|
+
// Does any spread/exported identifier bind a call to an identifier other
|
|
244
|
+
// than defineProvider — i.e. a builder call?
|
|
245
|
+
let found = false;
|
|
246
|
+
const visit = (node) => {
|
|
247
|
+
if (found)
|
|
248
|
+
return;
|
|
249
|
+
if (ts.isVariableDeclaration(node) &&
|
|
250
|
+
ts.isIdentifier(node.name) &&
|
|
251
|
+
candidateNames.includes(node.name.text) &&
|
|
252
|
+
node.initializer !== undefined &&
|
|
253
|
+
ts.isCallExpression(node.initializer) &&
|
|
254
|
+
node.initializer !== declarationCall &&
|
|
255
|
+
ts.isIdentifier(node.initializer.expression) &&
|
|
256
|
+
node.initializer.expression.text !== "defineProvider") {
|
|
257
|
+
found = true;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
ts.forEachChild(node, visit);
|
|
261
|
+
};
|
|
262
|
+
visit(source);
|
|
263
|
+
return found;
|
|
264
|
+
}
|
|
265
|
+
function findOperationsProperty(declaration) {
|
|
266
|
+
for (const property of declaration.properties) {
|
|
267
|
+
if (ts.isSpreadAssignment(property))
|
|
268
|
+
continue;
|
|
269
|
+
const name = property.name;
|
|
270
|
+
if (name === undefined)
|
|
271
|
+
continue;
|
|
272
|
+
if ((ts.isIdentifier(name) || ts.isStringLiteral(name)) &&
|
|
273
|
+
name.text === "operations") {
|
|
274
|
+
return property;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Source text of the value to hand the builder. Shorthand becomes the bare
|
|
281
|
+
* identifier so `{ operations }` stays idiomatic; a property assignment keeps
|
|
282
|
+
* its initializer verbatim, including a multi-line inline map.
|
|
283
|
+
*/
|
|
284
|
+
function operationsPropertyValueText(property, source) {
|
|
285
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
286
|
+
return property.name.text;
|
|
287
|
+
}
|
|
288
|
+
if (ts.isPropertyAssignment(property)) {
|
|
289
|
+
return property.initializer.getText(source);
|
|
290
|
+
}
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
function removeOperationsProperty(property, declaration, source) {
|
|
294
|
+
const properties = declaration.properties;
|
|
295
|
+
const index = properties.indexOf(property);
|
|
296
|
+
const start = property.getFullStart();
|
|
297
|
+
let end = property.getEnd();
|
|
298
|
+
// Absorb the trailing comma so the remaining literal stays well-formed,
|
|
299
|
+
// whether the key sat mid-list or last.
|
|
300
|
+
const text = source.getFullText();
|
|
301
|
+
let cursor = end;
|
|
302
|
+
while (cursor < text.length && /\s/.test(text.charAt(cursor)))
|
|
303
|
+
cursor += 1;
|
|
304
|
+
if (text.charAt(cursor) === ",") {
|
|
305
|
+
end = cursor + 1;
|
|
306
|
+
}
|
|
307
|
+
else if (index > 0) {
|
|
308
|
+
// Last property with no trailing comma: drop the preceding one instead.
|
|
309
|
+
const previous = properties[index - 1];
|
|
310
|
+
if (previous !== undefined) {
|
|
311
|
+
let back = previous.getEnd();
|
|
312
|
+
while (back < text.length && /\s/.test(text.charAt(back)))
|
|
313
|
+
back += 1;
|
|
314
|
+
if (text.charAt(back) === ",") {
|
|
315
|
+
return [{ start: back, end, text: "" }];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return [{ start, end, text: "" }];
|
|
320
|
+
}
|
|
321
|
+
function introduceBuilder(source, call, shape, builderName) {
|
|
322
|
+
if (shape.kind === "single-phase-default-export") {
|
|
323
|
+
// `export default defineProvider({...})` becomes a standalone builder
|
|
324
|
+
// declaration; the default export is re-appended separately.
|
|
325
|
+
const exportStatement = call.parent;
|
|
326
|
+
if (!ts.isExportAssignment(exportStatement))
|
|
327
|
+
return [];
|
|
328
|
+
return [
|
|
329
|
+
{
|
|
330
|
+
start: exportStatement.getStart(source),
|
|
331
|
+
end: call.getStart(source),
|
|
332
|
+
text: `const ${builderName} = `,
|
|
333
|
+
},
|
|
334
|
+
];
|
|
335
|
+
}
|
|
336
|
+
const statement = shape.variableStatement;
|
|
337
|
+
if (statement === undefined || shape.variableName === undefined)
|
|
338
|
+
return [];
|
|
339
|
+
// Rename the existing binding to the builder name. The old name is
|
|
340
|
+
// re-introduced for the built provider only in the spread-export shape,
|
|
341
|
+
// which needs an intermediate value to spread.
|
|
342
|
+
const declaration = statement.declarationList.declarations[0];
|
|
343
|
+
if (declaration === undefined || !ts.isIdentifier(declaration.name))
|
|
344
|
+
return [];
|
|
345
|
+
return [
|
|
346
|
+
{
|
|
347
|
+
start: declaration.name.getStart(source),
|
|
348
|
+
end: declaration.name.getEnd(),
|
|
349
|
+
text: builderName,
|
|
350
|
+
},
|
|
351
|
+
];
|
|
352
|
+
}
|
|
353
|
+
function rewriteDefaultExport(source, exportAssignment, shape, builderName, operationsText) {
|
|
354
|
+
const operationsArgument = operationsText === "operations"
|
|
355
|
+
? "{ operations }"
|
|
356
|
+
: `{ operations: ${operationsText} }`;
|
|
357
|
+
const call = `${builderName}(${operationsArgument})`;
|
|
358
|
+
if (shape.kind === "single-phase-default-export") {
|
|
359
|
+
// The builder declaration replaced the `export default` prefix; the
|
|
360
|
+
// export itself is appended after what is now the builder statement.
|
|
361
|
+
return [
|
|
362
|
+
{
|
|
363
|
+
start: exportAssignment.getEnd(),
|
|
364
|
+
end: exportAssignment.getEnd(),
|
|
365
|
+
text: `\n\nexport default ${call};`,
|
|
366
|
+
},
|
|
367
|
+
];
|
|
368
|
+
}
|
|
369
|
+
if (shape.kind === "single-phase-variable-export") {
|
|
370
|
+
return [
|
|
371
|
+
{
|
|
372
|
+
start: exportAssignment.expression.getStart(source),
|
|
373
|
+
end: exportAssignment.expression.getEnd(),
|
|
374
|
+
text: call,
|
|
375
|
+
},
|
|
376
|
+
];
|
|
377
|
+
}
|
|
378
|
+
// Spread export: re-introduce the provider binding so the extra properties
|
|
379
|
+
// (currently `deployment`) still spread over a built provider.
|
|
380
|
+
const providerName = shape.variableName ?? "provider";
|
|
381
|
+
const extras = shape.spreadExportExtras ?? "";
|
|
382
|
+
const rebuilt = extras.length > 0
|
|
383
|
+
? `{\n ...${providerName},\n ${extras},\n}`
|
|
384
|
+
: `{ ...${providerName} }`;
|
|
385
|
+
return [
|
|
386
|
+
{
|
|
387
|
+
start: exportAssignment.getStart(source),
|
|
388
|
+
end: exportAssignment.getStart(source),
|
|
389
|
+
text: `const ${providerName} = ${call};\n\n`,
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
start: exportAssignment.expression.getStart(source),
|
|
393
|
+
end: exportAssignment.expression.getEnd(),
|
|
394
|
+
text: rebuilt,
|
|
395
|
+
},
|
|
396
|
+
];
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Add `export type ProviderContext = ProviderContextOf<typeof buildProvider>`
|
|
400
|
+
* and the type-only import when absent. Operation handlers reference this
|
|
401
|
+
* type, so a migration that omits it leaves the provider without the context
|
|
402
|
+
* type the two-phase shape exists to provide.
|
|
403
|
+
*/
|
|
404
|
+
function ensureProviderContextType(source, call, shape, builderName) {
|
|
405
|
+
const text = source.getFullText();
|
|
406
|
+
const edits = [];
|
|
407
|
+
const hasContextTypeAlias = source.statements.some((statement) => ts.isTypeAliasDeclaration(statement) &&
|
|
408
|
+
statement.name.text === PROVIDER_CONTEXT_TYPE_NAME);
|
|
409
|
+
// A named import of `ProviderContext` (the deprecated SDK-root context
|
|
410
|
+
// type, imported by legacy sources) occupies the same name: adding the
|
|
411
|
+
// alias alongside it is TS2440. The import must yield to the derived
|
|
412
|
+
// alias — the whole point of the two-phase shape — so drop it and let the
|
|
413
|
+
// alias own the name.
|
|
414
|
+
const conflictingImportSpecifier = findNamedImportSpecifier(source, PROVIDER_CONTEXT_TYPE_NAME);
|
|
415
|
+
if (!hasContextTypeAlias) {
|
|
416
|
+
// After the builder statement — which is the variable statement when the
|
|
417
|
+
// declaration was bound, or the rewritten export statement otherwise.
|
|
418
|
+
const anchor = shape.variableStatement ??
|
|
419
|
+
(ts.isExportAssignment(call.parent) ? call.parent : undefined);
|
|
420
|
+
if (anchor !== undefined) {
|
|
421
|
+
const insertAt = anchor.getEnd();
|
|
422
|
+
edits.push({
|
|
423
|
+
start: insertAt,
|
|
424
|
+
end: insertAt,
|
|
425
|
+
text: `\n\nexport type ${PROVIDER_CONTEXT_TYPE_NAME} = ${PROVIDER_CONTEXT_OF_TYPE_NAME}<typeof ${builderName}>;`,
|
|
426
|
+
});
|
|
427
|
+
if (conflictingImportSpecifier !== undefined) {
|
|
428
|
+
edits.push(removeImportSpecifier(source, conflictingImportSpecifier));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (text.includes(PROVIDER_CONTEXT_OF_TYPE_NAME)) {
|
|
433
|
+
return edits;
|
|
434
|
+
}
|
|
435
|
+
const providerImport = findProviderSdkImport(source);
|
|
436
|
+
if (providerImport === undefined) {
|
|
437
|
+
// No named import from the provider subpath to extend; adding a new
|
|
438
|
+
// import line without knowing the module's style is riskier than
|
|
439
|
+
// leaving the type import to `bun run check` feedback.
|
|
440
|
+
return edits;
|
|
441
|
+
}
|
|
442
|
+
const named = providerImport.importClause?.namedBindings;
|
|
443
|
+
if (named === undefined || !ts.isNamedImports(named)) {
|
|
444
|
+
return edits;
|
|
445
|
+
}
|
|
446
|
+
const last = named.elements[named.elements.length - 1];
|
|
447
|
+
if (last === undefined) {
|
|
448
|
+
return edits;
|
|
449
|
+
}
|
|
450
|
+
edits.push({
|
|
451
|
+
start: last.getEnd(),
|
|
452
|
+
end: last.getEnd(),
|
|
453
|
+
text: `, type ${PROVIDER_CONTEXT_OF_TYPE_NAME}`,
|
|
454
|
+
});
|
|
455
|
+
return edits;
|
|
456
|
+
}
|
|
457
|
+
function findProviderSdkImport(source) {
|
|
458
|
+
for (const statement of source.statements) {
|
|
459
|
+
if (!ts.isImportDeclaration(statement))
|
|
460
|
+
continue;
|
|
461
|
+
const moduleSpecifier = statement.moduleSpecifier;
|
|
462
|
+
if (!ts.isStringLiteral(moduleSpecifier))
|
|
463
|
+
continue;
|
|
464
|
+
if (moduleSpecifier.text !== PROVIDER_SDK_PROVIDER_SUBPATH)
|
|
465
|
+
continue;
|
|
466
|
+
const named = statement.importClause?.namedBindings;
|
|
467
|
+
if (named !== undefined && ts.isNamedImports(named)) {
|
|
468
|
+
const importsDefineProvider = named.elements.some((element) => element.name.text === "defineProvider");
|
|
469
|
+
if (importsDefineProvider)
|
|
470
|
+
return statement;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
/** Named import specifier binding `localName` in any import declaration. */
|
|
476
|
+
function findNamedImportSpecifier(source, localName) {
|
|
477
|
+
for (const statement of source.statements) {
|
|
478
|
+
if (!ts.isImportDeclaration(statement))
|
|
479
|
+
continue;
|
|
480
|
+
const named = statement.importClause?.namedBindings;
|
|
481
|
+
if (named === undefined || !ts.isNamedImports(named))
|
|
482
|
+
continue;
|
|
483
|
+
for (const element of named.elements) {
|
|
484
|
+
if (element.name.text === localName)
|
|
485
|
+
return element;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Edit removing one specifier from its named-import list, absorbing one
|
|
492
|
+
* neighboring comma so the list stays well-formed. Callers guarantee the
|
|
493
|
+
* list has at least one other specifier (legacy sources always import
|
|
494
|
+
* defineProvider alongside the context type).
|
|
495
|
+
*/
|
|
496
|
+
function removeImportSpecifier(source, specifier) {
|
|
497
|
+
const list = specifier.parent;
|
|
498
|
+
const index = list.elements.indexOf(specifier);
|
|
499
|
+
const text = source.getFullText();
|
|
500
|
+
let start = specifier.getFullStart();
|
|
501
|
+
let end = specifier.getEnd();
|
|
502
|
+
let cursor = end;
|
|
503
|
+
while (cursor < text.length && /\s/.test(text.charAt(cursor)))
|
|
504
|
+
cursor += 1;
|
|
505
|
+
if (text.charAt(cursor) === ",") {
|
|
506
|
+
end = cursor + 1;
|
|
507
|
+
}
|
|
508
|
+
else if (index > 0) {
|
|
509
|
+
const previous = list.elements[index - 1];
|
|
510
|
+
if (previous !== undefined) {
|
|
511
|
+
let back = previous.getEnd();
|
|
512
|
+
while (back < text.length && /\s/.test(text.charAt(back)))
|
|
513
|
+
back += 1;
|
|
514
|
+
if (text.charAt(back) === ",")
|
|
515
|
+
start = back;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return { start, end, text: "" };
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* `buildProvider` unless the module already binds that name, in which case a
|
|
522
|
+
* numbered suffix keeps the transform from shadowing an existing binding.
|
|
523
|
+
*/
|
|
524
|
+
function pickBuilderName(source) {
|
|
525
|
+
const taken = new Set();
|
|
526
|
+
const visit = (node) => {
|
|
527
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
528
|
+
taken.add(node.name.text);
|
|
529
|
+
}
|
|
530
|
+
if (ts.isFunctionDeclaration(node) && node.name !== undefined) {
|
|
531
|
+
taken.add(node.name.text);
|
|
532
|
+
}
|
|
533
|
+
if (ts.isImportSpecifier(node)) {
|
|
534
|
+
taken.add(node.name.text);
|
|
535
|
+
}
|
|
536
|
+
ts.forEachChild(node, visit);
|
|
537
|
+
};
|
|
538
|
+
visit(source);
|
|
539
|
+
if (!taken.has(DECLARATION_BUILDER_NAME))
|
|
540
|
+
return DECLARATION_BUILDER_NAME;
|
|
541
|
+
for (let suffix = 2; suffix < 100; suffix += 1) {
|
|
542
|
+
const candidate = `${DECLARATION_BUILDER_NAME}${suffix}`;
|
|
543
|
+
if (!taken.has(candidate))
|
|
544
|
+
return candidate;
|
|
545
|
+
}
|
|
546
|
+
return `${DECLARATION_BUILDER_NAME}Migrated`;
|
|
547
|
+
}
|
|
548
|
+
function enclosingVariableStatement(node) {
|
|
549
|
+
let current = node.parent;
|
|
550
|
+
while (current !== undefined) {
|
|
551
|
+
if (ts.isVariableStatement(current))
|
|
552
|
+
return current;
|
|
553
|
+
if (ts.isSourceFile(current))
|
|
554
|
+
return undefined;
|
|
555
|
+
current = current.parent;
|
|
556
|
+
}
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
function firstSyntaxError(source) {
|
|
560
|
+
const diagnostics = source.parseDiagnostics;
|
|
561
|
+
if (diagnostics === undefined || diagnostics.length === 0)
|
|
562
|
+
return undefined;
|
|
563
|
+
const first = diagnostics[0];
|
|
564
|
+
if (first === undefined)
|
|
565
|
+
return undefined;
|
|
566
|
+
const message = ts.flattenDiagnosticMessageText(first.messageText, " ");
|
|
567
|
+
const { line } = source.getLineAndCharacterOfPosition(first.start);
|
|
568
|
+
return `${message} (line ${line + 1})`;
|
|
569
|
+
}
|
|
570
|
+
/** Apply edits back-to-front so earlier offsets stay valid. */
|
|
571
|
+
function applyEdits(text, edits) {
|
|
572
|
+
const ordered = [...edits].sort((a, b) => b.start - a.start || b.end - a.end);
|
|
573
|
+
let output = text;
|
|
574
|
+
for (const edit of ordered) {
|
|
575
|
+
output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
|
|
576
|
+
}
|
|
577
|
+
return output;
|
|
578
|
+
}
|
package/dist/contract.js
CHANGED
|
@@ -130,6 +130,7 @@ function extractHealthCheck(value) {
|
|
|
130
130
|
name: item.name,
|
|
131
131
|
description: item.description,
|
|
132
132
|
input: toJsonValue(item.input),
|
|
133
|
+
scenario: toJsonValue(item.scenario),
|
|
133
134
|
degradedThresholdMs: item.degradedThresholdMs,
|
|
134
135
|
timeoutMs: item.timeoutMs,
|
|
135
136
|
expectedStatus: item.expectedStatus,
|
package/dist/define.js
CHANGED
|
@@ -2,6 +2,7 @@ import ms from "ms";
|
|
|
2
2
|
import { validateFailClosedOperationDeclaration, validateFailClosedProviderDeclaration, } from "./declaration-validation.js";
|
|
3
3
|
import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
|
|
4
4
|
import { ProviderError, ValidationError } from "./errors.js";
|
|
5
|
+
import { HealthScenarioSchema } from "./health-scenario.js";
|
|
5
6
|
import { NativeEgressPolicyValidationError, validateNativeProviderConfig, } from "./native-egress-policy.js";
|
|
6
7
|
import { safeParseSchemaSync } from "./schema.js";
|
|
7
8
|
import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
|
|
@@ -1136,6 +1137,7 @@ const HEALTH_CHECK_CASE_FIELDS = new Set([
|
|
|
1136
1137
|
"input",
|
|
1137
1138
|
"prepareInput",
|
|
1138
1139
|
"assertions",
|
|
1140
|
+
"scenario",
|
|
1139
1141
|
"degradedThresholdMs",
|
|
1140
1142
|
"timeoutMs",
|
|
1141
1143
|
"expectedStatus",
|
|
@@ -1310,12 +1312,31 @@ function validateHealthCheckCase(providerId, operationName, caseValue, caseIndex
|
|
|
1310
1312
|
const c = caseValue;
|
|
1311
1313
|
if (typeof c.name !== "string" || c.name.length === 0)
|
|
1312
1314
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.name must be a non-empty string.`);
|
|
1313
|
-
|
|
1315
|
+
const hasScenario = c.scenario !== undefined;
|
|
1316
|
+
const imperativeFields = [
|
|
1317
|
+
...(c.prepareInput === undefined ? [] : ["prepareInput"]),
|
|
1318
|
+
...(c.assertions === undefined ? [] : ["assertions"]),
|
|
1319
|
+
];
|
|
1320
|
+
if (hasScenario && imperativeFields.length > 0)
|
|
1321
|
+
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" health-check case "${c.name}" cannot declare scenario with ${imperativeFields.join(" and ")}.`, {
|
|
1322
|
+
fix: `Remove ${imperativeFields.map((field) => `${fieldPath}.${field}`).join(" and ")} and keep ${fieldPath}.scenario, or remove ${fieldPath}.scenario to keep the imperative hooks.`,
|
|
1323
|
+
});
|
|
1324
|
+
if (!hasScenario && typeof c.assertions !== "function")
|
|
1314
1325
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.assertions must be a function.`, {
|
|
1315
1326
|
fix: `Set ${fieldPath}.assertions to (ctx) => { ... } that throws on failure.`,
|
|
1316
1327
|
});
|
|
1317
1328
|
if (c.prepareInput !== undefined && typeof c.prepareInput !== "function")
|
|
1318
1329
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.prepareInput must be a function.`);
|
|
1330
|
+
if (hasScenario) {
|
|
1331
|
+
const parsedScenario = HealthScenarioSchema.safeParse(c.scenario);
|
|
1332
|
+
if (!parsedScenario.success)
|
|
1333
|
+
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" health-check case "${c.name}" has an invalid scenario: it must conform to HealthScenario.`, { fix: `Build ${fieldPath}.scenario with defineHealthScenario().` });
|
|
1334
|
+
const unrelatedOperation = parsedScenario.data.coversOperations.find((operationId) => operationId !== operationName);
|
|
1335
|
+
if (unrelatedOperation !== undefined)
|
|
1336
|
+
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" health-check case "${c.name}" scenario.coversOperations cannot claim unrelated operation "${unrelatedOperation}".`, {
|
|
1337
|
+
fix: `Set ${fieldPath}.scenario.coversOperations to ["${operationName}"].`,
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1319
1340
|
if (c.degradedThresholdMs !== undefined &&
|
|
1320
1341
|
(typeof c.degradedThresholdMs !== "number" ||
|
|
1321
1342
|
!Number.isInteger(c.degradedThresholdMs) ||
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ProviderErrorObservability } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts only own data properties from branded provider errors. In
|
|
4
|
+
* particular, descriptor reads reject options/observability accessors without
|
|
5
|
+
* invoking provider-controlled getters.
|
|
6
|
+
*/
|
|
7
|
+
export declare function safeProviderErrorObservability(error: unknown): ProviderErrorObservability | undefined;
|