@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 +8 -0
- package/bin/apifuse-check.ts +1 -1
- package/bin/apifuse-dev.ts +1 -1
- package/bin/apifuse-migrate-shape.ts +141 -23
- package/bin/apifuse-submit-check.ts +99 -86
- package/dist/cli/commands.js +1 -1
- package/dist/cli/create.d.ts +1 -1
- package/dist/cli/create.js +2 -2
- 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.js +64 -1
- package/dist/define.d.ts +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/server/serve-implementation.d.ts +11 -11
- package/dist/server/serve-implementation.js +50 -7
- package/dist/types.d.ts +2 -2
- package/package.json +3 -3
- package/src/cli/commands.ts +1 -1
- package/src/cli/create.ts +2 -2
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +107 -36
- package/src/define.ts +10 -0
- package/src/index.ts +2 -0
- package/src/provider.ts +2 -0
- package/src/server/serve-implementation.ts +90 -36
- package/src/types.ts +2 -2
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
|
|
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
|
+
}
|
|
2
11
|
const DECLARATION_BUILDER_NAME = "buildProvider";
|
|
3
12
|
const PROVIDER_CONTEXT_TYPE_NAME = "ProviderContext";
|
|
4
13
|
const PROVIDER_CONTEXT_OF_TYPE_NAME = "ProviderContextOf";
|
|
@@ -397,6 +406,12 @@ function ensureProviderContextType(source, call, shape, builderName) {
|
|
|
397
406
|
const edits = [];
|
|
398
407
|
const hasContextTypeAlias = source.statements.some((statement) => ts.isTypeAliasDeclaration(statement) &&
|
|
399
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);
|
|
400
415
|
if (!hasContextTypeAlias) {
|
|
401
416
|
// After the builder statement — which is the variable statement when the
|
|
402
417
|
// declaration was bound, or the rewritten export statement otherwise.
|
|
@@ -409,6 +424,9 @@ function ensureProviderContextType(source, call, shape, builderName) {
|
|
|
409
424
|
end: insertAt,
|
|
410
425
|
text: `\n\nexport type ${PROVIDER_CONTEXT_TYPE_NAME} = ${PROVIDER_CONTEXT_OF_TYPE_NAME}<typeof ${builderName}>;`,
|
|
411
426
|
});
|
|
427
|
+
if (conflictingImportSpecifier !== undefined) {
|
|
428
|
+
edits.push(removeImportSpecifier(source, conflictingImportSpecifier));
|
|
429
|
+
}
|
|
412
430
|
}
|
|
413
431
|
}
|
|
414
432
|
if (text.includes(PROVIDER_CONTEXT_OF_TYPE_NAME)) {
|
|
@@ -454,6 +472,51 @@ function findProviderSdkImport(source) {
|
|
|
454
472
|
}
|
|
455
473
|
return undefined;
|
|
456
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
|
+
}
|
|
457
520
|
/**
|
|
458
521
|
* `buildProvider` unless the module already binds that name, in which case a
|
|
459
522
|
* numbered suffix keeps the transform from shadowing an existing binding.
|
package/dist/define.d.ts
CHANGED
|
@@ -130,6 +130,10 @@ export type ProviderBuilder<TDeclaration extends ProviderDeclaration> = <TOperat
|
|
|
130
130
|
};
|
|
131
131
|
/** Extract the declaration-derived operation context from a provider builder. */
|
|
132
132
|
export type ProviderContextOf<TBuilder> = TBuilder extends ProviderBuilder<infer TDeclaration> ? ProviderContextFor<TDeclaration> : never;
|
|
133
|
+
/** Annotate an operation while preserving the declaration-derived context. */
|
|
134
|
+
export type OperationDefinitionFor<TBuilder, TInput extends SchemaLike = SchemaLike, TOutput extends SchemaLike = SchemaLike> = OperationDefinition<TInput, TOutput, ProviderContextOf<TBuilder>>;
|
|
135
|
+
/** Annotate a built provider while preserving the declaration-derived context. */
|
|
136
|
+
export type ProviderDefinitionFor<TBuilder> = ProviderDefinition<ProviderContextOf<TBuilder>>;
|
|
133
137
|
/** Establish a provider declaration before its operations are contextually typed. */
|
|
134
138
|
export declare function defineProvider<const TDeclaration extends ProviderDeclaration>(declaration: TDeclaration & Record<Exclude<keyof TDeclaration, keyof ProviderDeclaration>, never> & AuthStartNoInputGuard<TDeclaration>): ProviderBuilder<TDeclaration>;
|
|
135
139
|
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export * from "./choice-token.js";
|
|
|
4
4
|
export type { ApiFuseConfig, BrowserConfig, ProxyCacheStatus, ProxyProtocol, ProxyResolutionOptions, ProxyResolutionSource, ProxyUserAgentSource, ProxyVendorName, ResolvedProxyConfig, SessionConfig, SmartproxyAllocatorBodyClass, } from "./config/loader.js";
|
|
5
5
|
export { defineConfig, loadApiFuseConfig, resolveProxy } from "./config/loader.js";
|
|
6
6
|
export { canonicalJson, digestProviderContract, extractProviderContract, type JsonPrimitive, type JsonValue, PROVIDER_CONTRACT_SCHEMA_VERSION, type ProviderContractOperation, type ProviderContractSnapshot, } from "./contract.js";
|
|
7
|
-
export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, defineStreamOperation, every, type AuthStartNoInputGuard, type ProviderBuilder, type ProviderContextOf, type ProviderDeclaration, } from "./define.js";
|
|
7
|
+
export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, defineStreamOperation, every, type AuthStartNoInputGuard, type OperationDefinitionFor, type ProviderBuilder, type ProviderContextOf, type ProviderDeclaration, type ProviderDefinitionFor, } from "./define.js";
|
|
8
8
|
export { AssertionExpressionSchema, AssertionPredicateSchema, AssertStepSchema, BoundedJsonPathSchema, CandidateBlockSchema, CandidatePolicySchema, CredentialRefDeclarationSchema, defineHealthScenario, HealthScenarioSchema, HealthStepSchema, ExtractStepSchema, FindFirstSchema, GuardStepSchema, JournalPolicySchema, JsonTemplateSchema, ManualTriggerPolicySchema, OperandSchema, OperationStepSchema, QuantifierSchema, ReferenceSchema, RetryPolicySchema, SafeRegexSchema, ScopedAssertionExpressionSchema, ScopedAssertionPredicateSchema, ScopedItemReferenceSchema, ScopedOperandSchema, StepReferenceSchema, RelativeDateNodeSchema, ValueTypeSchema, } from "./health-scenario.js";
|
|
9
9
|
export type { AssertionExpression, AssertionPredicate, AttemptReference, AssertResult, AssertStep, BoundedJsonPath, CandidateBlock, CandidatePolicy, CandidateReference, CredentialReference, CredentialRefDeclaration, EstablishedConnectionReference, ExtractStep, FindFirst, GuardAttribution, GuardReasonCode, GuardResult, GuardStep, HealthScenario, HealthStep, JsonTemplate, JournalPolicy, ManualTriggerPolicy, NonEmpty, OperationResult, OperationStep, Operand, Quantifier, ReferenceNode, Reference, RelativeDateNode, RetryPolicy, SafeRegex, ScopedAssertionExpression, ScopedAssertionPredicate, ScopedItemReference, ScopedOperand, StepBase, StepReference, ExtractResult, ValueType, } from "./health-scenario.js";
|
|
10
10
|
export type { DevServerOptions } from "./dev.js";
|
package/dist/provider.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { CredentialsAuthChallengeDefinition, CredentialsAuthChallengeReques
|
|
|
3
3
|
export { createFormCeremony } from "./ceremonies/index.js";
|
|
4
4
|
export { assertFreshProviderChoiceIssuedAt, createProviderChoiceToken, ProviderChoiceTokenError, type ProviderChoiceTokenErrorReason, type ProviderChoiceTokenPayload, parseProviderChoiceToken, } from "./choice-token.js";
|
|
5
5
|
export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, every, } from "./define.js";
|
|
6
|
-
export type { ProviderBuilder, ProviderContextOf, ProviderDeclaration, } from "./define.js";
|
|
6
|
+
export type { OperationDefinitionFor, ProviderBuilder, ProviderContextOf, ProviderDeclaration, ProviderDefinitionFor, } from "./define.js";
|
|
7
7
|
export type { JsonPrimitive, JsonValue } from "./contract-json.js";
|
|
8
8
|
export { AssertionExpressionSchema, AssertionPredicateSchema, AssertStepSchema, BoundedJsonPathSchema, CandidateBlockSchema, CandidatePolicySchema, CredentialRefDeclarationSchema, defineHealthScenario, HealthScenarioSchema, HealthStepSchema, ExtractStepSchema, FindFirstSchema, GuardStepSchema, JournalPolicySchema, JsonTemplateSchema, ManualTriggerPolicySchema, OperandSchema, OperationStepSchema, QuantifierSchema, ReferenceSchema, RetryPolicySchema, SafeRegexSchema, ScopedAssertionExpressionSchema, ScopedAssertionPredicateSchema, ScopedItemReferenceSchema, ScopedOperandSchema, StepReferenceSchema, RelativeDateNodeSchema, ValueTypeSchema, } from "./health-scenario.js";
|
|
9
9
|
export type { AssertionExpression, AssertionPredicate, AttemptReference, AssertResult, AssertStep, BoundedJsonPath, CandidateBlock, CandidatePolicy, CandidateReference, CredentialReference, CredentialRefDeclaration, EstablishedConnectionReference, ExtractStep, FindFirst, GuardAttribution, GuardReasonCode, GuardResult, GuardStep, HealthScenario, HealthStep, JsonTemplate, JournalPolicy, ManualTriggerPolicy, NonEmpty, OperationResult, OperationStep, Operand, Quantifier, ReferenceNode, Reference, RelativeDateNode, RetryPolicy, SafeRegex, ScopedAssertionExpression, ScopedAssertionPredicate, ScopedItemReference, ScopedOperand, StepBase, StepReference, ExtractResult, ValueType, } from "./health-scenario.js";
|
|
@@ -53,17 +53,17 @@ export declare const ProviderServerStatefulForwardEnvelopeSchema: z.ZodObject<{
|
|
|
53
53
|
export type ProviderServerStatefulForwardEnvelope = Readonly<z.infer<typeof ProviderServerStatefulForwardEnvelopeSchema>>;
|
|
54
54
|
export type ProviderServerStatefulOwnerFence = Readonly<Pick<ProviderServerStatefulForwardEnvelope, "providerId" | "sessionKey" | "ownerPodId" | "generation" | "sourcePodId" | "forwardedAt" | "requestId" | "idempotencyKey">>;
|
|
55
55
|
export type ProviderServerStatefulOwnerFenceValidator = (fence: ProviderServerStatefulOwnerFence, signal: AbortSignal) => boolean | Promise<boolean>;
|
|
56
|
-
export type ProviderServerOperationExecutorInput = {
|
|
57
|
-
readonly provider: ProviderDefinition
|
|
56
|
+
export type ProviderServerOperationExecutorInput<TContext extends Partial<ProviderContext> = ProviderContext> = {
|
|
57
|
+
readonly provider: ProviderDefinition<TContext>;
|
|
58
58
|
readonly operationId: string;
|
|
59
|
-
readonly ctx:
|
|
59
|
+
readonly ctx: TContext;
|
|
60
60
|
readonly request: OperationRequest & {
|
|
61
61
|
readonly deadlineAt?: string;
|
|
62
62
|
};
|
|
63
63
|
readonly signal?: AbortSignal;
|
|
64
64
|
readonly internalStatefulForward?: ProviderServerStatefulForwardEnvelope;
|
|
65
65
|
};
|
|
66
|
-
export type ProviderServerOperationExecutor = (input: ProviderServerOperationExecutorInput) => Promise<unknown>;
|
|
66
|
+
export type ProviderServerOperationExecutor<TContext extends Partial<ProviderContext> = ProviderContext> = (input: ProviderServerOperationExecutorInput<TContext>) => Promise<unknown>;
|
|
67
67
|
export declare function resolveProviderProxyAffinityKey(provider: ProviderDefinition, request: OperationRequest, operationId: string): string;
|
|
68
68
|
export declare function resolveProviderResolverIdentityScope(provider: ProviderDefinition, affinityKey: string, contextId: string): string;
|
|
69
69
|
export declare function resolveAuthFlowProxyAffinityKey(provider: ProviderDefinition, request: Pick<AuthFlowRequest, "connection" | "connectionId" | "externalRef" | "tenantId" | "providerId">): string;
|
|
@@ -138,12 +138,12 @@ export type ProviderServerLogEvent = (ProviderServerLogEventBase & {
|
|
|
138
138
|
message: string;
|
|
139
139
|
} | SelfTestCancellationLogEvent;
|
|
140
140
|
export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
|
|
141
|
-
export type ProviderServerOptions = {
|
|
141
|
+
export type ProviderServerOptions<TContext extends Partial<ProviderContext> = ProviderContext> = {
|
|
142
142
|
logger?: ProviderServerLogger;
|
|
143
143
|
/** Optional provider-specific operation executor. Stateful providers use this to preserve provider-local runtime semantics. */
|
|
144
|
-
operationExecutor?: ProviderServerOperationExecutor
|
|
144
|
+
operationExecutor?: ProviderServerOperationExecutor<TContext>;
|
|
145
145
|
/** Optional signed internal executor for stateful owner forwarding. */
|
|
146
|
-
internalOperationExecutor?: ProviderServerOperationExecutor
|
|
146
|
+
internalOperationExecutor?: ProviderServerOperationExecutor<TContext>;
|
|
147
147
|
statefulForwarding?: {
|
|
148
148
|
readonly secret: string;
|
|
149
149
|
readonly maxSkewMs?: number;
|
|
@@ -196,14 +196,14 @@ export type ProviderErrorCauseFrame = {
|
|
|
196
196
|
* Primary, cross-runtime app factory. Declared capability ESM is preloaded
|
|
197
197
|
* asynchronously, so this path works on Bun and every supported Node release.
|
|
198
198
|
*/
|
|
199
|
-
export declare function createServerAppAsync(provider: ProviderDefinition
|
|
199
|
+
export declare function createServerAppAsync<TContext extends Partial<ProviderContext> = ProviderContext>(provider: ProviderDefinition<TContext>, options?: ProviderServerOptions<TContext>): Promise<Hono>;
|
|
200
200
|
/**
|
|
201
201
|
* Synchronous compatibility factory. Standard providers remain synchronous on
|
|
202
202
|
* every runtime because they load no capability modules. Providers declaring a
|
|
203
203
|
* capability require Bun or Node >=22.12; older Node releases receive an
|
|
204
204
|
* actionable error directing them to createServerAppAsync().
|
|
205
205
|
*/
|
|
206
|
-
export declare function createServerApp(provider: ProviderDefinition
|
|
206
|
+
export declare function createServerApp<TContext extends Partial<ProviderContext> = ProviderContext>(provider: ProviderDefinition<TContext>, options?: ProviderServerOptions<TContext>): Hono;
|
|
207
207
|
export type ProviderServerCloseOptions = {
|
|
208
208
|
readonly timeoutMs?: number;
|
|
209
209
|
};
|
|
@@ -211,7 +211,7 @@ export type ProviderServerHandle = {
|
|
|
211
211
|
readonly port: number;
|
|
212
212
|
close(options?: ProviderServerCloseOptions): Promise<void>;
|
|
213
213
|
};
|
|
214
|
-
export interface ServeOptions extends ProviderServerOptions {
|
|
214
|
+
export interface ServeOptions<TContext extends Partial<ProviderContext> = ProviderContext> extends ProviderServerOptions<TContext> {
|
|
215
215
|
host?: string;
|
|
216
216
|
port?: number;
|
|
217
217
|
/**
|
|
@@ -221,5 +221,5 @@ export interface ServeOptions extends ProviderServerOptions {
|
|
|
221
221
|
*/
|
|
222
222
|
selfTestPort?: number;
|
|
223
223
|
}
|
|
224
|
-
export declare function serve(provider: ProviderDefinition
|
|
224
|
+
export declare function serve<TContext extends Partial<ProviderContext> = ProviderContext>(provider: ProviderDefinition<TContext>, options?: ServeOptions<TContext>): Promise<ProviderServerHandle>;
|
|
225
225
|
export {};
|
|
@@ -9,7 +9,7 @@ import { validateFailClosedDeclaration } from "../declaration-validation.js";
|
|
|
9
9
|
import { safeProviderErrorObservability } from "../error-observability.js";
|
|
10
10
|
import { SDK_OWNED_PROVIDER_ERROR_CODES, SDK_RUNTIME_OWNED_ERROR_CODES, SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES, } from "../error-resolution.js";
|
|
11
11
|
import { AuthError, isProviderError, isSessionExpiredError, isTransportError, isValidationError, ProviderError, } from "../errors.js";
|
|
12
|
-
import { sanitizeDiagnosticText } from "../fixture-sanitization.js";
|
|
12
|
+
import { REDACTED_FIXTURE_VALUE, sanitizeDiagnosticText, } from "../fixture-sanitization.js";
|
|
13
13
|
import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.js";
|
|
14
14
|
import { categoryForStatus, sourceForCategory, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
|
|
15
15
|
import { createScratchpad } from "../runtime/auth-flow.js";
|
|
@@ -979,8 +979,47 @@ function extractRequestId(raw) {
|
|
|
979
979
|
}
|
|
980
980
|
const MAX_PROVIDER_ERROR_CAUSE_FRAMES = 5;
|
|
981
981
|
const MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH = 300;
|
|
982
|
+
const UNSTRUCTURED_PROVIDER_ERROR_CAUSE_MESSAGE = "[UNSTRUCTURED_UPSTREAM_TEXT]";
|
|
983
|
+
const PROVIDER_ERROR_CAUSE_RETAINED_URL_RUN = /https?:\/\/[^\s"'<>]+/giu;
|
|
984
|
+
const PROVIDER_ERROR_CAUSE_TOKEN_RUN = /\S+/gu;
|
|
985
|
+
const PROVIDER_ERROR_CAUSE_TOKEN_EDGE_PUNCTUATION = /^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu;
|
|
986
|
+
const STRUCTURALLY_SAFE_PROVIDER_ERROR_CAUSE_WORDS = new Set([
|
|
987
|
+
"completion",
|
|
988
|
+
"diagnostic",
|
|
989
|
+
"provider",
|
|
990
|
+
"rejected",
|
|
991
|
+
"returned",
|
|
992
|
+
"upstream",
|
|
993
|
+
]);
|
|
994
|
+
/**
|
|
995
|
+
* Cause frames fail closed when sanitization leaves a plausible credential-shaped free-text run.
|
|
996
|
+
* Redaction sentinels and retained URLs are ignored. Every other whitespace token (or each side
|
|
997
|
+
* of a structured key=value token) with at least eight Unicode characters must reduce to this
|
|
998
|
+
* small vocabulary drawn from SDK diagnostics; counting punctuation keeps bare passwords opaque.
|
|
999
|
+
*/
|
|
1000
|
+
function isStructurallySafeProviderErrorCauseMessage(message) {
|
|
1001
|
+
const classifiableMessage = message
|
|
1002
|
+
.replaceAll(REDACTED_FIXTURE_VALUE, " ")
|
|
1003
|
+
.replace(PROVIDER_ERROR_CAUSE_RETAINED_URL_RUN, " ");
|
|
1004
|
+
for (const match of classifiableMessage.matchAll(PROVIDER_ERROR_CAUSE_TOKEN_RUN)) {
|
|
1005
|
+
const token = match[0];
|
|
1006
|
+
for (const run of token.split("=")) {
|
|
1007
|
+
const diagnosticWord = run
|
|
1008
|
+
.replace(PROVIDER_ERROR_CAUSE_TOKEN_EDGE_PUNCTUATION, "")
|
|
1009
|
+
.toLowerCase();
|
|
1010
|
+
if (STRUCTURALLY_SAFE_PROVIDER_ERROR_CAUSE_WORDS.has(diagnosticWord))
|
|
1011
|
+
continue;
|
|
1012
|
+
if ([...run].length >= 8)
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
982
1018
|
function providerErrorCauseMessage(message) {
|
|
983
1019
|
const sanitized = sanitizeDiagnosticText(message);
|
|
1020
|
+
if (!isStructurallySafeProviderErrorCauseMessage(sanitized)) {
|
|
1021
|
+
return UNSTRUCTURED_PROVIDER_ERROR_CAUSE_MESSAGE;
|
|
1022
|
+
}
|
|
984
1023
|
return sanitized.length > MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH
|
|
985
1024
|
? `${sanitized.slice(0, MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH)}… [truncated]`
|
|
986
1025
|
: sanitized;
|
|
@@ -1646,9 +1685,11 @@ function parseStatefulForwardingEnvelope(rawBody) {
|
|
|
1646
1685
|
* asynchronously, so this path works on Bun and every supported Node release.
|
|
1647
1686
|
*/
|
|
1648
1687
|
export async function createServerAppAsync(provider, options = {}) {
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1688
|
+
const runtimeProvider = provider;
|
|
1689
|
+
const runtimeOptions = options;
|
|
1690
|
+
validateFailClosedDeclaration(runtimeProvider);
|
|
1691
|
+
validateStatefulServerConfig(runtimeOptions);
|
|
1692
|
+
return createServerAppWithCapabilityModules(runtimeProvider, runtimeOptions, await loadProviderCapabilityModules(runtimeProvider));
|
|
1652
1693
|
}
|
|
1653
1694
|
/**
|
|
1654
1695
|
* Synchronous compatibility factory. Standard providers remain synchronous on
|
|
@@ -1657,9 +1698,11 @@ export async function createServerAppAsync(provider, options = {}) {
|
|
|
1657
1698
|
* actionable error directing them to createServerAppAsync().
|
|
1658
1699
|
*/
|
|
1659
1700
|
export function createServerApp(provider, options = {}) {
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1701
|
+
const runtimeProvider = provider;
|
|
1702
|
+
const runtimeOptions = options;
|
|
1703
|
+
validateFailClosedDeclaration(runtimeProvider);
|
|
1704
|
+
validateStatefulServerConfig(runtimeOptions);
|
|
1705
|
+
return createServerAppWithCapabilityModules(runtimeProvider, runtimeOptions, loadProviderCapabilityModulesSync(runtimeProvider));
|
|
1663
1706
|
}
|
|
1664
1707
|
function createServerAppWithCapabilityModules(provider, serverOptions, capabilityModules) {
|
|
1665
1708
|
const options = { ...serverOptions, capabilityModules };
|
package/dist/types.d.ts
CHANGED
|
@@ -2024,7 +2024,7 @@ export interface ProviderDeploymentOverrides {
|
|
|
2024
2024
|
};
|
|
2025
2025
|
buildContext?: string;
|
|
2026
2026
|
}
|
|
2027
|
-
export interface ProviderDefinition {
|
|
2027
|
+
export interface ProviderDefinition<TContext = ProviderContext> {
|
|
2028
2028
|
id: string;
|
|
2029
2029
|
version: string;
|
|
2030
2030
|
runtime: "standard" | "shared" | "browser";
|
|
@@ -2054,7 +2054,7 @@ export interface ProviderDefinition {
|
|
|
2054
2054
|
credential?: CredentialDeclaration;
|
|
2055
2055
|
context?: ContextDeclaration;
|
|
2056
2056
|
meta: ProviderMeta;
|
|
2057
|
-
operations: Record<string, OperationDefinition<SchemaLike, SchemaLike>>;
|
|
2057
|
+
operations: Record<string, OperationDefinition<SchemaLike, SchemaLike, TContext>>;
|
|
2058
2058
|
healthMonitor?: ProviderHealthMonitorConfig;
|
|
2059
2059
|
/** Transitional alias for `healthMonitor`; `defineProvider` mirrors both. */
|
|
2060
2060
|
healthProbe?: ProviderHealthProbeConfig;
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.2.0-beta.
|
|
2
|
+
"version": "2.2.0-beta.43",
|
|
3
3
|
"name": "@apifuse/provider-sdk",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
@@ -128,7 +128,8 @@
|
|
|
128
128
|
"@microsoft/api-extractor": "^7.58.13",
|
|
129
129
|
"@types/bun": "latest",
|
|
130
130
|
"@types/node": "^25.9.3",
|
|
131
|
-
"ajv": "^8.17"
|
|
131
|
+
"ajv": "^8.17",
|
|
132
|
+
"typescript": "6.0.3"
|
|
132
133
|
},
|
|
133
134
|
"dependencies": {
|
|
134
135
|
"@clack/prompts": "^1.5.1",
|
|
@@ -145,7 +146,6 @@
|
|
|
145
146
|
"safe-regex": "^2.1",
|
|
146
147
|
"socks": "^2.8.9",
|
|
147
148
|
"tough-cookie": "^6.0.2",
|
|
148
|
-
"typescript": "6.0.3",
|
|
149
149
|
"wreq-js": "3.0.0",
|
|
150
150
|
"zod": "^4.4.3"
|
|
151
151
|
},
|
package/src/cli/commands.ts
CHANGED
|
@@ -59,7 +59,7 @@ export const COMMAND_MANIFEST: Record<
|
|
|
59
59
|
"migrate-shape": {
|
|
60
60
|
name: "migrate-shape",
|
|
61
61
|
summary:
|
|
62
|
-
"Migrate a provider
|
|
62
|
+
"Migrate a provider to the phase-separated SDK: two-phase defineProvider in index.ts and curried defineOperation across sources.",
|
|
63
63
|
usage: "apifuse migrate-shape [path] [--check] [--json]",
|
|
64
64
|
examples: ["apifuse migrate-shape .", "apifuse migrate-shape . --check"],
|
|
65
65
|
modulePath: "./apifuse-migrate-shape",
|
package/src/cli/create.ts
CHANGED
|
@@ -26,7 +26,7 @@ export const CATEGORY_OPTIONS = [
|
|
|
26
26
|
"other",
|
|
27
27
|
] as const;
|
|
28
28
|
export const AUTH_MODE_OPTIONS = ["none", "platform-managed", "credentials", "oauth2"] as const;
|
|
29
|
-
export const RUNTIME_OPTIONS = ["standard", "browser"] as const;
|
|
29
|
+
export const RUNTIME_OPTIONS = ["standard", "shared", "browser"] as const;
|
|
30
30
|
export const PRESET_OPTIONS = ["standalone", "monorepo"] as const;
|
|
31
31
|
|
|
32
32
|
export type CreateCategory = (typeof CATEGORY_OPTIONS)[number];
|
|
@@ -102,7 +102,7 @@ Options:
|
|
|
102
102
|
--display-name <name>
|
|
103
103
|
--category <category>
|
|
104
104
|
--auth-mode <mode>
|
|
105
|
-
--runtime <standard|browser>
|
|
105
|
+
--runtime <standard|shared|browser>
|
|
106
106
|
--yes
|
|
107
107
|
--dry-run
|
|
108
108
|
--json
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type TS from "typescript";
|
|
2
|
+
|
|
3
|
+
const ts: typeof import("typescript") = await loadTypeScript();
|
|
4
|
+
|
|
5
|
+
async function loadTypeScript(): Promise<typeof import("typescript")> {
|
|
6
|
+
try {
|
|
7
|
+
return await import("typescript");
|
|
8
|
+
} catch {
|
|
9
|
+
console.error(
|
|
10
|
+
"apifuse migrate-shape requires typescript; install it in the workspace running the CLI (bun add -d typescript)",
|
|
11
|
+
);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Operation authoring-shape migration.
|
|
18
|
+
*
|
|
19
|
+
* `defineOperation` / `defineStreamOperation` changed alongside the provider
|
|
20
|
+
* builder in 2.2.0-beta.37: the identity helper became a curried,
|
|
21
|
+
* context-typed factory.
|
|
22
|
+
*
|
|
23
|
+
* before: defineOperation({ ...config })
|
|
24
|
+
* after: defineOperation<ProviderContext>()({ ...config })
|
|
25
|
+
*
|
|
26
|
+
* The old call still type-checks against an older pin, but under the new SDK
|
|
27
|
+
* it returns the INNER FACTORY FUNCTION with the config swallowed as an
|
|
28
|
+
* ignored argument, so every operation in the map becomes a function and the
|
|
29
|
+
* provider fails `finalizeProvider` with a misleading "declares neither
|
|
30
|
+
* healthCheck nor healthCheckUnsupported" error. Measured fleet blast radius
|
|
31
|
+
* (2026-08-29): 53 of 84 repositories, 450 legacy call sites.
|
|
32
|
+
*
|
|
33
|
+
* Same contract as the provider-shape transform: rewrite only what is fully
|
|
34
|
+
* understood, report a reasoned skip otherwise, never emit a partial file.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export type OperationShapeMigration =
|
|
38
|
+
| {
|
|
39
|
+
readonly status: "migrated";
|
|
40
|
+
readonly code: string;
|
|
41
|
+
/** Number of call sites rewritten in this file. */
|
|
42
|
+
readonly rewrites: number;
|
|
43
|
+
/** True when a `ProviderContext` type import was added. */
|
|
44
|
+
readonly importAdded: boolean;
|
|
45
|
+
}
|
|
46
|
+
| { readonly status: "unchanged"; readonly code: string }
|
|
47
|
+
| { readonly status: "skipped"; readonly reason: string };
|
|
48
|
+
|
|
49
|
+
const OPERATION_HELPERS = new Set(["defineOperation", "defineStreamOperation"]);
|
|
50
|
+
const PROVIDER_CONTEXT_TYPE_NAME = "ProviderContext";
|
|
51
|
+
const PROVIDER_SDK_PROVIDER_SUBPATH = "@apifuse/provider-sdk/provider";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Migrate legacy `defineOperation(config)` calls in one source file to the
|
|
55
|
+
* curried `defineOperation<ProviderContext>()(config)` form.
|
|
56
|
+
*
|
|
57
|
+
* @param contextImportSpecifier module specifier the `ProviderContext` type
|
|
58
|
+
* import should come from when one has to be added — `"../index"` for
|
|
59
|
+
* operation modules, `"./index"` is never needed because index.ts declares
|
|
60
|
+
* the alias itself. Callers pass the correct relative path per file.
|
|
61
|
+
*/
|
|
62
|
+
export function migrateOperationShape(
|
|
63
|
+
sourceText: string,
|
|
64
|
+
fileName: string,
|
|
65
|
+
contextImportSpecifier: string,
|
|
66
|
+
): OperationShapeMigration {
|
|
67
|
+
const source = ts.createSourceFile(
|
|
68
|
+
fileName,
|
|
69
|
+
sourceText,
|
|
70
|
+
ts.ScriptTarget.Latest,
|
|
71
|
+
true,
|
|
72
|
+
ts.ScriptKind.TS,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const parseError = firstSyntaxError(source);
|
|
76
|
+
if (parseError !== undefined) {
|
|
77
|
+
return { status: "skipped", reason: parseError };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Every legacy call site: a direct call to the bare helper identifier
|
|
81
|
+
// whose single argument is the config (i.e. NOT the curried form, whose
|
|
82
|
+
// outer call has zero arguments and optional type arguments).
|
|
83
|
+
const legacyCalls: TS.CallExpression[] = [];
|
|
84
|
+
let sawCurried = false;
|
|
85
|
+
const visit = (node: TS.Node): void => {
|
|
86
|
+
if (
|
|
87
|
+
ts.isCallExpression(node) &&
|
|
88
|
+
ts.isIdentifier(node.expression) &&
|
|
89
|
+
OPERATION_HELPERS.has(node.expression.text)
|
|
90
|
+
) {
|
|
91
|
+
if (node.arguments.length === 0) {
|
|
92
|
+
sawCurried = true; // already-migrated outer call
|
|
93
|
+
} else {
|
|
94
|
+
legacyCalls.push(node);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
ts.forEachChild(node, visit);
|
|
98
|
+
};
|
|
99
|
+
visit(source);
|
|
100
|
+
|
|
101
|
+
if (legacyCalls.length === 0) {
|
|
102
|
+
return { status: "unchanged", code: sourceText };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// A file mixing both forms almost certainly had a partial hand-migration;
|
|
106
|
+
// refuse rather than guess which convention the author wants.
|
|
107
|
+
if (sawCurried) {
|
|
108
|
+
return {
|
|
109
|
+
status: "skipped",
|
|
110
|
+
reason:
|
|
111
|
+
"File mixes legacy defineOperation(config) with curried defineOperation<...>()(config); migrate it by hand.",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const declaresContextAlias = source.statements.some(
|
|
116
|
+
(statement) =>
|
|
117
|
+
ts.isTypeAliasDeclaration(statement) &&
|
|
118
|
+
statement.name.text === PROVIDER_CONTEXT_TYPE_NAME,
|
|
119
|
+
);
|
|
120
|
+
const importsContext = sourceText.includes(PROVIDER_CONTEXT_TYPE_NAME);
|
|
121
|
+
const needsImport = !declaresContextAlias && !importsContext;
|
|
122
|
+
|
|
123
|
+
const edits: { start: number; end: number; text: string }[] = [];
|
|
124
|
+
for (const call of legacyCalls) {
|
|
125
|
+
// `defineOperation(` -> `defineOperation<ProviderContext>()(`
|
|
126
|
+
const callee = call.expression;
|
|
127
|
+
edits.push({
|
|
128
|
+
start: callee.getEnd(),
|
|
129
|
+
end: callee.getEnd(),
|
|
130
|
+
text: `<${PROVIDER_CONTEXT_TYPE_NAME}>()`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let importAdded = false;
|
|
135
|
+
if (needsImport) {
|
|
136
|
+
const lastImport = [...source.statements]
|
|
137
|
+
.reverse()
|
|
138
|
+
.find((statement) => ts.isImportDeclaration(statement));
|
|
139
|
+
const insertAt = lastImport ? lastImport.getEnd() : 0;
|
|
140
|
+
const importText = `${lastImport ? "\n" : ""}import type { ${PROVIDER_CONTEXT_TYPE_NAME} } from "${contextImportSpecifier}";${lastImport ? "" : "\n"}`;
|
|
141
|
+
edits.push({ start: insertAt, end: insertAt, text: importText });
|
|
142
|
+
importAdded = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const ordered = edits.sort((a, b) => b.start - a.start);
|
|
146
|
+
let code = sourceText;
|
|
147
|
+
for (const edit of ordered) {
|
|
148
|
+
code = code.slice(0, edit.start) + edit.text + code.slice(edit.end);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const verified = ts.createSourceFile(
|
|
152
|
+
fileName,
|
|
153
|
+
code,
|
|
154
|
+
ts.ScriptTarget.Latest,
|
|
155
|
+
true,
|
|
156
|
+
ts.ScriptKind.TS,
|
|
157
|
+
);
|
|
158
|
+
const outputError = firstSyntaxError(verified);
|
|
159
|
+
if (outputError !== undefined) {
|
|
160
|
+
return {
|
|
161
|
+
status: "skipped",
|
|
162
|
+
reason: `Transform produced source that does not parse (${outputError}); refusing to emit a partial migration.`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
status: "migrated",
|
|
168
|
+
code,
|
|
169
|
+
rewrites: legacyCalls.length,
|
|
170
|
+
importAdded,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function firstSyntaxError(source: TS.SourceFile): string | undefined {
|
|
175
|
+
const diagnostics = (
|
|
176
|
+
source as TS.SourceFile & { parseDiagnostics?: TS.DiagnosticWithLocation[] }
|
|
177
|
+
).parseDiagnostics;
|
|
178
|
+
if (diagnostics === undefined || diagnostics.length === 0) return undefined;
|
|
179
|
+
const first = diagnostics[0];
|
|
180
|
+
if (first === undefined) return undefined;
|
|
181
|
+
const message = ts.flattenDiagnosticMessageText(first.messageText, " ");
|
|
182
|
+
const { line } = source.getLineAndCharacterOfPosition(first.start);
|
|
183
|
+
return `${message} (line ${line + 1})`;
|
|
184
|
+
}
|