@apifuse/provider-sdk 2.2.0-beta.50 → 2.2.0-beta.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.51
4
+
5
+ - Release candidate for main commit a1269b0a7161618e5fc25e532aaa45de0efd632f.
6
+
3
7
  ## 2.2.0-beta.50
4
8
 
5
9
  - Release candidate for main commit 341c2f0e55a4159cac423fec0bf4f84625191731.
@@ -62,6 +62,7 @@ export function migrateOperationDeclaration(sourceText, fileName, options = {})
62
62
  };
63
63
  }
64
64
  const constObjects = collectModuleConstObjects(source);
65
+ const constArrays = collectModuleConstArrays(source);
65
66
  const discovery = discoverOperationSites(source, fileName, constObjects, options.operationIds);
66
67
  if (discovery.refusals.length > 0) {
67
68
  return { status: "refused", refusals: discovery.refusals };
@@ -70,7 +71,7 @@ export function migrateOperationDeclaration(sourceText, fileName, options = {})
70
71
  const todos = [];
71
72
  const refusals = [];
72
73
  for (const site of discovery.sites) {
73
- const plan = planOperationMigration(source, fileName, site, constObjects, options.localeFiles ?? []);
74
+ const plan = planOperationMigration(source, fileName, site, constObjects, constArrays, options.localeFiles ?? []);
74
75
  if ("refusal" in plan) {
75
76
  refusals.push(plan.refusal);
76
77
  continue;
@@ -87,7 +88,11 @@ export function migrateOperationDeclaration(sourceText, fileName, options = {})
87
88
  operations: discovery.sites.length,
88
89
  };
89
90
  }
90
- const code = applyEdits(sourceText, edits);
91
+ const normalizedEdits = normalizeEdits(edits, fileName);
92
+ if ("refusal" in normalizedEdits) {
93
+ return { status: "refused", refusals: [normalizedEdits.refusal] };
94
+ }
95
+ const code = applyEdits(sourceText, normalizedEdits.edits);
91
96
  const outputRefusal = verifyOperationDeclarationRewrite(code, fileName);
92
97
  if (outputRefusal !== undefined) {
93
98
  return {
@@ -109,7 +114,7 @@ export function verifyOperationDeclarationRewrite(code, fileName) {
109
114
  return undefined;
110
115
  return refusal(fileName, "<file>", "codemod_syntax", `Transform output did not parse: ${outputError}`);
111
116
  }
112
- function planOperationMigration(source, fileName, site, constObjects, localeFiles) {
117
+ function planOperationMigration(source, fileName, site, constObjects, constArrays, localeFiles) {
113
118
  if (site.object.properties.some(ts.isSpreadAssignment)) {
114
119
  return {
115
120
  refusal: refusal(fileName, site.operationKey, "non_literal", "Top-level operation spreads cannot be rewritten without changing their shared declaration."),
@@ -216,7 +221,7 @@ function planOperationMigration(source, fileName, site, constObjects, localeFile
216
221
  if (merged.insert !== undefined)
217
222
  addInsertion(insertions, "docs", merged.insert);
218
223
  }
219
- const examples = planExamples(top.byName.get("inputExamples"), top.byName.get("examples"), fileName, site, source, localeFiles);
224
+ const examples = planExamples(top.byName.get("inputExamples"), top.byName.get("examples"), fileName, site, source, constArrays, localeFiles);
220
225
  if ("refusal" in examples)
221
226
  return examples;
222
227
  const edits = [...examples.edits];
@@ -376,7 +381,7 @@ function mergeFlatAndNested(field, flat, nested, fileName, operationKey, source,
376
381
  }
377
382
  return {};
378
383
  }
379
- function planExamples(inputExamples, existingExamples, fileName, site, source, localeFiles) {
384
+ function planExamples(inputExamples, existingExamples, fileName, site, source, constArrays, localeFiles) {
380
385
  if (inputExamples === undefined)
381
386
  return { edits: [], localeTodos: [] };
382
387
  if (existingExamples !== undefined) {
@@ -388,12 +393,15 @@ function planExamples(inputExamples, existingExamples, fileName, site, source, l
388
393
  if (array === undefined || !ts.isArrayLiteralExpression(array)) {
389
394
  return nonLiteral(fileName, site.operationKey, "inputExamples must be an array literal.");
390
395
  }
391
- if (array.elements.length > 0 && !site.operationIdProven) {
396
+ const expanded = expandArrayElements(array, constArrays, fileName, site.operationKey);
397
+ if ("refusal" in expanded)
398
+ return expanded;
399
+ if (expanded.elements.length > 0 && !site.operationIdProven) {
392
400
  return {
393
401
  refusal: refusal(fileName, site.operationKey, "operation_id_unresolved", "inputExamples requires an exact operation id proven from a static operations map."),
394
402
  };
395
403
  }
396
- if (array.elements.length > 0 && !localeFiles.includes("locales/en.json")) {
404
+ if (expanded.elements.length > 0 && !localeFiles.includes("locales/en.json")) {
397
405
  return {
398
406
  refusal: refusal(fileName, site.operationKey, "missing_english_locale", "inputExamples cannot be migrated because locales/en.json does not exist."),
399
407
  };
@@ -409,8 +417,8 @@ function planExamples(inputExamples, existingExamples, fileName, site, source, l
409
417
  end: propertyNameNode.getEnd(),
410
418
  text: "examples",
411
419
  });
412
- for (let index = 0; index < array.elements.length; index += 1) {
413
- const element = array.elements[index];
420
+ for (let index = 0; index < expanded.elements.length; index += 1) {
421
+ const element = expanded.elements[index];
414
422
  const object = element === undefined ? undefined : unwrapExpression(element);
415
423
  if (object === undefined || !ts.isObjectLiteralExpression(object)) {
416
424
  return nonLiteral(fileName, site.operationKey, `inputExamples[${index}] must be an object literal.`);
@@ -696,6 +704,51 @@ function collectModuleConstObjects(source) {
696
704
  }
697
705
  return objects;
698
706
  }
707
+ function collectModuleConstArrays(source) {
708
+ const arrays = new Map();
709
+ for (const statement of source.statements) {
710
+ if (!ts.isVariableStatement(statement))
711
+ continue;
712
+ if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
713
+ continue;
714
+ for (const declaration of statement.declarationList.declarations) {
715
+ if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined)
716
+ continue;
717
+ const expression = unwrapExpression(declaration.initializer);
718
+ if (expression !== undefined && ts.isArrayLiteralExpression(expression)) {
719
+ arrays.set(declaration.name.text, expression);
720
+ }
721
+ }
722
+ }
723
+ return arrays;
724
+ }
725
+ function expandArrayElements(array, constArrays, fileName, operationKey, seen = new Set()) {
726
+ if (seen.has(array)) {
727
+ return nonLiteral(fileName, operationKey, "A module-level array spread is recursive.");
728
+ }
729
+ seen.add(array);
730
+ const elements = [];
731
+ for (const element of array.elements) {
732
+ if (!ts.isSpreadElement(element)) {
733
+ elements.push(element);
734
+ continue;
735
+ }
736
+ const expression = unwrapExpression(element.expression);
737
+ const spreadArray = expression !== undefined && ts.isArrayLiteralExpression(expression)
738
+ ? expression
739
+ : expression !== undefined && ts.isIdentifier(expression)
740
+ ? constArrays.get(expression.text)
741
+ : undefined;
742
+ if (spreadArray === undefined) {
743
+ return nonLiteral(fileName, operationKey, "inputExamples contains an unresolved array spread.");
744
+ }
745
+ const expanded = expandArrayElements(spreadArray, constArrays, fileName, operationKey, new Set(seen));
746
+ if ("refusal" in expanded)
747
+ return expanded;
748
+ elements.push(...expanded.elements);
749
+ }
750
+ return { elements };
751
+ }
699
752
  function expandMembers(object, constObjects, fileName, operationKey, seen = new Set()) {
700
753
  if (seen.has(object)) {
701
754
  return nonLiteral(fileName, operationKey, "A module-level object spread is recursive.");
@@ -929,6 +982,21 @@ function applyEdits(sourceText, edits) {
929
982
  }
930
983
  return code;
931
984
  }
985
+ function normalizeEdits(edits, fileName) {
986
+ const unique = new Map();
987
+ for (const edit of edits) {
988
+ const key = `${edit.start}:${edit.end}`;
989
+ const previous = unique.get(key);
990
+ if (previous === undefined) {
991
+ unique.set(key, edit);
992
+ continue;
993
+ }
994
+ if (previous.text !== edit.text) {
995
+ return nonLiteral(fileName, "<shared>", "A module-level input examples array is shared by operations that need different locale keys.");
996
+ }
997
+ }
998
+ return { edits: [...unique.values()] };
999
+ }
932
1000
  function indentationAt(text, position) {
933
1001
  const lineStart = text.lastIndexOf("\n", position - 1) + 1;
934
1002
  return text.slice(lineStart, position).match(/^\s*/)?.[0] ?? "";
@@ -1075,7 +1143,10 @@ function collectSourceFiles(root) {
1075
1143
  if (entry.isDirectory()) {
1076
1144
  if (SOURCE_SKIP_DIRECTORIES.has(entry.name))
1077
1145
  continue;
1078
- walk(join(directory, entry.name));
1146
+ const child = join(directory, entry.name);
1147
+ if (existsSync(join(child, ".git")))
1148
+ continue;
1149
+ walk(child);
1079
1150
  continue;
1080
1151
  }
1081
1152
  if (!entry.name.endsWith(".ts"))
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.50",
2
+ "version": "2.2.0-beta.51",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -0,0 +1,15 @@
1
+ const LIST_INPUT_EXAMPLES = [
2
+ {
3
+ scenario: "List a full year",
4
+ rationale: "Shows the common calendar query",
5
+ input: { year: 2026 },
6
+ },
7
+ ] as const;
8
+
9
+ const listOperation = defineOperation<ProviderContext>()({
10
+ annotations: { readOnly: true },
11
+ inputExamples: [...LIST_INPUT_EXAMPLES],
12
+ input: InputSchema,
13
+ output: OutputSchema,
14
+ handler,
15
+ });
@@ -0,0 +1,9 @@
1
+ import { LIST_INPUT_EXAMPLES } from "./shared.js";
2
+
3
+ const importedExamplesOperation = defineOperation<ProviderContext>()({
4
+ annotations: { readOnly: true },
5
+ inputExamples: [...LIST_INPUT_EXAMPLES],
6
+ input: InputSchema,
7
+ output: OutputSchema,
8
+ handler,
9
+ });
@@ -0,0 +1,24 @@
1
+ const SHARED_INPUT_EXAMPLES = [
2
+ {
3
+ scenario: "Shared middle example",
4
+ input: { page: 2 },
5
+ },
6
+ ] as const;
7
+
8
+ const mixedOperation = defineOperation<ProviderContext>()({
9
+ annotations: { readOnly: true },
10
+ inputExamples: [
11
+ {
12
+ scenario: "Literal first example",
13
+ input: { page: 1 },
14
+ },
15
+ ...SHARED_INPUT_EXAMPLES,
16
+ {
17
+ scenario: "Literal last example",
18
+ input: { page: 3 },
19
+ },
20
+ ],
21
+ input: InputSchema,
22
+ output: OutputSchema,
23
+ handler,
24
+ });
@@ -154,6 +154,7 @@ export function migrateOperationDeclaration(
154
154
  }
155
155
 
156
156
  const constObjects = collectModuleConstObjects(source);
157
+ const constArrays = collectModuleConstArrays(source);
157
158
  const discovery = discoverOperationSites(source, fileName, constObjects, options.operationIds);
158
159
  if (discovery.refusals.length > 0) {
159
160
  return { status: "refused", refusals: discovery.refusals };
@@ -168,6 +169,7 @@ export function migrateOperationDeclaration(
168
169
  fileName,
169
170
  site,
170
171
  constObjects,
172
+ constArrays,
171
173
  options.localeFiles ?? [],
172
174
  );
173
175
  if ("refusal" in plan) {
@@ -187,7 +189,12 @@ export function migrateOperationDeclaration(
187
189
  };
188
190
  }
189
191
 
190
- const code = applyEdits(sourceText, edits);
192
+ const normalizedEdits = normalizeEdits(edits, fileName);
193
+ if ("refusal" in normalizedEdits) {
194
+ return { status: "refused", refusals: [normalizedEdits.refusal] };
195
+ }
196
+
197
+ const code = applyEdits(sourceText, normalizedEdits.edits);
191
198
  const outputRefusal = verifyOperationDeclarationRewrite(code, fileName);
192
199
  if (outputRefusal !== undefined) {
193
200
  return {
@@ -224,6 +231,7 @@ function planOperationMigration(
224
231
  fileName: string,
225
232
  site: OperationSite,
226
233
  constObjects: ReadonlyMap<string, TS.ObjectLiteralExpression>,
234
+ constArrays: ReadonlyMap<string, TS.ArrayLiteralExpression>,
227
235
  localeFiles: readonly string[],
228
236
  ): PlannedOperation | { readonly refusal: OperationDeclarationRefusal } {
229
237
  if (site.object.properties.some(ts.isSpreadAssignment)) {
@@ -398,6 +406,7 @@ function planOperationMigration(
398
406
  fileName,
399
407
  site,
400
408
  source,
409
+ constArrays,
401
410
  localeFiles,
402
411
  );
403
412
  if ("refusal" in examples) return examples;
@@ -653,6 +662,7 @@ function planExamples(
653
662
  fileName: string,
654
663
  site: OperationSite,
655
664
  source: TS.SourceFile,
665
+ constArrays: ReadonlyMap<string, TS.ArrayLiteralExpression>,
656
666
  localeFiles: readonly string[],
657
667
  ):
658
668
  | { readonly edits: readonly TextEdit[]; readonly localeTodos: readonly LocaleTodo[] }
@@ -672,7 +682,9 @@ function planExamples(
672
682
  if (array === undefined || !ts.isArrayLiteralExpression(array)) {
673
683
  return nonLiteral(fileName, site.operationKey, "inputExamples must be an array literal.");
674
684
  }
675
- if (array.elements.length > 0 && !site.operationIdProven) {
685
+ const expanded = expandArrayElements(array, constArrays, fileName, site.operationKey);
686
+ if ("refusal" in expanded) return expanded;
687
+ if (expanded.elements.length > 0 && !site.operationIdProven) {
676
688
  return {
677
689
  refusal: refusal(
678
690
  fileName,
@@ -682,7 +694,7 @@ function planExamples(
682
694
  ),
683
695
  };
684
696
  }
685
- if (array.elements.length > 0 && !localeFiles.includes("locales/en.json")) {
697
+ if (expanded.elements.length > 0 && !localeFiles.includes("locales/en.json")) {
686
698
  return {
687
699
  refusal: refusal(
688
700
  fileName,
@@ -709,8 +721,8 @@ function planExamples(
709
721
  text: "examples",
710
722
  });
711
723
 
712
- for (let index = 0; index < array.elements.length; index += 1) {
713
- const element = array.elements[index];
724
+ for (let index = 0; index < expanded.elements.length; index += 1) {
725
+ const element = expanded.elements[index];
714
726
  const object = element === undefined ? undefined : unwrapExpression(element);
715
727
  if (object === undefined || !ts.isObjectLiteralExpression(object)) {
716
728
  return nonLiteral(
@@ -1070,6 +1082,66 @@ function collectModuleConstObjects(source: TS.SourceFile): Map<string, TS.Object
1070
1082
  return objects;
1071
1083
  }
1072
1084
 
1085
+ function collectModuleConstArrays(source: TS.SourceFile): Map<string, TS.ArrayLiteralExpression> {
1086
+ const arrays = new Map<string, TS.ArrayLiteralExpression>();
1087
+ for (const statement of source.statements) {
1088
+ if (!ts.isVariableStatement(statement)) continue;
1089
+ if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
1090
+ for (const declaration of statement.declarationList.declarations) {
1091
+ if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined) continue;
1092
+ const expression = unwrapExpression(declaration.initializer);
1093
+ if (expression !== undefined && ts.isArrayLiteralExpression(expression)) {
1094
+ arrays.set(declaration.name.text, expression);
1095
+ }
1096
+ }
1097
+ }
1098
+ return arrays;
1099
+ }
1100
+
1101
+ function expandArrayElements(
1102
+ array: TS.ArrayLiteralExpression,
1103
+ constArrays: ReadonlyMap<string, TS.ArrayLiteralExpression>,
1104
+ fileName: string,
1105
+ operationKey: string,
1106
+ seen = new Set<TS.ArrayLiteralExpression>(),
1107
+ ): { readonly elements: TS.Expression[] } | { readonly refusal: OperationDeclarationRefusal } {
1108
+ if (seen.has(array)) {
1109
+ return nonLiteral(fileName, operationKey, "A module-level array spread is recursive.");
1110
+ }
1111
+ seen.add(array);
1112
+ const elements: TS.Expression[] = [];
1113
+ for (const element of array.elements) {
1114
+ if (!ts.isSpreadElement(element)) {
1115
+ elements.push(element);
1116
+ continue;
1117
+ }
1118
+ const expression = unwrapExpression(element.expression);
1119
+ const spreadArray =
1120
+ expression !== undefined && ts.isArrayLiteralExpression(expression)
1121
+ ? expression
1122
+ : expression !== undefined && ts.isIdentifier(expression)
1123
+ ? constArrays.get(expression.text)
1124
+ : undefined;
1125
+ if (spreadArray === undefined) {
1126
+ return nonLiteral(
1127
+ fileName,
1128
+ operationKey,
1129
+ "inputExamples contains an unresolved array spread.",
1130
+ );
1131
+ }
1132
+ const expanded = expandArrayElements(
1133
+ spreadArray,
1134
+ constArrays,
1135
+ fileName,
1136
+ operationKey,
1137
+ new Set(seen),
1138
+ );
1139
+ if ("refusal" in expanded) return expanded;
1140
+ elements.push(...expanded.elements);
1141
+ }
1142
+ return { elements };
1143
+ }
1144
+
1073
1145
  function expandMembers(
1074
1146
  object: TS.ObjectLiteralExpression,
1075
1147
  constObjects: ReadonlyMap<string, TS.ObjectLiteralExpression>,
@@ -1357,6 +1429,29 @@ function applyEdits(sourceText: string, edits: readonly TextEdit[]): string {
1357
1429
  return code;
1358
1430
  }
1359
1431
 
1432
+ function normalizeEdits(
1433
+ edits: readonly TextEdit[],
1434
+ fileName: string,
1435
+ ): { readonly edits: readonly TextEdit[] } | { readonly refusal: OperationDeclarationRefusal } {
1436
+ const unique = new Map<string, TextEdit>();
1437
+ for (const edit of edits) {
1438
+ const key = `${edit.start}:${edit.end}`;
1439
+ const previous = unique.get(key);
1440
+ if (previous === undefined) {
1441
+ unique.set(key, edit);
1442
+ continue;
1443
+ }
1444
+ if (previous.text !== edit.text) {
1445
+ return nonLiteral(
1446
+ fileName,
1447
+ "<shared>",
1448
+ "A module-level input examples array is shared by operations that need different locale keys.",
1449
+ );
1450
+ }
1451
+ }
1452
+ return { edits: [...unique.values()] };
1453
+ }
1454
+
1360
1455
  function indentationAt(text: string, position: number): string {
1361
1456
  const lineStart = text.lastIndexOf("\n", position - 1) + 1;
1362
1457
  return text.slice(lineStart, position).match(/^\s*/)?.[0] ?? "";
@@ -1548,7 +1643,9 @@ function collectSourceFiles(root: string): string[] {
1548
1643
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
1549
1644
  if (entry.isDirectory()) {
1550
1645
  if (SOURCE_SKIP_DIRECTORIES.has(entry.name)) continue;
1551
- walk(join(directory, entry.name));
1646
+ const child = join(directory, entry.name);
1647
+ if (existsSync(join(child, ".git"))) continue;
1648
+ walk(child);
1552
1649
  continue;
1553
1650
  }
1554
1651
  if (!entry.name.endsWith(".ts")) continue;