@adhisang/minecraft-modding-mcp 3.1.1 → 4.0.0
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 +49 -0
- package/README.md +37 -18
- package/dist/access-transformer-parser.d.ts +17 -0
- package/dist/access-transformer-parser.js +97 -0
- package/dist/cache-registry.d.ts +1 -1
- package/dist/cache-registry.js +10 -2
- package/dist/concurrency.d.ts +1 -0
- package/dist/concurrency.js +24 -0
- package/dist/config.d.ts +10 -1
- package/dist/config.js +52 -1
- package/dist/decompiler/vineflower.js +22 -21
- package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
- package/dist/entry-tools/analyze-symbol-service.d.ts +22 -22
- package/dist/entry-tools/analyze-symbol-service.js +13 -2
- package/dist/entry-tools/inspect-minecraft-service.d.ts +168 -168
- package/dist/entry-tools/inspect-minecraft-service.js +8 -2
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/entry-tools/validate-project-service.d.ts +153 -16
- package/dist/entry-tools/validate-project-service.js +442 -25
- package/dist/gradle-paths.d.ts +4 -0
- package/dist/gradle-paths.js +57 -0
- package/dist/index.js +148 -30
- package/dist/lru-list.d.ts +31 -0
- package/dist/lru-list.js +102 -0
- package/dist/mapping-pipeline-service.d.ts +12 -1
- package/dist/mapping-pipeline-service.js +28 -1
- package/dist/mapping-service.d.ts +16 -0
- package/dist/mapping-service.js +405 -68
- package/dist/minecraft-explorer-service.d.ts +13 -0
- package/dist/minecraft-explorer-service.js +8 -4
- package/dist/mixin-validator.d.ts +33 -2
- package/dist/mixin-validator.js +218 -17
- package/dist/mod-analyzer.d.ts +1 -0
- package/dist/mod-analyzer.js +17 -1
- package/dist/mod-decompile-service.js +4 -4
- package/dist/mod-remap-service.js +1 -54
- package/dist/mod-search-service.d.ts +1 -0
- package/dist/mod-search-service.js +84 -51
- package/dist/observability.d.ts +18 -1
- package/dist/observability.js +44 -1
- package/dist/response-utils.d.ts +69 -0
- package/dist/response-utils.js +227 -0
- package/dist/source-jar-reader.d.ts +16 -0
- package/dist/source-jar-reader.js +103 -1
- package/dist/source-resolver.d.ts +9 -1
- package/dist/source-resolver.js +23 -16
- package/dist/source-service.d.ts +119 -3
- package/dist/source-service.js +1836 -218
- package/dist/storage/artifacts-repo.d.ts +4 -1
- package/dist/storage/artifacts-repo.js +33 -5
- package/dist/storage/files-repo.d.ts +0 -2
- package/dist/storage/files-repo.js +0 -11
- package/dist/storage/migrations.d.ts +1 -1
- package/dist/storage/migrations.js +10 -2
- package/dist/storage/schema.d.ts +2 -0
- package/dist/storage/schema.js +25 -0
- package/dist/tool-contract-manifest.js +8 -6
- package/dist/types.d.ts +20 -0
- package/dist/workspace-mapping-service.d.ts +13 -0
- package/dist/workspace-mapping-service.js +146 -14
- package/package.json +3 -1
package/dist/mapping-service.js
CHANGED
|
@@ -3,8 +3,9 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import fastGlob from "fast-glob";
|
|
5
5
|
import { createError, ERROR_CODES } from "./errors.js";
|
|
6
|
+
import { buildVersionSourceSearchRoots, normalizeOptionalProjectPath } from "./gradle-paths.js";
|
|
6
7
|
import { defaultDownloadPath, downloadToCache } from "./repo-downloader.js";
|
|
7
|
-
import {
|
|
8
|
+
import { collectMatchedJarEntriesAsUtf8 } from "./source-jar-reader.js";
|
|
8
9
|
import { VersionService, isUnobfuscatedVersion } from "./version-service.js";
|
|
9
10
|
const SUPPORTED_MAPPINGS = new Set([
|
|
10
11
|
"obfuscated",
|
|
@@ -19,6 +20,7 @@ const MATCH_RANK = {
|
|
|
19
20
|
};
|
|
20
21
|
const DESCRIPTOR_FALLBACK_CONFIDENCE = 0.85;
|
|
21
22
|
const MAX_CANDIDATES = 200;
|
|
23
|
+
const GLOB_SPECIAL_CHARS = /[\\!*+?()[\]{}@|]/g;
|
|
22
24
|
function createDirectionIndex() {
|
|
23
25
|
return {
|
|
24
26
|
exact: new Map(),
|
|
@@ -706,15 +708,93 @@ function normalizeMemberName(name) {
|
|
|
706
708
|
}
|
|
707
709
|
return normalized;
|
|
708
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* Validate a JVM method descriptor such as `(I)V`, `()Lfoo/Bar;`, `(Lfoo/Bar;[I)V`.
|
|
713
|
+
* Rejects empty strings, missing/mis-positioned parens, empty return type, and invalid base
|
|
714
|
+
* type tokens so "(" or "()" style half-descriptors surface as ERR_INVALID_INPUT instead of
|
|
715
|
+
* being silently accepted.
|
|
716
|
+
*/
|
|
709
717
|
function normalizeMethodDescriptor(descriptor) {
|
|
710
718
|
const normalized = descriptor?.trim() ?? "";
|
|
711
|
-
if (!normalized
|
|
719
|
+
if (!normalized) {
|
|
720
|
+
throw invalidInputError("descriptor must be a valid JVM descriptor when kind=method.", {
|
|
721
|
+
descriptor
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
if (!isValidMethodDescriptor(normalized)) {
|
|
712
725
|
throw invalidInputError("descriptor must be a valid JVM descriptor when kind=method.", {
|
|
713
726
|
descriptor
|
|
714
727
|
});
|
|
715
728
|
}
|
|
716
729
|
return normalized;
|
|
717
730
|
}
|
|
731
|
+
function isValidMethodDescriptor(descriptor) {
|
|
732
|
+
if (!descriptor.startsWith("("))
|
|
733
|
+
return false;
|
|
734
|
+
const closingIndex = descriptor.indexOf(")");
|
|
735
|
+
if (closingIndex < 0)
|
|
736
|
+
return false;
|
|
737
|
+
const argsSection = descriptor.slice(1, closingIndex);
|
|
738
|
+
const returnSection = descriptor.slice(closingIndex + 1);
|
|
739
|
+
if (returnSection.length === 0)
|
|
740
|
+
return false;
|
|
741
|
+
let cursor = 0;
|
|
742
|
+
while (cursor < argsSection.length) {
|
|
743
|
+
const next = consumeFieldType(argsSection, cursor, /*allowVoid*/ false);
|
|
744
|
+
if (next < 0)
|
|
745
|
+
return false;
|
|
746
|
+
cursor = next;
|
|
747
|
+
}
|
|
748
|
+
const returnEnd = consumeFieldType(returnSection, 0, /*allowVoid*/ true);
|
|
749
|
+
return returnEnd === returnSection.length;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* JVM specification §4.3.2: "An array type descriptor is valid only if it represents a type
|
|
753
|
+
* with 255 or fewer dimensions." Matches the `multianewarray` / field-signature limit.
|
|
754
|
+
*/
|
|
755
|
+
const JVM_MAX_ARRAY_DIMENSIONS = 255;
|
|
756
|
+
function consumeFieldType(descriptor, position, allowVoid) {
|
|
757
|
+
// Arrays are handled iteratively so pathological inputs such as `(` + "[".repeat(20000) + `I)V`
|
|
758
|
+
// cannot blow the call stack. After consuming every leading `[`, only the element type token
|
|
759
|
+
// is dispatched through the switch below. Dimensions above the JVM limit are rejected rather
|
|
760
|
+
// than merely accepted as "syntactically valid but semantically absurd" — clients must not be
|
|
761
|
+
// able to push a 20000-dimension descriptor through cache-key construction.
|
|
762
|
+
let cursor = position;
|
|
763
|
+
let arrayDimensions = 0;
|
|
764
|
+
while (cursor < descriptor.length && descriptor[cursor] === "[") {
|
|
765
|
+
cursor += 1;
|
|
766
|
+
arrayDimensions += 1;
|
|
767
|
+
if (arrayDimensions > JVM_MAX_ARRAY_DIMENSIONS)
|
|
768
|
+
return -1;
|
|
769
|
+
}
|
|
770
|
+
if (cursor >= descriptor.length)
|
|
771
|
+
return -1;
|
|
772
|
+
// Void is only valid at the outermost position — inside an array element it is illegal.
|
|
773
|
+
const elementAllowsVoid = cursor === position && allowVoid;
|
|
774
|
+
const token = descriptor[cursor];
|
|
775
|
+
switch (token) {
|
|
776
|
+
case "B":
|
|
777
|
+
case "C":
|
|
778
|
+
case "D":
|
|
779
|
+
case "F":
|
|
780
|
+
case "I":
|
|
781
|
+
case "J":
|
|
782
|
+
case "S":
|
|
783
|
+
case "Z":
|
|
784
|
+
return cursor + 1;
|
|
785
|
+
case "V":
|
|
786
|
+
return elementAllowsVoid ? cursor + 1 : -1;
|
|
787
|
+
case "L": {
|
|
788
|
+
const end = descriptor.indexOf(";", cursor);
|
|
789
|
+
// Reject empty class names like L; and unterminated references.
|
|
790
|
+
if (end < 0 || end === cursor + 1)
|
|
791
|
+
return -1;
|
|
792
|
+
return end + 1;
|
|
793
|
+
}
|
|
794
|
+
default:
|
|
795
|
+
return -1;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
718
798
|
function normalizeQuerySymbol(input, signatureMode, options) {
|
|
719
799
|
if (input.kind !== "class" && input.kind !== "field" && input.kind !== "method") {
|
|
720
800
|
throw invalidInputError('kind must be one of "class", "field", or "method".', {
|
|
@@ -771,9 +851,19 @@ function normalizeQuerySymbol(input, signatureMode, options) {
|
|
|
771
851
|
querySymbol: toSymbolReference(record)
|
|
772
852
|
};
|
|
773
853
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
854
|
+
let descriptor;
|
|
855
|
+
if (signatureMode === "name-only") {
|
|
856
|
+
// name-only matches by owner+name only; a supplied descriptor is validated (so malformed
|
|
857
|
+
// input still surfaces as ERR_INVALID_INPUT) but discarded afterwards so downstream
|
|
858
|
+
// projection / filtering treats the query as "no descriptor".
|
|
859
|
+
if (input.descriptor?.trim()) {
|
|
860
|
+
normalizeMethodDescriptor(input.descriptor);
|
|
861
|
+
}
|
|
862
|
+
descriptor = "";
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
descriptor = normalizeMethodDescriptor(input.descriptor);
|
|
866
|
+
}
|
|
777
867
|
const record = createMethodSymbolRecord(owner, normalizeMemberName(normalizedName), descriptor);
|
|
778
868
|
return {
|
|
779
869
|
record,
|
|
@@ -811,13 +901,32 @@ function applyDisambiguationHints(candidates, disambiguation) {
|
|
|
811
901
|
}
|
|
812
902
|
const descriptorHint = normalizeDescriptorHint(disambiguation.descriptorHint);
|
|
813
903
|
if (descriptorHint) {
|
|
814
|
-
const descriptorMatched = filtered.filter((candidate) => candidate.descriptor === descriptorHint);
|
|
904
|
+
const descriptorMatched = filtered.filter((candidate) => candidate.descriptor != null && candidate.descriptor === descriptorHint);
|
|
815
905
|
if (descriptorMatched.length > 0) {
|
|
816
906
|
filtered = descriptorMatched;
|
|
817
907
|
}
|
|
818
908
|
}
|
|
819
909
|
return filtered;
|
|
820
910
|
}
|
|
911
|
+
function projectLookupCandidateDescriptor(candidate, sourceDescriptor, targetDescriptor) {
|
|
912
|
+
// Tiny mappings preserve method descriptors verbatim, so single-hop tiny paths often
|
|
913
|
+
// return the source descriptor even though the final symbol is already in the target
|
|
914
|
+
// namespace. Multi-hop paths that already produced a target-side descriptor are left
|
|
915
|
+
// unchanged by design.
|
|
916
|
+
if (candidate.kind !== "method" ||
|
|
917
|
+
!candidate.descriptor ||
|
|
918
|
+
!targetDescriptor ||
|
|
919
|
+
candidate.descriptor !== sourceDescriptor) {
|
|
920
|
+
return candidate;
|
|
921
|
+
}
|
|
922
|
+
return {
|
|
923
|
+
...candidate,
|
|
924
|
+
descriptor: targetDescriptor
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
function effectiveLoomSearchProjectPath(projectPath) {
|
|
928
|
+
return normalizeOptionalProjectPath(projectPath) ?? normalizeOptionalProjectPath(process.cwd());
|
|
929
|
+
}
|
|
821
930
|
function collectTargetRecords(graph, targetMapping) {
|
|
822
931
|
return [...(graph.recordsByTarget.get(targetMapping) ?? [])];
|
|
823
932
|
}
|
|
@@ -895,6 +1004,18 @@ export class MappingService {
|
|
|
895
1004
|
fetchFn;
|
|
896
1005
|
graphCache = new Map();
|
|
897
1006
|
buildLocks = new Map();
|
|
1007
|
+
resolutionCache = new Map();
|
|
1008
|
+
static RESOLUTION_CACHE_MAX = 512;
|
|
1009
|
+
static RESOLUTION_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
1010
|
+
resolutionCacheHits = 0;
|
|
1011
|
+
resolutionCacheMisses = 0;
|
|
1012
|
+
get resolutionCacheStats() {
|
|
1013
|
+
return {
|
|
1014
|
+
hits: this.resolutionCacheHits,
|
|
1015
|
+
misses: this.resolutionCacheMisses,
|
|
1016
|
+
size: this.resolutionCache.size
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
898
1019
|
constructor(config, versionService = new VersionService(config), fetchFn = globalThis.fetch) {
|
|
899
1020
|
this.config = config;
|
|
900
1021
|
this.versionService = versionService;
|
|
@@ -911,9 +1032,25 @@ export class MappingService {
|
|
|
911
1032
|
}
|
|
912
1033
|
});
|
|
913
1034
|
}
|
|
914
|
-
|
|
1035
|
+
// Normalize the effective signatureMode exactly once so every downstream path — query
|
|
1036
|
+
// symbol normalization, the strict-overload filter, the cache key, and warning text —
|
|
1037
|
+
// sees the same value. The public tool schema defaults to "name-only", so an omitted
|
|
1038
|
+
// signatureMode reaching the service (e.g. internal callers, MCP resource handlers, the
|
|
1039
|
+
// resolution cache) must default to "name-only" too, otherwise the service contradicts
|
|
1040
|
+
// the advertised default and silently reverts to the old descriptor-required path.
|
|
1041
|
+
// Callers that genuinely need strict descriptor matching pass `signatureMode: "exact"`
|
|
1042
|
+
// explicitly.
|
|
1043
|
+
const effectiveSignatureMode = input.signatureMode ?? "name-only";
|
|
1044
|
+
const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, effectiveSignatureMode, {
|
|
915
1045
|
allowShortClassName: input.kind === "class" && input.sourceMapping === "obfuscated"
|
|
916
1046
|
});
|
|
1047
|
+
const cacheKey = this.buildResolutionCacheKey(version, input, querySymbol, effectiveSignatureMode);
|
|
1048
|
+
const cached = this.resolutionCache.get(cacheKey);
|
|
1049
|
+
if (cached && Date.now() - cached.cachedAt < MappingService.RESOLUTION_CACHE_TTL_MS) {
|
|
1050
|
+
this.resolutionCacheHits += 1;
|
|
1051
|
+
return cached.result;
|
|
1052
|
+
}
|
|
1053
|
+
this.resolutionCacheMisses += 1;
|
|
917
1054
|
const sourceMapping = input.sourceMapping;
|
|
918
1055
|
const targetMapping = input.targetMapping;
|
|
919
1056
|
if (!SUPPORTED_MAPPINGS.has(sourceMapping) || !SUPPORTED_MAPPINGS.has(targetMapping)) {
|
|
@@ -953,7 +1090,7 @@ export class MappingService {
|
|
|
953
1090
|
warnings: []
|
|
954
1091
|
};
|
|
955
1092
|
}
|
|
956
|
-
const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full");
|
|
1093
|
+
const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full", input.projectPath);
|
|
957
1094
|
const path = namespacePath(graph, sourceMapping, targetMapping);
|
|
958
1095
|
if (!path) {
|
|
959
1096
|
return {
|
|
@@ -968,8 +1105,54 @@ export class MappingService {
|
|
|
968
1105
|
]
|
|
969
1106
|
};
|
|
970
1107
|
}
|
|
971
|
-
const
|
|
1108
|
+
const descriptorProjection = queryRecord.kind === "method" && queryRecord.descriptor
|
|
1109
|
+
? this.projectMethodDescriptorToTarget(graph, path, queryRecord.descriptor)
|
|
1110
|
+
: undefined;
|
|
1111
|
+
// Partial projections are still useful for comparison: projectMethodDescriptorToTarget
|
|
1112
|
+
// leaves unmapped `L...;` references unchanged, so JDK / external classes pass through
|
|
1113
|
+
// while Minecraft class references get rewritten to the target namespace. Using the
|
|
1114
|
+
// projected descriptor even when `complete === false` produces descriptors shaped like
|
|
1115
|
+
// the stored records (`(Lnet/minecraft/class_1799;Ljava/lang/String;)V`) and avoids
|
|
1116
|
+
// false negatives for the very common mixed MC + JDK descriptor shape.
|
|
1117
|
+
const projectedDescriptor = descriptorProjection?.hadClassReferences ? descriptorProjection.descriptor : undefined;
|
|
1118
|
+
let rawCandidates = this
|
|
1119
|
+
.mapCandidatesAlongPath(graph, path, queryRecord)
|
|
1120
|
+
.map((candidate) => queryRecord.kind === "method" && queryRecord.descriptor
|
|
1121
|
+
? projectLookupCandidateDescriptor(candidate, queryRecord.descriptor, projectedDescriptor)
|
|
1122
|
+
: candidate);
|
|
972
1123
|
const warnings = [];
|
|
1124
|
+
// signatureMode="exact" on kind=method must not return descriptorless fallback candidates
|
|
1125
|
+
// (lookupCandidates adds owner+name fallbacks by design for the loose path). Without this
|
|
1126
|
+
// filter a caller who supplied `foo(I)V` could be told `foo(Z)V` is the exact mapping,
|
|
1127
|
+
// which would be wrong for migration tooling. Mirror resolveMethodMappingExact's strict
|
|
1128
|
+
// behavior: keep only candidates whose descriptor equals the (projected) requested
|
|
1129
|
+
// descriptor. If nothing passes, the normal "candidates.length === 0 -> not_found" path
|
|
1130
|
+
// takes over. Partial projection is accepted here for the same reason as above — we do
|
|
1131
|
+
// not reject mixed MC + JDK descriptors as mapping_unavailable just because the JDK
|
|
1132
|
+
// class reference was not in the mapping graph.
|
|
1133
|
+
if (queryRecord.kind === "method" &&
|
|
1134
|
+
queryRecord.descriptor &&
|
|
1135
|
+
effectiveSignatureMode === "exact") {
|
|
1136
|
+
// Tiny v2 stores a single descriptor per method entry (typically in the obfuscated
|
|
1137
|
+
// namespace) and shares it across every column, while client mappings attach a mojang
|
|
1138
|
+
// descriptor on the mojang side and an obfuscated descriptor on the obfuscated side.
|
|
1139
|
+
// In multi-hop paths (e.g. mojang -> obfuscated -> intermediary -> yarn) the final
|
|
1140
|
+
// candidate's owner and name live in the target namespace but its descriptor can still
|
|
1141
|
+
// be the obfuscated form that rode along the Tiny hop. A strict filter that compared
|
|
1142
|
+
// only against the fully projected target descriptor dropped those valid candidates
|
|
1143
|
+
// and produced false `not_found` for common Mojang -> Yarn lookups. Accept any
|
|
1144
|
+
// candidate whose descriptor matches the caller's descriptor, the target-space
|
|
1145
|
+
// projection, or the obfuscated-space projection — the three forms that actually
|
|
1146
|
+
// appear in the mapping graph.
|
|
1147
|
+
const strictDescriptor = projectedDescriptor ?? queryRecord.descriptor;
|
|
1148
|
+
const acceptedDescriptors = new Set([queryRecord.descriptor, strictDescriptor]);
|
|
1149
|
+
const toObfuscatedPath = namespacePath(graph, sourceMapping, "obfuscated");
|
|
1150
|
+
if (toObfuscatedPath) {
|
|
1151
|
+
const obfuscatedProjection = this.projectMethodDescriptorToTarget(graph, toObfuscatedPath, queryRecord.descriptor);
|
|
1152
|
+
acceptedDescriptors.add(obfuscatedProjection.descriptor);
|
|
1153
|
+
}
|
|
1154
|
+
rawCandidates = rawCandidates.filter((candidate) => candidate.descriptor !== undefined && acceptedDescriptors.has(candidate.descriptor));
|
|
1155
|
+
}
|
|
973
1156
|
const disambiguatedCandidates = applyDisambiguationHints(rawCandidates, input.disambiguation);
|
|
974
1157
|
if (rawCandidates.length > disambiguatedCandidates.length) {
|
|
975
1158
|
warnings.push(`Disambiguation hints narrowed candidates from ${rawCandidates.length} to ${disambiguatedCandidates.length}.`);
|
|
@@ -987,12 +1170,21 @@ export class MappingService {
|
|
|
987
1170
|
}
|
|
988
1171
|
else if (candidates.length > 1) {
|
|
989
1172
|
warnings.push(`Ambiguous mapping: ${candidates.length} candidates matched. Provide a stricter symbol input or disambiguation hints.`);
|
|
1173
|
+
if (queryRecord.kind === "method") {
|
|
1174
|
+
// find-mapping defaults to signatureMode="name-only", which discards any supplied
|
|
1175
|
+
// descriptor. Telling the caller to "add descriptor" would be ineffective unless they
|
|
1176
|
+
// also switch mode, so we point to the exact alternatives instead.
|
|
1177
|
+
warnings.push("Retry with signatureMode=\"exact\" plus a JVM descriptor, or pass disambiguation.descriptorHint, or raise maxCandidates up to 200 to inspect the full candidate list.");
|
|
1178
|
+
}
|
|
1179
|
+
else {
|
|
1180
|
+
warnings.push("Raise maxCandidates up to 200 to inspect the full candidate list, or use disambiguation.ownerHint to narrow the search.");
|
|
1181
|
+
}
|
|
990
1182
|
}
|
|
991
1183
|
const status = candidates.length === 0 ? "not_found" : candidates.length === 1 ? "resolved" : "ambiguous";
|
|
992
1184
|
const ambiguityReasons = candidates.length > 1
|
|
993
1185
|
? inferAmbiguityReasons(candidates, pathUsesSource(graph.pairs, path, "mojang-client-mappings"))
|
|
994
1186
|
: undefined;
|
|
995
|
-
|
|
1187
|
+
const output = {
|
|
996
1188
|
querySymbol,
|
|
997
1189
|
mappingContext,
|
|
998
1190
|
resolved: status === "resolved",
|
|
@@ -1005,6 +1197,9 @@ export class MappingService {
|
|
|
1005
1197
|
provenance: this.provenanceForPath(graph, path),
|
|
1006
1198
|
ambiguityReasons
|
|
1007
1199
|
};
|
|
1200
|
+
this.resolutionCache.set(cacheKey, { result: output, cachedAt: Date.now() });
|
|
1201
|
+
this.trimResolutionCache();
|
|
1202
|
+
return output;
|
|
1008
1203
|
}
|
|
1009
1204
|
async ensureMappingAvailable(input) {
|
|
1010
1205
|
const version = input.version.trim();
|
|
@@ -1037,7 +1232,7 @@ export class MappingService {
|
|
|
1037
1232
|
warnings: []
|
|
1038
1233
|
};
|
|
1039
1234
|
}
|
|
1040
|
-
const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full");
|
|
1235
|
+
const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full", input.projectPath);
|
|
1041
1236
|
const path = namespacePath(graph, sourceMapping, targetMapping);
|
|
1042
1237
|
if (!path) {
|
|
1043
1238
|
throw createError({
|
|
@@ -1121,7 +1316,7 @@ export class MappingService {
|
|
|
1121
1316
|
warnings: []
|
|
1122
1317
|
};
|
|
1123
1318
|
}
|
|
1124
|
-
const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full");
|
|
1319
|
+
const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full", input.projectPath);
|
|
1125
1320
|
const path = namespacePath(graph, sourceMapping, targetMapping);
|
|
1126
1321
|
if (!path) {
|
|
1127
1322
|
return {
|
|
@@ -1137,12 +1332,16 @@ export class MappingService {
|
|
|
1137
1332
|
};
|
|
1138
1333
|
}
|
|
1139
1334
|
const warnings = [];
|
|
1335
|
+
const descriptorProjection = this.projectMethodDescriptorToTarget(graph, path, descriptor);
|
|
1336
|
+
const projectedDescriptor = descriptorProjection.complete ? descriptorProjection.descriptor : undefined;
|
|
1140
1337
|
const rawCandidates = this
|
|
1141
1338
|
.mapCandidatesAlongPath(graph, path, queryRecord)
|
|
1142
|
-
.filter((candidate) => candidate.kind === "method")
|
|
1339
|
+
.filter((candidate) => candidate.kind === "method")
|
|
1340
|
+
.map((candidate) => projectLookupCandidateDescriptor(candidate, descriptor, projectedDescriptor));
|
|
1143
1341
|
const candidates = rawCandidates.map(toResolutionCandidate);
|
|
1144
1342
|
const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
|
|
1145
|
-
const
|
|
1343
|
+
const strictDescriptor = projectedDescriptor ?? descriptor;
|
|
1344
|
+
const strictCandidates = rawCandidates.filter((candidate) => candidate.descriptor === strictDescriptor);
|
|
1146
1345
|
if (strictCandidates.length === 1) {
|
|
1147
1346
|
const resolved = toResolutionCandidate(strictCandidates[0]);
|
|
1148
1347
|
return {
|
|
@@ -1160,6 +1359,9 @@ export class MappingService {
|
|
|
1160
1359
|
}
|
|
1161
1360
|
if (strictCandidates.length > 1) {
|
|
1162
1361
|
warnings.push("Exact method mapping is ambiguous for owner+method+descriptor.");
|
|
1362
|
+
if (limitedCandidates.candidatesTruncated) {
|
|
1363
|
+
warnings.push("Raise maxCandidates up to 200 to inspect the full candidate list, or narrow the lookup via find-mapping disambiguation hints.");
|
|
1364
|
+
}
|
|
1163
1365
|
return {
|
|
1164
1366
|
querySymbol,
|
|
1165
1367
|
mappingContext,
|
|
@@ -1172,8 +1374,10 @@ export class MappingService {
|
|
|
1172
1374
|
provenance: this.provenanceForPath(graph, path)
|
|
1173
1375
|
};
|
|
1174
1376
|
}
|
|
1175
|
-
if (
|
|
1176
|
-
warnings.push(
|
|
1377
|
+
if (descriptorProjection.hadClassReferences && !descriptorProjection.complete) {
|
|
1378
|
+
warnings.push(pathUsesSource(graph.pairs, path, "mojang-client-mappings")
|
|
1379
|
+
? "Method descriptor could not be preserved through mojang-client-mappings and exact resolution is unavailable."
|
|
1380
|
+
: "Method descriptor could not be fully remapped across the mapping path and exact resolution is unavailable.");
|
|
1177
1381
|
return {
|
|
1178
1382
|
querySymbol,
|
|
1179
1383
|
mappingContext,
|
|
@@ -1460,6 +1664,9 @@ export class MappingService {
|
|
|
1460
1664
|
const buildOutput = (querySymbol, matched, status) => {
|
|
1461
1665
|
const candidates = matched.map((record) => toResolutionCandidate(toLookupCandidate(record)));
|
|
1462
1666
|
const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
|
|
1667
|
+
if (status === "ambiguous" && limitedCandidates.candidatesTruncated) {
|
|
1668
|
+
warnings.push("Raise maxCandidates up to 200 to inspect the full candidate list.");
|
|
1669
|
+
}
|
|
1463
1670
|
return {
|
|
1464
1671
|
querySymbol,
|
|
1465
1672
|
mappingContext,
|
|
@@ -1502,11 +1709,40 @@ export class MappingService {
|
|
|
1502
1709
|
if (input.signatureMode === "name-only") {
|
|
1503
1710
|
const status = methodCandidates.length === 1 ? "resolved" : methodCandidates.length > 1 ? "ambiguous" : "not_found";
|
|
1504
1711
|
if (status === "ambiguous") {
|
|
1505
|
-
|
|
1712
|
+
// name-only discards any supplied descriptor, so telling the caller to "provide
|
|
1713
|
+
// descriptor" would not disambiguate — they need to switch to signatureMode="exact".
|
|
1714
|
+
warnings.push(`Multiple method overloads matched name "${queryRecord.name}" in owner "${queryRecord.owner}". Retry with signatureMode="exact" plus a JVM descriptor to pick one overload.`);
|
|
1506
1715
|
}
|
|
1507
1716
|
return buildOutput(querySymbol, methodCandidates, status);
|
|
1508
1717
|
}
|
|
1509
|
-
|
|
1718
|
+
// Tiny parsing stores a single descriptor per method entry (typically in the obfuscated
|
|
1719
|
+
// namespace) and copies it into every namespace at load time. That means a descriptor
|
|
1720
|
+
// supplied in `sourceMapping` coordinates will not string-compare equal to the record
|
|
1721
|
+
// for any method whose descriptor references remapped Minecraft classes. Project the
|
|
1722
|
+
// caller's descriptor to obfuscated coordinates first so class references line up with
|
|
1723
|
+
// the stored record descriptors. The projection is accepted even when
|
|
1724
|
+
// `projection.complete` is false: `projectMethodDescriptorToTarget` leaves every
|
|
1725
|
+
// unresolvable `L...;` reference unchanged (JDK/external classes like
|
|
1726
|
+
// `Ljava/lang/String;` are never in the mapping graph and pass through by design), so a
|
|
1727
|
+
// partial projection still aligns the Minecraft class refs with the stored descriptor
|
|
1728
|
+
// form while leaving external class refs identical to the user input. Falling back to
|
|
1729
|
+
// verbatim comparison on `complete === false` would send mixed descriptors like
|
|
1730
|
+
// `(Lnet/minecraft/world/item/ItemStack;Ljava/lang/String;)V` down the raw-compare path
|
|
1731
|
+
// and produce false negatives in the most common lookup shape. When no class references
|
|
1732
|
+
// exist at all (primitives-only descriptors such as `(I)V`) the projector marks
|
|
1733
|
+
// `hadClassReferences === false` and we simply reuse the original descriptor.
|
|
1734
|
+
const queryDescriptor = queryRecord.descriptor;
|
|
1735
|
+
let effectiveDescriptor = queryDescriptor;
|
|
1736
|
+
if (sourceMapping !== "obfuscated") {
|
|
1737
|
+
const projectionPath = namespacePath(graph, sourceMapping, "obfuscated");
|
|
1738
|
+
if (projectionPath) {
|
|
1739
|
+
const projection = this.projectMethodDescriptorToTarget(graph, projectionPath, queryDescriptor);
|
|
1740
|
+
if (projection.hadClassReferences) {
|
|
1741
|
+
effectiveDescriptor = projection.descriptor;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
const descriptorMatched = methodCandidates.filter((record) => record.descriptor === effectiveDescriptor || record.descriptor === queryDescriptor);
|
|
1510
1746
|
if (descriptorMatched.length === 1) {
|
|
1511
1747
|
return buildOutput(querySymbol, descriptorMatched, "resolved");
|
|
1512
1748
|
}
|
|
@@ -1624,6 +1860,33 @@ export class MappingService {
|
|
|
1624
1860
|
descriptor: item.record.descriptor
|
|
1625
1861
|
}));
|
|
1626
1862
|
}
|
|
1863
|
+
projectMethodDescriptorToTarget(graph, path, descriptor) {
|
|
1864
|
+
let hadClassReferences = false;
|
|
1865
|
+
let complete = true;
|
|
1866
|
+
const classProjectionCache = new Map();
|
|
1867
|
+
const projectedDescriptor = descriptor.replace(/L([^;]+);/g, (fullMatch, internalName) => {
|
|
1868
|
+
hadClassReferences = true;
|
|
1869
|
+
const cached = classProjectionCache.get(internalName);
|
|
1870
|
+
if (cached) {
|
|
1871
|
+
return `L${cached};`;
|
|
1872
|
+
}
|
|
1873
|
+
const projectedClassCandidates = this
|
|
1874
|
+
.mapCandidatesAlongPath(graph, path, createClassSymbolRecord(internalName.replace(/\//g, ".")))
|
|
1875
|
+
.filter((candidate) => candidate.kind === "class");
|
|
1876
|
+
if (projectedClassCandidates.length !== 1) {
|
|
1877
|
+
complete = false;
|
|
1878
|
+
return fullMatch;
|
|
1879
|
+
}
|
|
1880
|
+
const projectedInternalName = projectedClassCandidates[0].symbol.replace(/\./g, "/");
|
|
1881
|
+
classProjectionCache.set(internalName, projectedInternalName);
|
|
1882
|
+
return `L${projectedInternalName};`;
|
|
1883
|
+
});
|
|
1884
|
+
return {
|
|
1885
|
+
descriptor: projectedDescriptor,
|
|
1886
|
+
hadClassReferences,
|
|
1887
|
+
complete
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1627
1890
|
provenanceForPath(graph, path) {
|
|
1628
1891
|
if (path.length <= 1) {
|
|
1629
1892
|
return undefined;
|
|
@@ -1646,6 +1909,18 @@ export class MappingService {
|
|
|
1646
1909
|
async checkMappingHealth(input) {
|
|
1647
1910
|
const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, input.sourcePriority);
|
|
1648
1911
|
const degradations = [];
|
|
1912
|
+
if (isUnobfuscatedVersion(input.version)) {
|
|
1913
|
+
const requestFulfillable = input.requestedMapping === "obfuscated" || input.requestedMapping === "mojang";
|
|
1914
|
+
if (!requestFulfillable) {
|
|
1915
|
+
degradations.push(`Version ${input.version} is unobfuscated; ${input.requestedMapping} mappings are not applicable.`);
|
|
1916
|
+
}
|
|
1917
|
+
return {
|
|
1918
|
+
mojangMappingsAvailable: true,
|
|
1919
|
+
tinyMappingsAvailable: false,
|
|
1920
|
+
memberRemapAvailable: requestFulfillable,
|
|
1921
|
+
degradations
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1649
1924
|
let graph;
|
|
1650
1925
|
try {
|
|
1651
1926
|
graph = await this.loadGraph(input.version, priority, "full");
|
|
@@ -1692,8 +1967,9 @@ export class MappingService {
|
|
|
1692
1967
|
degradations
|
|
1693
1968
|
};
|
|
1694
1969
|
}
|
|
1695
|
-
async loadGraph(version, priority, mode) {
|
|
1696
|
-
const
|
|
1970
|
+
async loadGraph(version, priority, mode, projectPath) {
|
|
1971
|
+
const effectiveProjectPath = effectiveLoomSearchProjectPath(projectPath);
|
|
1972
|
+
const cacheKey = `${version}|${priority}|${mode}|${effectiveProjectPath ?? ""}`;
|
|
1697
1973
|
const cached = this.graphCache.get(cacheKey);
|
|
1698
1974
|
if (cached) {
|
|
1699
1975
|
this.graphCache.delete(cacheKey);
|
|
@@ -1704,7 +1980,7 @@ export class MappingService {
|
|
|
1704
1980
|
if (existingLock) {
|
|
1705
1981
|
return existingLock;
|
|
1706
1982
|
}
|
|
1707
|
-
const buildPromise = this.buildGraph(version, priority, mode);
|
|
1983
|
+
const buildPromise = this.buildGraph(version, priority, mode, effectiveProjectPath);
|
|
1708
1984
|
this.buildLocks.set(cacheKey, buildPromise);
|
|
1709
1985
|
try {
|
|
1710
1986
|
const built = await buildPromise;
|
|
@@ -1716,7 +1992,7 @@ export class MappingService {
|
|
|
1716
1992
|
this.buildLocks.delete(cacheKey);
|
|
1717
1993
|
}
|
|
1718
1994
|
}
|
|
1719
|
-
async buildGraph(version, priority, mode) {
|
|
1995
|
+
async buildGraph(version, priority, mode, projectPath) {
|
|
1720
1996
|
if (isUnobfuscatedVersion(version)) {
|
|
1721
1997
|
return {
|
|
1722
1998
|
version,
|
|
@@ -1749,7 +2025,7 @@ export class MappingService {
|
|
|
1749
2025
|
const deferredTinyWarnings = [];
|
|
1750
2026
|
for (const source of mappingSourceOrder(priority)) {
|
|
1751
2027
|
const tinyLoad = source === "loom-cache"
|
|
1752
|
-
? await this.loadTinyPairsFromLoom(version)
|
|
2028
|
+
? await this.loadTinyPairsFromLoom(version, projectPath)
|
|
1753
2029
|
: await this.loadTinyPairsFromMaven(version);
|
|
1754
2030
|
if (tinyLoad.pairs.size === 0) {
|
|
1755
2031
|
deferredTinyWarnings.push(...tinyLoad.warnings);
|
|
@@ -1847,46 +2123,67 @@ export class MappingService {
|
|
|
1847
2123
|
};
|
|
1848
2124
|
}
|
|
1849
2125
|
}
|
|
1850
|
-
async loadTinyPairsFromLoom(version) {
|
|
1851
|
-
const
|
|
1852
|
-
const candidates = fastGlob.sync(patterns, {
|
|
1853
|
-
cwd: process.cwd(),
|
|
1854
|
-
absolute: true,
|
|
1855
|
-
onlyFiles: true
|
|
1856
|
-
});
|
|
1857
|
-
const byVersion = candidates
|
|
1858
|
-
.filter((p) => p.replaceAll("\\", "/").includes(`/${version}/`))
|
|
1859
|
-
.sort((left, right) => left.localeCompare(right));
|
|
1860
|
-
if (byVersion.length === 0) {
|
|
1861
|
-
return {
|
|
1862
|
-
pairs: new Map(),
|
|
1863
|
-
warnings: [`No Loom tiny mapping files matched version "${version}".`],
|
|
1864
|
-
mappingArtifact: "loom-cache:none"
|
|
1865
|
-
};
|
|
1866
|
-
}
|
|
2126
|
+
async loadTinyPairsFromLoom(version, projectPath) {
|
|
2127
|
+
const searchRoots = buildVersionSourceSearchRoots(effectiveLoomSearchProjectPath(projectPath));
|
|
1867
2128
|
const merged = new Map();
|
|
1868
|
-
|
|
2129
|
+
const discoveredPaths = new Set();
|
|
2130
|
+
for (const root of searchRoots) {
|
|
2131
|
+
let discovered = [];
|
|
2132
|
+
const versionRoot = join(root, version);
|
|
1869
2133
|
try {
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2134
|
+
discovered = existsSync(versionRoot)
|
|
2135
|
+
? await fastGlob.glob(["**/*.tiny", "**/*.tinyv2"], {
|
|
2136
|
+
cwd: versionRoot,
|
|
2137
|
+
absolute: true,
|
|
2138
|
+
onlyFiles: true
|
|
2139
|
+
})
|
|
2140
|
+
: await fastGlob.glob([`${version.replace(GLOB_SPECIAL_CHARS, "\\$&")}/**/*.tiny`, `${version.replace(GLOB_SPECIAL_CHARS, "\\$&")}/**/*.tinyv2`], {
|
|
2141
|
+
cwd: root,
|
|
2142
|
+
absolute: true,
|
|
2143
|
+
onlyFiles: true
|
|
2144
|
+
});
|
|
1881
2145
|
}
|
|
1882
2146
|
catch {
|
|
1883
|
-
|
|
2147
|
+
continue;
|
|
1884
2148
|
}
|
|
2149
|
+
const byVersion = discovered
|
|
2150
|
+
.filter((path) => path.replaceAll("\\", "/").includes(`/${version}/`))
|
|
2151
|
+
.sort((left, right) => left.localeCompare(right));
|
|
2152
|
+
if (byVersion.length === 0) {
|
|
2153
|
+
continue;
|
|
2154
|
+
}
|
|
2155
|
+
for (const path of byVersion) {
|
|
2156
|
+
discoveredPaths.add(path);
|
|
2157
|
+
try {
|
|
2158
|
+
const content = await readFile(path, "utf8");
|
|
2159
|
+
const parsed = parseTinyMappings(content);
|
|
2160
|
+
for (const [key, index] of parsed.entries()) {
|
|
2161
|
+
const existing = merged.get(key);
|
|
2162
|
+
if (!existing) {
|
|
2163
|
+
merged.set(key, index);
|
|
2164
|
+
}
|
|
2165
|
+
else {
|
|
2166
|
+
mergeDirectionIndexes(existing, index);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
catch {
|
|
2171
|
+
// best effort: skip unreadable or invalid files
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
const orderedPaths = [...discoveredPaths].sort((left, right) => left.localeCompare(right));
|
|
2176
|
+
if (orderedPaths.length > 0) {
|
|
2177
|
+
return {
|
|
2178
|
+
pairs: merged,
|
|
2179
|
+
warnings: [],
|
|
2180
|
+
mappingArtifact: orderedPaths[0]
|
|
2181
|
+
};
|
|
1885
2182
|
}
|
|
1886
2183
|
return {
|
|
1887
|
-
pairs:
|
|
1888
|
-
warnings: [],
|
|
1889
|
-
mappingArtifact:
|
|
2184
|
+
pairs: new Map(),
|
|
2185
|
+
warnings: [`No Loom tiny mapping files matched version "${version}".`],
|
|
2186
|
+
mappingArtifact: "loom-cache:none"
|
|
1890
2187
|
};
|
|
1891
2188
|
}
|
|
1892
2189
|
async loadTinyPairsFromMaven(version) {
|
|
@@ -1942,15 +2239,11 @@ export class MappingService {
|
|
|
1942
2239
|
};
|
|
1943
2240
|
}
|
|
1944
2241
|
async parseTinyFromJar(jarPath) {
|
|
1945
|
-
const
|
|
1946
|
-
const tinyEntries = entries
|
|
1947
|
-
.filter((entry) => entry.toLowerCase().endsWith(".tiny") || entry.toLowerCase().endsWith(".tinyv2"))
|
|
1948
|
-
.sort((left, right) => left.localeCompare(right));
|
|
2242
|
+
const tinyEntries = (await collectMatchedJarEntriesAsUtf8(jarPath, (entry) => entry.toLowerCase().endsWith(".tiny") || entry.toLowerCase().endsWith(".tinyv2"), { continueOnError: true })).sort((left, right) => left.filePath.localeCompare(right.filePath));
|
|
1949
2243
|
const merged = new Map();
|
|
1950
2244
|
for (const entry of tinyEntries) {
|
|
1951
2245
|
try {
|
|
1952
|
-
const
|
|
1953
|
-
const parsed = parseTinyMappings(text);
|
|
2246
|
+
const parsed = parseTinyMappings(entry.content);
|
|
1954
2247
|
for (const [key, index] of parsed.entries()) {
|
|
1955
2248
|
const existing = merged.get(key);
|
|
1956
2249
|
if (!existing) {
|
|
@@ -2002,14 +2295,59 @@ export class MappingService {
|
|
|
2002
2295
|
this.graphCache.delete(oldestKey);
|
|
2003
2296
|
}
|
|
2004
2297
|
}
|
|
2298
|
+
// Note: in-flight buildLocks may re-populate graphCache after release.
|
|
2299
|
+
// Resolution cache entries created by concurrent findMapping() calls may also
|
|
2300
|
+
// survive this invalidation. Both are bounded by TTL (5 min) and will expire
|
|
2301
|
+
// naturally. A full epoch-based invalidation would add complexity for a rare
|
|
2302
|
+
// user-initiated operation (manage-cache).
|
|
2005
2303
|
releaseGraphCacheEntry(version, sourcePriority) {
|
|
2006
2304
|
const normalizedVersion = version.trim();
|
|
2007
2305
|
if (!normalizedVersion) {
|
|
2008
2306
|
return;
|
|
2009
2307
|
}
|
|
2010
2308
|
const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, sourcePriority);
|
|
2011
|
-
|
|
2012
|
-
this.graphCache.
|
|
2309
|
+
const prefix = `${normalizedVersion}|${priority}|`;
|
|
2310
|
+
for (const key of this.graphCache.keys()) {
|
|
2311
|
+
if (key.startsWith(prefix)) {
|
|
2312
|
+
this.graphCache.delete(key);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
const resolutionPrefix = `${normalizedVersion}\0`;
|
|
2316
|
+
for (const key of this.resolutionCache.keys()) {
|
|
2317
|
+
if (key.startsWith(resolutionPrefix)) {
|
|
2318
|
+
this.resolutionCache.delete(key);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
buildResolutionCacheKey(version, input, querySymbol, effectiveSignatureMode) {
|
|
2323
|
+
return [
|
|
2324
|
+
version,
|
|
2325
|
+
input.kind,
|
|
2326
|
+
querySymbol.symbol,
|
|
2327
|
+
querySymbol.descriptor ?? "",
|
|
2328
|
+
input.sourceMapping,
|
|
2329
|
+
input.targetMapping,
|
|
2330
|
+
input.sourcePriority ?? "",
|
|
2331
|
+
effectiveLoomSearchProjectPath(input.projectPath) ?? "",
|
|
2332
|
+
effectiveSignatureMode,
|
|
2333
|
+
String(input.maxCandidates ?? ""),
|
|
2334
|
+
JSON.stringify(input.disambiguation ?? "")
|
|
2335
|
+
].join("\0");
|
|
2336
|
+
}
|
|
2337
|
+
trimResolutionCache() {
|
|
2338
|
+
if (this.resolutionCache.size <= MappingService.RESOLUTION_CACHE_MAX)
|
|
2339
|
+
return;
|
|
2340
|
+
const now = Date.now();
|
|
2341
|
+
for (const [key, entry] of this.resolutionCache) {
|
|
2342
|
+
if (now - entry.cachedAt > MappingService.RESOLUTION_CACHE_TTL_MS) {
|
|
2343
|
+
this.resolutionCache.delete(key);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
while (this.resolutionCache.size > MappingService.RESOLUTION_CACHE_MAX) {
|
|
2347
|
+
const firstKey = this.resolutionCache.keys().next().value;
|
|
2348
|
+
if (firstKey !== undefined)
|
|
2349
|
+
this.resolutionCache.delete(firstKey);
|
|
2350
|
+
}
|
|
2013
2351
|
}
|
|
2014
2352
|
}
|
|
2015
2353
|
// ---------------------------------------------------------------------------
|
|
@@ -2039,14 +2377,13 @@ async function fetchYarnCoordinatesStandalone(version, fetchFn = globalThis.fetc
|
|
|
2039
2377
|
}
|
|
2040
2378
|
}
|
|
2041
2379
|
async function extractTinyFromJar(jarPath, outputPath) {
|
|
2042
|
-
const
|
|
2043
|
-
const tinyEntry =
|
|
2380
|
+
const matchedEntries = await collectMatchedJarEntriesAsUtf8(jarPath, (entry) => entry === "mappings/mappings.tiny" || entry.toLowerCase().endsWith(".tiny"), { maxEntries: 1 });
|
|
2381
|
+
const tinyEntry = matchedEntries[0];
|
|
2044
2382
|
if (!tinyEntry) {
|
|
2045
2383
|
return false;
|
|
2046
2384
|
}
|
|
2047
|
-
const content = await readJarEntryAsUtf8(jarPath, tinyEntry);
|
|
2048
2385
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
2049
|
-
await writeFile(outputPath, content, "utf8");
|
|
2386
|
+
await writeFile(outputPath, tinyEntry.content, "utf8");
|
|
2050
2387
|
return true;
|
|
2051
2388
|
}
|
|
2052
2389
|
/**
|