@apifuse/provider-sdk 2.2.0-beta.53 → 2.2.0-beta.55
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/dist/cli/migrate-operation-declaration.d.ts +5 -1
- package/dist/cli/migrate-operation-declaration.js +645 -46
- package/dist/runtime/auth-flow.d.ts +1 -0
- package/dist/runtime/auth-flow.js +2 -0
- package/dist/runtime/trace.d.ts +7 -0
- package/dist/runtime/trace.js +24 -1
- package/dist/server/serve-implementation.d.ts +4 -0
- package/dist/server/serve-implementation.js +576 -321
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-a.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-b.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-barrel.ts.txt +2 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-index.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-computed.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-a.ts.txt +1 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-b.ts.txt +1 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-index.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-barrel.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-catalog.ts.txt +19 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-docs.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-index.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-stores.ts.txt +8 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-indirect-config.ts.txt +5 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-nonstatic.ts.txt +4 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-package-index.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-index.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-leaf.ts.txt +10 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-one.ts.txt +1 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-two.ts.txt +1 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-barrel.ts.txt +2 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-index.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-leaf.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-duplicate-provider.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-id-unresolved.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-many.ts.txt +18 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-one.ts.txt +13 -0
- package/src/cli/migrate-operation-declaration.ts +844 -49
- package/src/runtime/auth-flow.ts +3 -0
- package/src/runtime/trace.ts +37 -1
- package/src/server/serve-implementation.ts +759 -586
- package/src/types.ts +1 -0
|
@@ -55,6 +55,9 @@ const SIDECAR_NAME = "migrate-operation-declaration.locales-todo.json";
|
|
|
55
55
|
* copies every untouched source slice byte-for-byte.
|
|
56
56
|
*/
|
|
57
57
|
export function migrateOperationDeclaration(sourceText, fileName, options = {}) {
|
|
58
|
+
return migrateOperationDeclarationInternal(sourceText, fileName, options);
|
|
59
|
+
}
|
|
60
|
+
function migrateOperationDeclarationInternal(sourceText, fileName, options, context = {}) {
|
|
58
61
|
const source = parseSource(fileName, sourceText);
|
|
59
62
|
const parseError = firstSyntaxError(source);
|
|
60
63
|
if (parseError !== undefined) {
|
|
@@ -65,7 +68,7 @@ export function migrateOperationDeclaration(sourceText, fileName, options = {})
|
|
|
65
68
|
}
|
|
66
69
|
const constObjects = collectModuleConstObjects(source);
|
|
67
70
|
const constArrays = collectModuleConstArrays(source);
|
|
68
|
-
const discovery = discoverOperationSites(source, fileName, constObjects, options.operationIds);
|
|
71
|
+
const discovery = discoverOperationSites(source, fileName, constObjects, options.operationIds, context.operationSites, context.excludedBindings);
|
|
69
72
|
if (discovery.refusals.length > 0) {
|
|
70
73
|
return { status: "refused", refusals: discovery.refusals };
|
|
71
74
|
}
|
|
@@ -73,7 +76,7 @@ export function migrateOperationDeclaration(sourceText, fileName, options = {})
|
|
|
73
76
|
const todos = [];
|
|
74
77
|
const refusals = [];
|
|
75
78
|
for (const site of discovery.sites) {
|
|
76
|
-
const plan = planOperationMigration(source, fileName, site, constObjects, constArrays, options.localeFiles ?? []);
|
|
79
|
+
const plan = planOperationMigration(source, fileName, site, constObjects, constArrays, options.localeFiles ?? [], context.staticObjectResolver);
|
|
77
80
|
if ("refusal" in plan) {
|
|
78
81
|
refusals.push(plan.refusal);
|
|
79
82
|
continue;
|
|
@@ -120,13 +123,13 @@ export function verifyOperationDeclarationRewrite(code, fileName) {
|
|
|
120
123
|
return undefined;
|
|
121
124
|
return refusal(fileName, "<file>", "codemod_syntax", `Transform output did not parse: ${outputError}`);
|
|
122
125
|
}
|
|
123
|
-
function planOperationMigration(source, fileName, site, constObjects, constArrays, localeFiles) {
|
|
126
|
+
function planOperationMigration(source, fileName, site, constObjects, constArrays, localeFiles, staticObjectResolver) {
|
|
124
127
|
if (site.object.properties.some(ts.isSpreadAssignment)) {
|
|
125
128
|
return {
|
|
126
129
|
refusal: refusal(fileName, site.operationKey, "non_literal", "Top-level operation spreads cannot be rewritten without changing their shared declaration."),
|
|
127
130
|
};
|
|
128
131
|
}
|
|
129
|
-
const expanded = expandMembers(site.object, constObjects, fileName, site.operationKey);
|
|
132
|
+
const expanded = expandMembers(site.object, source, constObjects, fileName, site.operationKey, staticObjectResolver);
|
|
130
133
|
if ("refusal" in expanded)
|
|
131
134
|
return expanded;
|
|
132
135
|
const top = indexMembers(expanded.members, fileName, site.operationKey);
|
|
@@ -142,13 +145,13 @@ function planOperationMigration(source, fileName, site, constObjects, constArray
|
|
|
142
145
|
const member = top.byName.get(containerName);
|
|
143
146
|
if (member === undefined)
|
|
144
147
|
continue;
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
148
|
+
const resolvedObject = resolveObjectInitializer(member, source, constObjects, staticObjectResolver);
|
|
149
|
+
if (resolvedObject === undefined) {
|
|
147
150
|
return {
|
|
148
151
|
refusal: refusal(fileName, site.operationKey, "non_literal", `${containerName} must be an object literal or a module-level const object literal.`),
|
|
149
152
|
};
|
|
150
153
|
}
|
|
151
|
-
const members = expandMembers(object, constObjects, fileName, site.operationKey);
|
|
154
|
+
const members = expandMembers(resolvedObject.object, resolvedObject.source, constObjects, fileName, site.operationKey, staticObjectResolver);
|
|
152
155
|
if ("refusal" in members)
|
|
153
156
|
return members;
|
|
154
157
|
const indexed = indexMembers(members.members, fileName, site.operationKey);
|
|
@@ -196,24 +199,24 @@ function planOperationMigration(source, fileName, site, constObjects, constArray
|
|
|
196
199
|
const sourceContainer = risk.source === "annotations" ? "annotations" : "toolRouter";
|
|
197
200
|
addInsertion(insertions, sourceContainer, `riskClass: ${risk.text}`);
|
|
198
201
|
}
|
|
199
|
-
const timeout = mergeFlatAndNested("timeoutMs", top.byName.get("timeoutMs"), annotations.get("timeoutMs"), fileName, site.operationKey,
|
|
202
|
+
const timeout = mergeFlatAndNested("timeoutMs", top.byName.get("timeoutMs"), annotations.get("timeoutMs"), fileName, site.operationKey, "execution_conflict");
|
|
200
203
|
if ("refusal" in timeout)
|
|
201
204
|
return timeout;
|
|
202
205
|
if (timeout.insert !== undefined)
|
|
203
206
|
addInsertion(insertions, "annotations", timeout.insert);
|
|
204
|
-
const connection = resolveConnectionMode(top.byName, toolRouter, fileName, site.operationKey
|
|
207
|
+
const connection = resolveConnectionMode(top.byName, toolRouter, fileName, site.operationKey);
|
|
205
208
|
if ("refusal" in connection)
|
|
206
209
|
return connection;
|
|
207
210
|
if (connection.insert !== undefined) {
|
|
208
211
|
addInsertion(insertions, "toolRouter", connection.insert);
|
|
209
212
|
}
|
|
210
|
-
const externalRef = mergeFlatAndNested("connectionExternalRefParam", top.byName.get("connectionExternalRefParam"), toolRouter.get("connectionExternalRefParam"), fileName, site.operationKey,
|
|
213
|
+
const externalRef = mergeFlatAndNested("connectionExternalRefParam", top.byName.get("connectionExternalRefParam"), toolRouter.get("connectionExternalRefParam"), fileName, site.operationKey, "connection_mode_conflict");
|
|
211
214
|
if ("refusal" in externalRef)
|
|
212
215
|
return externalRef;
|
|
213
216
|
if (externalRef.insert !== undefined) {
|
|
214
217
|
addInsertion(insertions, "toolRouter", externalRef.insert);
|
|
215
218
|
}
|
|
216
|
-
const approval = resolveApproval(top.byName.get("approval"), toolRouter.get("approval"), risk.value, fileName, site.operationKey
|
|
219
|
+
const approval = resolveApproval(top.byName.get("approval"), toolRouter.get("approval"), risk.value, fileName, site.operationKey);
|
|
217
220
|
if ("refusal" in approval)
|
|
218
221
|
return approval;
|
|
219
222
|
if (approval.removeTop !== undefined)
|
|
@@ -221,7 +224,7 @@ function planOperationMigration(source, fileName, site, constObjects, constArray
|
|
|
221
224
|
if (approval.insert !== undefined)
|
|
222
225
|
addInsertion(insertions, "toolRouter", approval.insert);
|
|
223
226
|
for (const field of DOCS_FIELDS) {
|
|
224
|
-
const merged = mergeFlatAndNested(field, top.byName.get(field), docs.get(field), fileName, site.operationKey,
|
|
227
|
+
const merged = mergeFlatAndNested(field, top.byName.get(field), docs.get(field), fileName, site.operationKey, "locale_key_conflict");
|
|
225
228
|
if ("refusal" in merged)
|
|
226
229
|
return merged;
|
|
227
230
|
if (merged.insert !== undefined)
|
|
@@ -380,7 +383,7 @@ function resolveRiskClass(top, annotations, toolRouter, fileName, operationKey)
|
|
|
380
383
|
refusal: refusal(fileName, operationKey, "no_safety", "No authored riskClass, annotations.destructive: true, or annotations.readOnly: true declaration exists; refusing to invent write."),
|
|
381
384
|
};
|
|
382
385
|
}
|
|
383
|
-
function resolveConnectionMode(top, toolRouter, fileName, operationKey
|
|
386
|
+
function resolveConnectionMode(top, toolRouter, fileName, operationKey) {
|
|
384
387
|
const flat = top.get("connectionMode");
|
|
385
388
|
const nested = toolRouter.get("connectionMode");
|
|
386
389
|
if (flat !== undefined && nested !== undefined) {
|
|
@@ -398,7 +401,7 @@ function resolveConnectionMode(top, toolRouter, fileName, operationKey, source)
|
|
|
398
401
|
if (flat !== undefined)
|
|
399
402
|
return {};
|
|
400
403
|
if (nested !== undefined) {
|
|
401
|
-
return { insert: memberText(nested, "connectionMode"
|
|
404
|
+
return { insert: memberText(nested, "connectionMode") };
|
|
402
405
|
}
|
|
403
406
|
const required = toolRouter.get("requiresConnection");
|
|
404
407
|
if (required === undefined)
|
|
@@ -408,7 +411,7 @@ function resolveConnectionMode(top, toolRouter, fileName, operationKey, source)
|
|
|
408
411
|
return value;
|
|
409
412
|
return { insert: `connectionMode: ${value.value ? '"required"' : '"none"'}` };
|
|
410
413
|
}
|
|
411
|
-
function resolveApproval(flat, nested, riskClass, fileName, operationKey
|
|
414
|
+
function resolveApproval(flat, nested, riskClass, fileName, operationKey) {
|
|
412
415
|
let selected = flat ?? nested;
|
|
413
416
|
if (flat !== undefined && nested !== undefined) {
|
|
414
417
|
const same = equivalentLiteral(flat.initializer, nested.initializer);
|
|
@@ -434,7 +437,7 @@ function resolveApproval(flat, nested, riskClass, fileName, operationKey, source
|
|
|
434
437
|
}
|
|
435
438
|
if (flat !== undefined)
|
|
436
439
|
return {};
|
|
437
|
-
return { insert: memberText(selected, "approval"
|
|
440
|
+
return { insert: memberText(selected, "approval") };
|
|
438
441
|
}
|
|
439
442
|
function defaultApprovalPolicy(riskClass) {
|
|
440
443
|
if (riskClass === "read")
|
|
@@ -443,16 +446,22 @@ function defaultApprovalPolicy(riskClass) {
|
|
|
443
446
|
return "risk-based";
|
|
444
447
|
return "always";
|
|
445
448
|
}
|
|
446
|
-
function mergeFlatAndNested(field, flat, nested, fileName, operationKey,
|
|
449
|
+
function mergeFlatAndNested(field, flat, nested, fileName, operationKey, reason) {
|
|
447
450
|
if (nested === undefined)
|
|
448
451
|
return {};
|
|
449
452
|
if (flat === undefined)
|
|
450
|
-
return { insert: memberText(nested, field
|
|
453
|
+
return { insert: memberText(nested, field) };
|
|
451
454
|
const same = equivalentLiteral(flat.initializer, nested.initializer);
|
|
452
455
|
if (same === undefined) {
|
|
453
456
|
return nonLiteral(fileName, operationKey, `${field} must be literal when both top-level and nested declarations exist.`);
|
|
454
457
|
}
|
|
455
458
|
if (!same) {
|
|
459
|
+
// A shared imported docs template is intentionally lower-precedence than
|
|
460
|
+
// an operation's explicit flat locale key. Keep direct-vs-direct
|
|
461
|
+
// conflicts fail-closed, but do not let a spread default replace the
|
|
462
|
+
// operation-specific key during flattening.
|
|
463
|
+
if (reason === "locale_key_conflict" && nested.fromSpread === true)
|
|
464
|
+
return {};
|
|
456
465
|
return {
|
|
457
466
|
refusal: refusal(fileName, operationKey, reason, `Top-level ${field} conflicts with nested ${field}.`),
|
|
458
467
|
};
|
|
@@ -550,7 +559,9 @@ function planExamples(inputExamples, existingExamples, fileName, site, source, c
|
|
|
550
559
|
}
|
|
551
560
|
function resolveOperationLocaleNamespace(members, fileName, site) {
|
|
552
561
|
const authoredNamespaces = new Set();
|
|
553
|
-
|
|
562
|
+
const directMembers = members.filter((member) => member !== undefined && member.fromSpread !== true);
|
|
563
|
+
const namespaceMembers = directMembers.length > 0 ? directMembers : members;
|
|
564
|
+
for (const member of namespaceMembers) {
|
|
554
565
|
const localeKey = literalString(member?.initializer);
|
|
555
566
|
if (localeKey === undefined)
|
|
556
567
|
continue;
|
|
@@ -567,6 +578,11 @@ function resolveOperationLocaleNamespace(members, fileName, site) {
|
|
|
567
578
|
const authored = authoredNamespaces.values().next().value;
|
|
568
579
|
if (authored !== undefined)
|
|
569
580
|
return { namespace: authored };
|
|
581
|
+
// planExamples proves the id before using this placeholder to construct a
|
|
582
|
+
// locale key. Avoid validating an unproven binding such as <anonymous> as
|
|
583
|
+
// though it were an authored operation id.
|
|
584
|
+
if (!site.operationIdProven)
|
|
585
|
+
return { namespace: site.operationKey };
|
|
570
586
|
try {
|
|
571
587
|
return { namespace: operationIdToLocaleNamespace(site.operationKey) };
|
|
572
588
|
}
|
|
@@ -583,10 +599,13 @@ function replaceExampleLocaleMember(member, newName, localeKey, source) {
|
|
|
583
599
|
text: `${newName}: ${JSON.stringify(localeKey)}`,
|
|
584
600
|
};
|
|
585
601
|
}
|
|
586
|
-
function discoverOperationSites(source, fileName, constObjects, operationIds) {
|
|
602
|
+
function discoverOperationSites(source, fileName, constObjects, operationIds, operationSites, excludedBindings) {
|
|
587
603
|
const sitesByStart = new Map();
|
|
588
604
|
const localIds = new Map(operationIds ?? []);
|
|
605
|
+
const localExcludedBindings = new Set(excludedBindings ?? []);
|
|
589
606
|
const refusals = [];
|
|
607
|
+
const operationFactories = collectSimpleOperationFactories(source);
|
|
608
|
+
const factoryCallIds = new Map();
|
|
590
609
|
for (const map of collectOperationsMaps(source, constObjects, fileName, refusals)) {
|
|
591
610
|
for (const property of map.properties) {
|
|
592
611
|
if (ts.isSpreadAssignment(property))
|
|
@@ -615,6 +634,16 @@ function discoverOperationSites(source, fileName, constObjects, operationIds) {
|
|
|
615
634
|
});
|
|
616
635
|
continue;
|
|
617
636
|
}
|
|
637
|
+
if (ts.isCallExpression(unwrapped) &&
|
|
638
|
+
ts.isIdentifier(unwrapped.expression) &&
|
|
639
|
+
operationFactories.has(unwrapped.expression.text) &&
|
|
640
|
+
!localExcludedBindings.has(unwrapped.expression.text)) {
|
|
641
|
+
const factoryName = unwrapped.expression.text;
|
|
642
|
+
const operationIdsForFactory = factoryCallIds.get(factoryName) ?? new Set();
|
|
643
|
+
operationIdsForFactory.add(key);
|
|
644
|
+
factoryCallIds.set(factoryName, operationIdsForFactory);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
618
647
|
if (ts.isObjectLiteralExpression(unwrapped)) {
|
|
619
648
|
sitesByStart.set(unwrapped.getStart(source), {
|
|
620
649
|
object: unwrapped,
|
|
@@ -624,10 +653,35 @@ function discoverOperationSites(source, fileName, constObjects, operationIds) {
|
|
|
624
653
|
}
|
|
625
654
|
}
|
|
626
655
|
}
|
|
656
|
+
for (const [factoryName, operationIdsForFactory] of factoryCallIds) {
|
|
657
|
+
const ids = [...operationIdsForFactory];
|
|
658
|
+
if (ids.length === 1) {
|
|
659
|
+
const operationId = ids[0];
|
|
660
|
+
if (operationId !== undefined)
|
|
661
|
+
localIds.set(factoryName, operationId);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
localExcludedBindings.add(factoryName);
|
|
665
|
+
refusals.push(refusal(fileName, factoryName, "factory_operation_id_ambiguous", `Factory ${factoryName} is registered under multiple operation ids: ${ids
|
|
666
|
+
.map((id) => JSON.stringify(id))
|
|
667
|
+
.join(", ")}. A shared operation body cannot own one examples locale namespace.`));
|
|
668
|
+
}
|
|
627
669
|
const visit = (node) => {
|
|
670
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
671
|
+
const indexedOperationKey = operationSites?.get(node.getStart(source));
|
|
672
|
+
if (indexedOperationKey !== undefined) {
|
|
673
|
+
sitesByStart.set(node.getStart(source), {
|
|
674
|
+
object: node,
|
|
675
|
+
operationKey: indexedOperationKey,
|
|
676
|
+
operationIdProven: true,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
628
680
|
if (ts.isCallExpression(node) && isOperationHelperCall(node)) {
|
|
629
681
|
const argument = operationArgument(node);
|
|
630
682
|
const bindingName = enclosingBindingName(node);
|
|
683
|
+
if (bindingName !== undefined && localExcludedBindings.has(bindingName))
|
|
684
|
+
return;
|
|
631
685
|
const operationKey = (bindingName === undefined ? undefined : localIds.get(bindingName)) ??
|
|
632
686
|
bindingName ??
|
|
633
687
|
"<anonymous>";
|
|
@@ -662,6 +716,31 @@ function discoverOperationSites(source, fileName, constObjects, operationIds) {
|
|
|
662
716
|
refusals,
|
|
663
717
|
};
|
|
664
718
|
}
|
|
719
|
+
function collectSimpleOperationFactories(source) {
|
|
720
|
+
const factories = new Set();
|
|
721
|
+
for (const statement of source.statements) {
|
|
722
|
+
if (!ts.isFunctionDeclaration(statement) ||
|
|
723
|
+
statement.name === undefined ||
|
|
724
|
+
statement.body === undefined ||
|
|
725
|
+
statement.body.statements.length !== 1) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const returned = statement.body.statements[0];
|
|
729
|
+
if (!ts.isReturnStatement(returned) || returned.expression === undefined)
|
|
730
|
+
continue;
|
|
731
|
+
const expression = unwrapExpression(returned.expression);
|
|
732
|
+
if (expression !== undefined &&
|
|
733
|
+
ts.isCallExpression(expression) &&
|
|
734
|
+
isOperationHelperCall(expression)) {
|
|
735
|
+
const argument = operationArgument(expression);
|
|
736
|
+
const object = argument === undefined ? undefined : unwrapExpression(argument);
|
|
737
|
+
if (object !== undefined && ts.isObjectLiteralExpression(object)) {
|
|
738
|
+
factories.add(statement.name.text);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return factories;
|
|
743
|
+
}
|
|
665
744
|
function collectOperationsMaps(source, constObjects, fileName, refusals) {
|
|
666
745
|
const maps = new Map();
|
|
667
746
|
const inspect = (expression, label) => {
|
|
@@ -784,6 +863,9 @@ function enclosingBindingName(node) {
|
|
|
784
863
|
if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name)) {
|
|
785
864
|
return current.name.text;
|
|
786
865
|
}
|
|
866
|
+
if (ts.isFunctionDeclaration(current) && current.name !== undefined) {
|
|
867
|
+
return current.name.text;
|
|
868
|
+
}
|
|
787
869
|
if (ts.isPropertyAssignment(current))
|
|
788
870
|
return staticPropertyName(current.name);
|
|
789
871
|
if (ts.isExportAssignment(current))
|
|
@@ -855,7 +937,7 @@ function expandArrayElements(array, constArrays, fileName, operationKey, seen =
|
|
|
855
937
|
}
|
|
856
938
|
return { elements };
|
|
857
939
|
}
|
|
858
|
-
function expandMembers(object, constObjects, fileName, operationKey, seen = new Set()) {
|
|
940
|
+
function expandMembers(object, source, constObjects, fileName, operationKey, staticObjectResolver, seen = new Set()) {
|
|
859
941
|
if (seen.has(object)) {
|
|
860
942
|
return nonLiteral(fileName, operationKey, "A module-level object spread is recursive.");
|
|
861
943
|
}
|
|
@@ -863,17 +945,12 @@ function expandMembers(object, constObjects, fileName, operationKey, seen = new
|
|
|
863
945
|
const members = [];
|
|
864
946
|
for (const property of object.properties) {
|
|
865
947
|
if (ts.isSpreadAssignment(property)) {
|
|
866
|
-
const
|
|
867
|
-
const spreadObject = expression !== undefined && ts.isObjectLiteralExpression(expression)
|
|
868
|
-
? expression
|
|
869
|
-
: expression !== undefined && ts.isIdentifier(expression)
|
|
870
|
-
? constObjects.get(expression.text)
|
|
871
|
-
: undefined;
|
|
948
|
+
const spreadObject = resolveStaticObjectExpression(property.expression, source, constObjects, staticObjectResolver);
|
|
872
949
|
if (spreadObject === undefined) {
|
|
873
|
-
members.push({ name: "<spread>", property });
|
|
950
|
+
members.push({ name: "<spread>", property, source });
|
|
874
951
|
continue;
|
|
875
952
|
}
|
|
876
|
-
const expanded = expandMembers(spreadObject, constObjects, fileName, operationKey, new Set(seen));
|
|
953
|
+
const expanded = expandMembers(spreadObject.object, spreadObject.source, constObjects, fileName, operationKey, staticObjectResolver, new Set(seen));
|
|
877
954
|
if ("refusal" in expanded)
|
|
878
955
|
return expanded;
|
|
879
956
|
members.push(...expanded.members.map((member) => ({ ...member, fromSpread: true })));
|
|
@@ -884,7 +961,7 @@ function expandMembers(object, constObjects, fileName, operationKey, seen = new
|
|
|
884
961
|
return nonLiteral(fileName, operationKey, "A declaration member uses a computed name.");
|
|
885
962
|
}
|
|
886
963
|
const initializer = propertyValue(property);
|
|
887
|
-
members.push({ name, property, initializer });
|
|
964
|
+
members.push({ name, property, initializer, source });
|
|
888
965
|
}
|
|
889
966
|
return { members };
|
|
890
967
|
}
|
|
@@ -915,21 +992,32 @@ function indexMembersFromObject(object, fileName, operationKey) {
|
|
|
915
992
|
if (name === undefined || initializer === undefined) {
|
|
916
993
|
return nonLiteral(fileName, operationKey, "Example members must be literal properties.");
|
|
917
994
|
}
|
|
918
|
-
members.push({ name, property, initializer });
|
|
995
|
+
members.push({ name, property, initializer, source: object.getSourceFile() });
|
|
919
996
|
}
|
|
920
997
|
const indexed = indexMembers(members, fileName, operationKey);
|
|
921
998
|
if ("refusal" in indexed)
|
|
922
999
|
return indexed;
|
|
923
1000
|
return { byName: indexed.byName };
|
|
924
1001
|
}
|
|
925
|
-
function resolveObjectInitializer(member, constObjects) {
|
|
926
|
-
|
|
927
|
-
if (initializer === undefined)
|
|
1002
|
+
function resolveObjectInitializer(member, rootSource, constObjects, staticObjectResolver) {
|
|
1003
|
+
if (member.initializer === undefined)
|
|
928
1004
|
return undefined;
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1005
|
+
return resolveStaticObjectExpression(member.initializer, member.source, constObjects, staticObjectResolver, rootSource);
|
|
1006
|
+
}
|
|
1007
|
+
function resolveStaticObjectExpression(expression, source, constObjects, staticObjectResolver, rootSource = source) {
|
|
1008
|
+
const unwrapped = unwrapExpression(expression);
|
|
1009
|
+
if (unwrapped === undefined)
|
|
1010
|
+
return undefined;
|
|
1011
|
+
if (ts.isObjectLiteralExpression(unwrapped))
|
|
1012
|
+
return { object: unwrapped, source };
|
|
1013
|
+
const externallyResolved = staticObjectResolver?.(unwrapped, source);
|
|
1014
|
+
if (externallyResolved !== undefined)
|
|
1015
|
+
return externallyResolved;
|
|
1016
|
+
if (source === rootSource && ts.isIdentifier(unwrapped)) {
|
|
1017
|
+
const object = constObjects.get(unwrapped.text);
|
|
1018
|
+
if (object !== undefined)
|
|
1019
|
+
return { object, source };
|
|
1020
|
+
}
|
|
933
1021
|
return undefined;
|
|
934
1022
|
}
|
|
935
1023
|
function memberMap(members) {
|
|
@@ -1020,10 +1108,10 @@ function literalValue(expression) {
|
|
|
1020
1108
|
}
|
|
1021
1109
|
return NOT_LITERAL;
|
|
1022
1110
|
}
|
|
1023
|
-
function memberText(member, name
|
|
1111
|
+
function memberText(member, name) {
|
|
1024
1112
|
if (member.initializer === undefined)
|
|
1025
1113
|
return name;
|
|
1026
|
-
return `${name}: ${member.initializer.getText(source)}`;
|
|
1114
|
+
return `${name}: ${member.initializer.getText(member.source)}`;
|
|
1027
1115
|
}
|
|
1028
1116
|
function addInsertion(insertions, sourceContainer, text) {
|
|
1029
1117
|
const items = insertions.get(sourceContainer) ?? [];
|
|
@@ -1180,17 +1268,21 @@ export function migrateOperationDeclarationRepository(providerRootInput, options
|
|
|
1180
1268
|
const providerRoot = resolve(providerRootInput);
|
|
1181
1269
|
const sourceFiles = collectSourceFiles(providerRoot);
|
|
1182
1270
|
const localeFiles = collectLocaleFiles(providerRoot);
|
|
1183
|
-
const
|
|
1271
|
+
const repositoryIndex = buildRepositoryOperationIndex(sourceFiles, providerRoot);
|
|
1184
1272
|
const pendingWrites = new Map();
|
|
1185
1273
|
const changedFiles = [];
|
|
1186
1274
|
const todos = [];
|
|
1187
|
-
const refusals = [];
|
|
1275
|
+
const refusals = [...repositoryIndex.refusals];
|
|
1188
1276
|
let operationCount = 0;
|
|
1189
1277
|
for (const sourcePath of sourceFiles) {
|
|
1190
1278
|
const relativePath = slash(relative(providerRoot, sourcePath));
|
|
1191
|
-
const result =
|
|
1192
|
-
operationIds: operationIds.get(sourcePath),
|
|
1279
|
+
const result = migrateOperationDeclarationInternal(readFileSync(sourcePath, "utf8"), relativePath, {
|
|
1280
|
+
operationIds: repositoryIndex.operationIds.get(sourcePath),
|
|
1193
1281
|
localeFiles,
|
|
1282
|
+
}, {
|
|
1283
|
+
operationSites: repositoryIndex.operationSites.get(sourcePath),
|
|
1284
|
+
excludedBindings: repositoryIndex.excludedBindings.get(sourcePath),
|
|
1285
|
+
staticObjectResolver: repositoryIndex.staticObjectResolverFor(sourcePath),
|
|
1194
1286
|
});
|
|
1195
1287
|
if (result.status === "refused") {
|
|
1196
1288
|
refusals.push(...result.refusals);
|
|
@@ -1203,11 +1295,23 @@ export function migrateOperationDeclarationRepository(providerRootInput, options
|
|
|
1203
1295
|
todos.push(...result.localeTodos);
|
|
1204
1296
|
}
|
|
1205
1297
|
}
|
|
1298
|
+
if (repositoryIndex.declarations.length > 0 && repositoryIndex.discoveredCount === 0) {
|
|
1299
|
+
for (const declaration of repositoryIndex.declarations) {
|
|
1300
|
+
refusals.push(refusal(slash(relative(providerRoot, declaration.path)), "<operations>", "no_operations_discovered", `Provider construct ${declaration.construct} declares operations via unresolved initializer ${JSON.stringify(declaration.initializer.getText(declaration.initializer.getSourceFile()))}, but repository-wide discovery found zero operation sites.`));
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1206
1303
|
const todoConflict = findLocaleTodoConflict(todos);
|
|
1207
1304
|
if (todoConflict !== undefined)
|
|
1208
1305
|
refusals.push(todoConflict);
|
|
1209
1306
|
if (refusals.length > 0) {
|
|
1210
|
-
return {
|
|
1307
|
+
return {
|
|
1308
|
+
status: "refused",
|
|
1309
|
+
providerRoot,
|
|
1310
|
+
refusals,
|
|
1311
|
+
operationCount,
|
|
1312
|
+
changedFiles,
|
|
1313
|
+
localeTodoCount: todos.length,
|
|
1314
|
+
};
|
|
1211
1315
|
}
|
|
1212
1316
|
for (const [path, code] of renderLocaleCatalogWrites(providerRoot, todos)) {
|
|
1213
1317
|
pendingWrites.set(path, code);
|
|
@@ -1375,6 +1479,496 @@ function collectLocaleFiles(root) {
|
|
|
1375
1479
|
.map((entry) => `locales/${entry.name}`)
|
|
1376
1480
|
.sort();
|
|
1377
1481
|
}
|
|
1482
|
+
function buildRepositoryOperationIndex(sourceFiles, providerRoot) {
|
|
1483
|
+
const sources = new Map();
|
|
1484
|
+
for (const path of sourceFiles) {
|
|
1485
|
+
const source = parseSource(path, readFileSync(path, "utf8"));
|
|
1486
|
+
if (firstSyntaxError(source) === undefined)
|
|
1487
|
+
sources.set(path, source);
|
|
1488
|
+
}
|
|
1489
|
+
const operationIds = buildOperationIdIndex(sourceFiles);
|
|
1490
|
+
const operationSites = new Map();
|
|
1491
|
+
const excludedBindings = new Map();
|
|
1492
|
+
const refusals = [];
|
|
1493
|
+
const declarations = [];
|
|
1494
|
+
const indexedProviderProperties = new Set();
|
|
1495
|
+
const factoryIds = new Map();
|
|
1496
|
+
const bindingCandidates = new Map();
|
|
1497
|
+
let discoveredCount = 0;
|
|
1498
|
+
const relativePath = (path) => slash(relative(providerRoot, path));
|
|
1499
|
+
const resolutionRefusal = (path, operationKey, detail) => {
|
|
1500
|
+
refusals.push(refusal(relativePath(path), operationKey, "non_literal", detail));
|
|
1501
|
+
};
|
|
1502
|
+
const resolveExport = (path, exportedName, active) => {
|
|
1503
|
+
const key = `${path}\0export\0${exportedName}`;
|
|
1504
|
+
if (active.has(key)) {
|
|
1505
|
+
return {
|
|
1506
|
+
status: "refused",
|
|
1507
|
+
detail: `Static relative export cycle while resolving ${JSON.stringify(exportedName)} from ${relativePath(path)}.`,
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
const source = sources.get(path);
|
|
1511
|
+
if (source === undefined)
|
|
1512
|
+
return { status: "missing" };
|
|
1513
|
+
const nextActive = new Set(active);
|
|
1514
|
+
nextActive.add(key);
|
|
1515
|
+
const explicit = [];
|
|
1516
|
+
const exportStars = [];
|
|
1517
|
+
for (const statement of source.statements) {
|
|
1518
|
+
if (ts.isVariableStatement(statement) && hasExportModifier(statement)) {
|
|
1519
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1520
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
1521
|
+
declaration.name.text === exportedName &&
|
|
1522
|
+
declaration.initializer !== undefined) {
|
|
1523
|
+
const initializer = declaration.initializer;
|
|
1524
|
+
const bindingName = declaration.name.text;
|
|
1525
|
+
explicit.push(() => ({
|
|
1526
|
+
status: "resolved",
|
|
1527
|
+
value: {
|
|
1528
|
+
path,
|
|
1529
|
+
source,
|
|
1530
|
+
expression: initializer,
|
|
1531
|
+
bindingName,
|
|
1532
|
+
},
|
|
1533
|
+
}));
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
if (ts.isExportAssignment(statement) &&
|
|
1538
|
+
!statement.isExportEquals &&
|
|
1539
|
+
exportedName === "default") {
|
|
1540
|
+
explicit.push(() => ({
|
|
1541
|
+
status: "resolved",
|
|
1542
|
+
value: { path, source, expression: statement.expression },
|
|
1543
|
+
}));
|
|
1544
|
+
}
|
|
1545
|
+
if (!ts.isExportDeclaration(statement))
|
|
1546
|
+
continue;
|
|
1547
|
+
const specifier = statement.moduleSpecifier !== undefined && ts.isStringLiteral(statement.moduleSpecifier)
|
|
1548
|
+
? statement.moduleSpecifier.text
|
|
1549
|
+
: undefined;
|
|
1550
|
+
if (statement.exportClause === undefined) {
|
|
1551
|
+
if (specifier !== undefined)
|
|
1552
|
+
exportStars.push(specifier);
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
if (!ts.isNamedExports(statement.exportClause))
|
|
1556
|
+
continue;
|
|
1557
|
+
for (const element of statement.exportClause.elements) {
|
|
1558
|
+
if (element.name.text !== exportedName)
|
|
1559
|
+
continue;
|
|
1560
|
+
const localName = element.propertyName?.text ?? element.name.text;
|
|
1561
|
+
explicit.push(() => {
|
|
1562
|
+
if (specifier === undefined) {
|
|
1563
|
+
return resolveIdentifier(path, localName, nextActive);
|
|
1564
|
+
}
|
|
1565
|
+
const target = resolveStaticModule(path, specifier, sources);
|
|
1566
|
+
if (target.status !== "resolved")
|
|
1567
|
+
return target;
|
|
1568
|
+
return resolveExport(target.path, localName, nextActive);
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
if (explicit.length > 1) {
|
|
1573
|
+
return {
|
|
1574
|
+
status: "refused",
|
|
1575
|
+
detail: `Export ${JSON.stringify(exportedName)} is ambiguous in ${relativePath(path)}.`,
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
if (explicit.length === 1)
|
|
1579
|
+
return explicit[0]?.() ?? { status: "missing" };
|
|
1580
|
+
const resolvedStars = [];
|
|
1581
|
+
let firstStarRefusal;
|
|
1582
|
+
for (const specifier of exportStars) {
|
|
1583
|
+
const target = resolveStaticModule(path, specifier, sources);
|
|
1584
|
+
if (target.status !== "resolved") {
|
|
1585
|
+
if (target.status === "refused")
|
|
1586
|
+
firstStarRefusal ??= target;
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
const candidate = resolveExport(target.path, exportedName, nextActive);
|
|
1590
|
+
if (candidate.status === "resolved")
|
|
1591
|
+
resolvedStars.push(candidate.value);
|
|
1592
|
+
else if (candidate.status === "refused")
|
|
1593
|
+
firstStarRefusal ??= candidate;
|
|
1594
|
+
}
|
|
1595
|
+
const origins = new Set(resolvedStars.map((item) => `${item.path}\0${item.expression.getStart(item.source)}`));
|
|
1596
|
+
if (origins.size > 1) {
|
|
1597
|
+
return {
|
|
1598
|
+
status: "refused",
|
|
1599
|
+
detail: `Export ${JSON.stringify(exportedName)} is ambiguous across relative re-exports from ${relativePath(path)}.`,
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
if (firstStarRefusal !== undefined)
|
|
1603
|
+
return firstStarRefusal;
|
|
1604
|
+
const resolved = resolvedStars[0];
|
|
1605
|
+
return resolved === undefined ? { status: "missing" } : { status: "resolved", value: resolved };
|
|
1606
|
+
};
|
|
1607
|
+
const resolveIdentifier = (path, name, active) => {
|
|
1608
|
+
const source = sources.get(path);
|
|
1609
|
+
if (source === undefined)
|
|
1610
|
+
return { status: "missing" };
|
|
1611
|
+
for (const statement of source.statements) {
|
|
1612
|
+
if (!ts.isVariableStatement(statement))
|
|
1613
|
+
continue;
|
|
1614
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
|
|
1615
|
+
continue;
|
|
1616
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1617
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
1618
|
+
declaration.name.text === name &&
|
|
1619
|
+
declaration.initializer !== undefined) {
|
|
1620
|
+
return {
|
|
1621
|
+
status: "resolved",
|
|
1622
|
+
value: {
|
|
1623
|
+
path,
|
|
1624
|
+
source,
|
|
1625
|
+
expression: declaration.initializer,
|
|
1626
|
+
bindingName: name,
|
|
1627
|
+
},
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
for (const statement of source.statements) {
|
|
1633
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
let exportedName;
|
|
1637
|
+
if (statement.importClause?.name?.text === name)
|
|
1638
|
+
exportedName = "default";
|
|
1639
|
+
const bindings = statement.importClause?.namedBindings;
|
|
1640
|
+
if (bindings !== undefined && ts.isNamedImports(bindings)) {
|
|
1641
|
+
for (const element of bindings.elements) {
|
|
1642
|
+
if (element.name.text === name) {
|
|
1643
|
+
exportedName = element.propertyName?.text ?? element.name.text;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
if (exportedName === undefined)
|
|
1648
|
+
continue;
|
|
1649
|
+
const specifier = statement.moduleSpecifier.text;
|
|
1650
|
+
const target = resolveStaticModule(path, specifier, sources);
|
|
1651
|
+
if (target.status !== "resolved")
|
|
1652
|
+
return target;
|
|
1653
|
+
return resolveExport(target.path, exportedName, active);
|
|
1654
|
+
}
|
|
1655
|
+
return { status: "missing" };
|
|
1656
|
+
};
|
|
1657
|
+
const resolveLocatedExpression = (located, active) => {
|
|
1658
|
+
const expression = unwrapExpression(located.expression);
|
|
1659
|
+
if (expression === undefined)
|
|
1660
|
+
return { status: "missing" };
|
|
1661
|
+
if (!ts.isIdentifier(expression)) {
|
|
1662
|
+
return { status: "resolved", value: { ...located, expression } };
|
|
1663
|
+
}
|
|
1664
|
+
const key = `${located.path}\0identifier\0${expression.text}`;
|
|
1665
|
+
if (active.has(key)) {
|
|
1666
|
+
return {
|
|
1667
|
+
status: "refused",
|
|
1668
|
+
detail: `Static identifier cycle while resolving ${JSON.stringify(expression.text)} in ${relativePath(located.path)}.`,
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
const nextActive = new Set(active);
|
|
1672
|
+
nextActive.add(key);
|
|
1673
|
+
const resolved = resolveIdentifier(located.path, expression.text, nextActive);
|
|
1674
|
+
return resolved.status === "resolved"
|
|
1675
|
+
? resolveLocatedExpression(resolved.value, nextActive)
|
|
1676
|
+
: resolved;
|
|
1677
|
+
};
|
|
1678
|
+
const flattenRegistry = (locatedInput, active) => {
|
|
1679
|
+
const resolved = resolveLocatedExpression(locatedInput, active);
|
|
1680
|
+
if (resolved.status === "refused")
|
|
1681
|
+
return { detail: resolved.detail };
|
|
1682
|
+
if (resolved.status === "missing") {
|
|
1683
|
+
return {
|
|
1684
|
+
detail: `Operations initializer ${JSON.stringify(locatedInput.expression.getText(locatedInput.source))} is not a static local or relative-imported object.`,
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
const expression = unwrapExpression(resolved.value.expression);
|
|
1688
|
+
if (expression === undefined || !ts.isObjectLiteralExpression(expression)) {
|
|
1689
|
+
return {
|
|
1690
|
+
detail: `Operations initializer ${JSON.stringify(resolved.value.expression.getText(resolved.value.source))} is not a static object literal.`,
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
const cycleKey = `${resolved.value.path}\0object\0${expression.getStart(resolved.value.source)}`;
|
|
1694
|
+
if (active.has(cycleKey)) {
|
|
1695
|
+
return {
|
|
1696
|
+
detail: `Static operations registry spread cycle reaches ${relativePath(resolved.value.path)}.`,
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
const nextActive = new Set(active);
|
|
1700
|
+
nextActive.add(cycleKey);
|
|
1701
|
+
const entries = new Map();
|
|
1702
|
+
for (const property of expression.properties) {
|
|
1703
|
+
if (ts.isSpreadAssignment(property)) {
|
|
1704
|
+
const spread = flattenRegistry({
|
|
1705
|
+
path: resolved.value.path,
|
|
1706
|
+
source: resolved.value.source,
|
|
1707
|
+
expression: property.expression,
|
|
1708
|
+
}, nextActive);
|
|
1709
|
+
if ("detail" in spread)
|
|
1710
|
+
return spread;
|
|
1711
|
+
for (const [operationId, entry] of spread.entries)
|
|
1712
|
+
entries.set(operationId, entry);
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
const operationId = staticPropertyName(property.name);
|
|
1716
|
+
const value = propertyValue(property);
|
|
1717
|
+
if (operationId === undefined) {
|
|
1718
|
+
return {
|
|
1719
|
+
detail: `Operations registry in ${relativePath(resolved.value.path)} uses a computed key.`,
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
if (value === undefined) {
|
|
1723
|
+
return { detail: `Operation ${JSON.stringify(operationId)} has a non-static initializer.` };
|
|
1724
|
+
}
|
|
1725
|
+
entries.set(operationId, {
|
|
1726
|
+
operationId,
|
|
1727
|
+
value: {
|
|
1728
|
+
path: resolved.value.path,
|
|
1729
|
+
source: resolved.value.source,
|
|
1730
|
+
expression: value,
|
|
1731
|
+
},
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
return { entries };
|
|
1735
|
+
};
|
|
1736
|
+
const addBindingCandidate = (path, name, operationId) => {
|
|
1737
|
+
const key = `${path}\0${name}`;
|
|
1738
|
+
const candidate = bindingCandidates.get(key) ?? { path, name, ids: new Set() };
|
|
1739
|
+
candidate.ids.add(operationId);
|
|
1740
|
+
bindingCandidates.set(key, candidate);
|
|
1741
|
+
};
|
|
1742
|
+
const addObjectSite = (path, source, object, operationId) => {
|
|
1743
|
+
const sites = operationSites.get(path) ?? new Map();
|
|
1744
|
+
const start = object.getStart(source);
|
|
1745
|
+
const previous = sites.get(start);
|
|
1746
|
+
if (previous !== undefined && previous !== operationId) {
|
|
1747
|
+
resolutionRefusal(path, operationId, `One static operation object is registered under both ${JSON.stringify(previous)} and ${JSON.stringify(operationId)}.`);
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
sites.set(start, operationId);
|
|
1751
|
+
operationSites.set(path, sites);
|
|
1752
|
+
discoveredCount += 1;
|
|
1753
|
+
};
|
|
1754
|
+
const classifyEntry = (entry) => {
|
|
1755
|
+
const resolved = resolveLocatedExpression(entry.value, new Set());
|
|
1756
|
+
if (resolved.status === "refused") {
|
|
1757
|
+
resolutionRefusal(entry.value.path, entry.operationId, resolved.detail);
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
if (resolved.status === "missing") {
|
|
1761
|
+
resolutionRefusal(entry.value.path, entry.operationId, `Operation initializer ${JSON.stringify(entry.value.expression.getText(entry.value.source))} is not statically resolvable.`);
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
const expression = unwrapExpression(resolved.value.expression);
|
|
1765
|
+
if (expression === undefined)
|
|
1766
|
+
return;
|
|
1767
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
1768
|
+
addObjectSite(resolved.value.path, resolved.value.source, expression, entry.operationId);
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (ts.isCallExpression(expression) && isOperationHelperCall(expression)) {
|
|
1772
|
+
const argument = operationArgument(expression);
|
|
1773
|
+
const object = argument === undefined ? undefined : unwrapExpression(argument);
|
|
1774
|
+
if (object === undefined || !ts.isObjectLiteralExpression(object)) {
|
|
1775
|
+
resolutionRefusal(resolved.value.path, entry.operationId, "Operation helper argument must be a static object literal.");
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (resolved.value.bindingName !== undefined) {
|
|
1779
|
+
addBindingCandidate(resolved.value.path, resolved.value.bindingName, entry.operationId);
|
|
1780
|
+
}
|
|
1781
|
+
addObjectSite(resolved.value.path, resolved.value.source, object, entry.operationId);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
if (ts.isCallExpression(expression) &&
|
|
1785
|
+
ts.isIdentifier(expression.expression) &&
|
|
1786
|
+
collectSimpleOperationFactories(resolved.value.source).has(expression.expression.text)) {
|
|
1787
|
+
const name = expression.expression.text;
|
|
1788
|
+
const key = `${resolved.value.path}\0${name}`;
|
|
1789
|
+
const factory = factoryIds.get(key) ?? {
|
|
1790
|
+
path: resolved.value.path,
|
|
1791
|
+
name,
|
|
1792
|
+
ids: new Set(),
|
|
1793
|
+
};
|
|
1794
|
+
factory.ids.add(entry.operationId);
|
|
1795
|
+
factoryIds.set(key, factory);
|
|
1796
|
+
discoveredCount += 1;
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
resolutionRefusal(resolved.value.path, entry.operationId, `Operation initializer ${JSON.stringify(expression.getText(resolved.value.source))} is not a raw object, operation helper, or simple same-file factory call.`);
|
|
1800
|
+
};
|
|
1801
|
+
const inspectProviderProperty = (path, source, node, construct) => {
|
|
1802
|
+
const key = `${path}\0${node.getStart(source)}`;
|
|
1803
|
+
if (indexedProviderProperties.has(key))
|
|
1804
|
+
return;
|
|
1805
|
+
indexedProviderProperties.add(key);
|
|
1806
|
+
declarations.push({ path, construct, initializer: node.initializer });
|
|
1807
|
+
const flattened = flattenRegistry({ path, source, expression: node.initializer }, new Set());
|
|
1808
|
+
if ("detail" in flattened) {
|
|
1809
|
+
resolutionRefusal(path, "<operations>", flattened.detail);
|
|
1810
|
+
}
|
|
1811
|
+
else {
|
|
1812
|
+
for (const entry of flattened.entries.values())
|
|
1813
|
+
classifyEntry(entry);
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
for (const [path, source] of sources) {
|
|
1817
|
+
const constObjects = collectModuleConstObjects(source);
|
|
1818
|
+
const visit = (node) => {
|
|
1819
|
+
if (ts.isPropertyAssignment(node) &&
|
|
1820
|
+
staticPropertyName(node.name) === "operations" &&
|
|
1821
|
+
isProviderOperationsProperty(node, constObjects)) {
|
|
1822
|
+
inspectProviderProperty(path, source, node, providerConstructName(node));
|
|
1823
|
+
}
|
|
1824
|
+
if (ts.isCallExpression(node)) {
|
|
1825
|
+
const construct = providerCallConstruct(node);
|
|
1826
|
+
if (construct !== undefined) {
|
|
1827
|
+
for (const argument of node.arguments) {
|
|
1828
|
+
const resolved = resolveLocatedExpression({ path, source, expression: argument }, new Set());
|
|
1829
|
+
if (resolved.status !== "resolved")
|
|
1830
|
+
continue;
|
|
1831
|
+
const object = unwrapExpression(resolved.value.expression);
|
|
1832
|
+
if (object === undefined || !ts.isObjectLiteralExpression(object))
|
|
1833
|
+
continue;
|
|
1834
|
+
for (const property of object.properties) {
|
|
1835
|
+
if (ts.isPropertyAssignment(property) &&
|
|
1836
|
+
staticPropertyName(property.name) === "operations") {
|
|
1837
|
+
inspectProviderProperty(resolved.value.path, resolved.value.source, property, construct);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
ts.forEachChild(node, visit);
|
|
1844
|
+
};
|
|
1845
|
+
visit(source);
|
|
1846
|
+
}
|
|
1847
|
+
for (const candidate of bindingCandidates.values()) {
|
|
1848
|
+
const ids = [...candidate.ids];
|
|
1849
|
+
const indexed = operationIds.get(candidate.path) ?? new Map();
|
|
1850
|
+
if (ids.length === 1) {
|
|
1851
|
+
const operationId = ids[0];
|
|
1852
|
+
if (operationId !== undefined)
|
|
1853
|
+
indexed.set(candidate.name, operationId);
|
|
1854
|
+
}
|
|
1855
|
+
else {
|
|
1856
|
+
indexed.delete(candidate.name);
|
|
1857
|
+
const excluded = excludedBindings.get(candidate.path) ?? new Set();
|
|
1858
|
+
excluded.add(candidate.name);
|
|
1859
|
+
excludedBindings.set(candidate.path, excluded);
|
|
1860
|
+
}
|
|
1861
|
+
operationIds.set(candidate.path, indexed);
|
|
1862
|
+
}
|
|
1863
|
+
for (const factory of factoryIds.values()) {
|
|
1864
|
+
const indexed = operationIds.get(factory.path) ?? new Map();
|
|
1865
|
+
const ids = [...factory.ids];
|
|
1866
|
+
if (ids.length === 1) {
|
|
1867
|
+
const operationId = ids[0];
|
|
1868
|
+
if (operationId !== undefined)
|
|
1869
|
+
indexed.set(factory.name, operationId);
|
|
1870
|
+
}
|
|
1871
|
+
else {
|
|
1872
|
+
indexed.delete(factory.name);
|
|
1873
|
+
const excluded = excludedBindings.get(factory.path) ?? new Set();
|
|
1874
|
+
excluded.add(factory.name);
|
|
1875
|
+
excludedBindings.set(factory.path, excluded);
|
|
1876
|
+
refusals.push(refusal(relativePath(factory.path), factory.name, "factory_operation_id_ambiguous", `Factory ${factory.name} is registered under multiple operation ids: ${ids
|
|
1877
|
+
.map((id) => JSON.stringify(id))
|
|
1878
|
+
.join(", ")}. A shared operation body cannot own one examples locale namespace.`));
|
|
1879
|
+
}
|
|
1880
|
+
operationIds.set(factory.path, indexed);
|
|
1881
|
+
}
|
|
1882
|
+
const resolveStaticObject = (expressionInput, path, active = new Set()) => {
|
|
1883
|
+
const expression = unwrapExpression(expressionInput);
|
|
1884
|
+
if (expression === undefined)
|
|
1885
|
+
return undefined;
|
|
1886
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
1887
|
+
const source = sources.get(path) ?? expression.getSourceFile();
|
|
1888
|
+
return { object: expression, source };
|
|
1889
|
+
}
|
|
1890
|
+
if (ts.isIdentifier(expression)) {
|
|
1891
|
+
const key = `${path}\0member\0${expression.text}`;
|
|
1892
|
+
if (active.has(key))
|
|
1893
|
+
return undefined;
|
|
1894
|
+
const nextActive = new Set(active);
|
|
1895
|
+
nextActive.add(key);
|
|
1896
|
+
const resolved = resolveIdentifier(path, expression.text, nextActive);
|
|
1897
|
+
return resolved.status === "resolved"
|
|
1898
|
+
? resolveStaticObject(resolved.value.expression, resolved.value.path, nextActive)
|
|
1899
|
+
: undefined;
|
|
1900
|
+
}
|
|
1901
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
1902
|
+
const owner = resolveStaticObject(expression.expression, path, active);
|
|
1903
|
+
if (owner === undefined)
|
|
1904
|
+
return undefined;
|
|
1905
|
+
for (const property of owner.object.properties) {
|
|
1906
|
+
if (ts.isSpreadAssignment(property))
|
|
1907
|
+
continue;
|
|
1908
|
+
if (staticPropertyName(property.name) !== expression.name.text)
|
|
1909
|
+
continue;
|
|
1910
|
+
const value = propertyValue(property);
|
|
1911
|
+
if (value === undefined)
|
|
1912
|
+
return undefined;
|
|
1913
|
+
return resolveStaticObject(value, owner.source.fileName, active);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
return undefined;
|
|
1917
|
+
};
|
|
1918
|
+
return {
|
|
1919
|
+
operationIds,
|
|
1920
|
+
operationSites,
|
|
1921
|
+
excludedBindings,
|
|
1922
|
+
refusals,
|
|
1923
|
+
declarations,
|
|
1924
|
+
discoveredCount,
|
|
1925
|
+
staticObjectResolverFor: (currentPath) => (expression, source) => resolveStaticObject(expression, sources.has(source.fileName) ? source.fileName : currentPath),
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
function hasExportModifier(node) {
|
|
1929
|
+
return (ts.canHaveModifiers(node) &&
|
|
1930
|
+
ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ===
|
|
1931
|
+
true);
|
|
1932
|
+
}
|
|
1933
|
+
function resolveStaticModule(containingPath, specifier, sources) {
|
|
1934
|
+
if (!specifier.startsWith(".")) {
|
|
1935
|
+
return {
|
|
1936
|
+
status: "refused",
|
|
1937
|
+
detail: `Static operation discovery refuses non-relative import ${JSON.stringify(specifier)}.`,
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
const path = resolveLocalModule(containingPath, specifier, sources);
|
|
1941
|
+
if (path === undefined) {
|
|
1942
|
+
return {
|
|
1943
|
+
status: "refused",
|
|
1944
|
+
detail: `Static relative import ${JSON.stringify(specifier)} from ${containingPath} could not be resolved.`,
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
return { status: "resolved", path };
|
|
1948
|
+
}
|
|
1949
|
+
function providerConstructName(node) {
|
|
1950
|
+
let current = node;
|
|
1951
|
+
while (current.parent !== undefined) {
|
|
1952
|
+
const parent = current.parent;
|
|
1953
|
+
if (ts.isCallExpression(parent)) {
|
|
1954
|
+
return providerCallConstruct(parent) ?? parent.expression.getText();
|
|
1955
|
+
}
|
|
1956
|
+
current = parent;
|
|
1957
|
+
}
|
|
1958
|
+
return "provider declaration";
|
|
1959
|
+
}
|
|
1960
|
+
function providerCallConstruct(call) {
|
|
1961
|
+
if (ts.isIdentifier(call.expression) &&
|
|
1962
|
+
/(?:defineProvider|Provider)\b/.test(call.expression.text)) {
|
|
1963
|
+
return call.expression.text;
|
|
1964
|
+
}
|
|
1965
|
+
if (ts.isCallExpression(call.expression) &&
|
|
1966
|
+
ts.isIdentifier(call.expression.expression) &&
|
|
1967
|
+
/\bdefineProvider\b/.test(call.expression.expression.text)) {
|
|
1968
|
+
return call.expression.expression.text;
|
|
1969
|
+
}
|
|
1970
|
+
return undefined;
|
|
1971
|
+
}
|
|
1378
1972
|
function buildOperationIdIndex(sourceFiles) {
|
|
1379
1973
|
const result = new Map();
|
|
1380
1974
|
const idsByPath = new Map();
|
|
@@ -1556,7 +2150,12 @@ function resolveLocalModule(containingPath, specifier, sources) {
|
|
|
1556
2150
|
if (!specifier.startsWith("."))
|
|
1557
2151
|
return undefined;
|
|
1558
2152
|
const base = resolve(dirname(containingPath), specifier);
|
|
1559
|
-
|
|
2153
|
+
const emittedExtensionSource = /\.(?:c|m)?js$/.test(base)
|
|
2154
|
+
? base.replace(/\.(?:c|m)?js$/, ".ts")
|
|
2155
|
+
: undefined;
|
|
2156
|
+
for (const candidate of [base, emittedExtensionSource, `${base}.ts`, join(base, "index.ts")]) {
|
|
2157
|
+
if (candidate === undefined)
|
|
2158
|
+
continue;
|
|
1560
2159
|
if (sources.has(candidate))
|
|
1561
2160
|
return candidate;
|
|
1562
2161
|
}
|