@adhisang/minecraft-modding-mcp 4.2.1 → 6.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 +69 -0
- package/README.md +5 -4
- package/dist/access-transformer-parser.d.ts +2 -1
- package/dist/access-transformer-parser.js +10 -1
- package/dist/access-widener-parser.d.ts +1 -0
- package/dist/access-widener-parser.js +6 -3
- package/dist/build-suggested-call.js +31 -10
- package/dist/cache-registry.d.ts +8 -0
- package/dist/cache-registry.js +50 -32
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/decompiler/vineflower.js +8 -2
- package/dist/entry-tools/analyze-mod-service.d.ts +24 -24
- package/dist/entry-tools/analyze-mod-service.js +2 -1
- package/dist/entry-tools/analyze-symbol-service.d.ts +57 -22
- package/dist/entry-tools/analyze-symbol-service.js +139 -34
- package/dist/entry-tools/batch-class-members-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-members-service.js +6 -6
- package/dist/entry-tools/batch-class-source-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-source-service.js +5 -6
- package/dist/entry-tools/batch-mappings-service.d.ts +3 -1
- package/dist/entry-tools/batch-mappings-service.js +4 -5
- package/dist/entry-tools/batch-symbol-exists-service.d.ts +3 -1
- package/dist/entry-tools/batch-symbol-exists-service.js +4 -5
- package/dist/entry-tools/compare-minecraft-service.d.ts +10 -10
- package/dist/entry-tools/compare-minecraft-service.js +1 -1
- package/dist/entry-tools/entry-tool-schema.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +1 -3
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +4 -3
- package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +0 -5
- package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -2
- package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +8 -0
- package/dist/entry-tools/inspect-minecraft/handlers/search.js +13 -2
- package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +3 -2
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +90 -89
- package/dist/entry-tools/inspect-minecraft/internal.js +32 -14
- package/dist/entry-tools/inspect-minecraft-service.d.ts +218 -218
- package/dist/entry-tools/inspect-minecraft-service.js +1 -1
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/entry-tools/response-contract.d.ts +3 -1
- package/dist/entry-tools/response-contract.js +3 -2
- package/dist/entry-tools/validate-project/cases/mixin.js +2 -1
- package/dist/entry-tools/validate-project/cases/project-summary.js +5 -3
- package/dist/entry-tools/validate-project-service.d.ts +42 -36
- package/dist/entry-tools/validate-project-service.js +3 -1
- package/dist/entry-tools/verify-mixin-target-service.d.ts +30 -0
- package/dist/entry-tools/verify-mixin-target-service.js +120 -29
- package/dist/error-mapping.d.ts +51 -0
- package/dist/error-mapping.js +157 -2
- package/dist/index.js +147 -76
- package/dist/java-process.js +1 -1
- package/dist/json-rpc-framing.d.ts +4 -0
- package/dist/json-rpc-framing.js +23 -1
- package/dist/mapping/internal-types.d.ts +24 -0
- package/dist/mapping/loaders/tiny-maven.js +46 -20
- package/dist/mapping/lookup.d.ts +13 -2
- package/dist/mapping/lookup.js +60 -2
- package/dist/mapping/types.d.ts +6 -0
- package/dist/mapping-service.d.ts +26 -2
- package/dist/mapping-service.js +212 -55
- package/dist/mcp-helpers.d.ts +1 -0
- package/dist/mcp-helpers.js +25 -3
- package/dist/minecraft-explorer-service.js +24 -11
- package/dist/mixin/access-validators.js +33 -1
- package/dist/mixin/annotation-validators.js +73 -9
- package/dist/mixin/parsed-validator.js +3 -2
- package/dist/mixin-parser.js +119 -7
- package/dist/mod-decompile-service.d.ts +2 -0
- package/dist/mod-decompile-service.js +21 -3
- package/dist/mod-remap-service.d.ts +20 -1
- package/dist/mod-remap-service.js +101 -45
- package/dist/mojang-tiny-mapping-service.js +5 -2
- package/dist/nbt/java-nbt-codec.js +46 -16
- package/dist/nbt/json-patch.js +51 -12
- package/dist/nbt/typed-json.d.ts +1 -0
- package/dist/nbt/typed-json.js +12 -2
- package/dist/nbt/types.d.ts +2 -2
- package/dist/observability.d.ts +6 -0
- package/dist/observability.js +10 -0
- package/dist/page-cursor.d.ts +17 -0
- package/dist/page-cursor.js +0 -0
- package/dist/path-converter.js +10 -3
- package/dist/registry-service.d.ts +2 -0
- package/dist/registry-service.js +41 -2
- package/dist/repo-downloader.js +4 -3
- package/dist/resources.js +45 -9
- package/dist/response-utils.d.ts +29 -4
- package/dist/response-utils.js +95 -5
- package/dist/source/access-validate.js +12 -2
- package/dist/source/artifact-resolver.js +2 -2
- package/dist/source/class-source/members-builder.d.ts +44 -1
- package/dist/source/class-source/members-builder.js +51 -4
- package/dist/source/class-source/snippet-builder.d.ts +13 -0
- package/dist/source/class-source/snippet-builder.js +54 -4
- package/dist/source/class-source-helpers.d.ts +4 -4
- package/dist/source/class-source-helpers.js +77 -20
- package/dist/source/class-source.d.ts +19 -0
- package/dist/source/class-source.js +144 -40
- package/dist/source/file-access.js +3 -2
- package/dist/source/indexer.js +29 -9
- package/dist/source/lifecycle/mapping-helpers.js +28 -3
- package/dist/source/lifecycle/runtime-check.js +20 -6
- package/dist/source/search.d.ts +1 -1
- package/dist/source/search.js +117 -30
- package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
- package/dist/source/validate-mixin.d.ts +2 -1
- package/dist/source/validate-mixin.js +45 -4
- package/dist/source/workspace-target.js +17 -10
- package/dist/source-jar-reader.d.ts +54 -1
- package/dist/source-jar-reader.js +121 -10
- package/dist/source-resolver.js +2 -2
- package/dist/source-service.d.ts +28 -7
- package/dist/source-service.js +9 -1
- package/dist/stdio-supervisor.d.ts +2 -2
- package/dist/stdio-supervisor.js +29 -9
- package/dist/storage/db.js +24 -1
- package/dist/storage/files-repo.d.ts +15 -0
- package/dist/storage/files-repo.js +67 -37
- package/dist/storage/symbols-repo.d.ts +15 -0
- package/dist/storage/symbols-repo.js +53 -19
- package/dist/symbols/symbol-extractor.js +35 -6
- package/dist/text-truncate.d.ts +7 -0
- package/dist/text-truncate.js +21 -0
- package/dist/tiny-remapper-resolver.js +10 -0
- package/dist/tool-contract-manifest.js +2 -2
- package/dist/tool-execution-gate.js +12 -1
- package/dist/tool-guidance.d.ts +6 -2
- package/dist/tool-guidance.js +63 -13
- package/dist/tool-schemas.d.ts +561 -496
- package/dist/tool-schemas.js +175 -139
- package/dist/types.d.ts +1 -0
- package/dist/version-diff-service.js +5 -0
- package/dist/version-service.d.ts +3 -0
- package/dist/version-service.js +18 -2
- package/dist/warning-details.d.ts +30 -0
- package/dist/warning-details.js +80 -0
- package/dist/workspace-mapping-service.js +9 -0
- package/docs/README-ja.md +2 -2
- package/docs/examples.md +26 -2
- package/docs/tool-reference.md +22 -17
- package/package.json +1 -1
|
@@ -183,6 +183,17 @@ export function validateParsedAccessTransformer(parsed, membersByClass, warnings
|
|
|
183
183
|
invalidCount++;
|
|
184
184
|
continue;
|
|
185
185
|
}
|
|
186
|
+
if (entry.targetKind === "wildcard-all" || entry.targetKind === "wildcard-method") {
|
|
187
|
+
// A wildcard transforms all members (or all methods); it is valid as long as
|
|
188
|
+
// the owner class resolved, which it has by this point.
|
|
189
|
+
validatedEntries.push({
|
|
190
|
+
...entry,
|
|
191
|
+
valid: true,
|
|
192
|
+
...(options?.includeRuntimeEvidence ? { resolvedInRuntime: true } : {})
|
|
193
|
+
});
|
|
194
|
+
validCount++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
186
197
|
if (entry.targetKind === "field") {
|
|
187
198
|
const fieldNames = allFieldNames(members);
|
|
188
199
|
const matchedField = members.fields.find((member) => member.name === entry.name);
|
|
@@ -215,8 +226,29 @@ export function validateParsedAccessTransformer(parsed, membersByClass, warnings
|
|
|
215
226
|
continue;
|
|
216
227
|
}
|
|
217
228
|
const methodNames = allMethodNames(members);
|
|
218
|
-
const
|
|
229
|
+
const sameNameMethods = [...members.methods, ...members.constructors].filter((member) => member.name === entry.name);
|
|
230
|
+
// Match by descriptor when one is supplied (AT method lines always carry one);
|
|
231
|
+
// fall back to a unique name match when it is absent.
|
|
232
|
+
const matchedMethod = entry.descriptor
|
|
233
|
+
? sameNameMethods.find((member) => member.jvmDescriptor === entry.descriptor)
|
|
234
|
+
: sameNameMethods.length === 1
|
|
235
|
+
? sameNameMethods[0]
|
|
236
|
+
: undefined;
|
|
219
237
|
if (!matchedMethod) {
|
|
238
|
+
// Distinguish a true missing method from a descriptor mismatch so the user
|
|
239
|
+
// can see which overloads exist instead of a flat "not found".
|
|
240
|
+
if (entry.descriptor && sameNameMethods.length > 0) {
|
|
241
|
+
const available = [...new Set(sameNameMethods.map((member) => member.jvmDescriptor))];
|
|
242
|
+
validatedEntries.push({
|
|
243
|
+
...entry,
|
|
244
|
+
valid: false,
|
|
245
|
+
issue: `Method "${entry.name}" exists in "${ownerFqn}" but no overload matches descriptor ${entry.descriptor} (available: ${available.join(", ")}).`,
|
|
246
|
+
suggestions: available,
|
|
247
|
+
...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
|
|
248
|
+
});
|
|
249
|
+
invalidCount++;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
220
252
|
const suggestions = entry.name ? suggestSimilar(entry.name, methodNames) : [];
|
|
221
253
|
validatedEntries.push({
|
|
222
254
|
...entry,
|
|
@@ -6,8 +6,26 @@ export function validateInjection(inj, targetMembers, targetNames, issues, resol
|
|
|
6
6
|
continue;
|
|
7
7
|
const methodNames = allMethodNames(members);
|
|
8
8
|
// Strip owner prefix and JVM descriptor from the method reference
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
const rawMethodName = extractMethodName(inj.method);
|
|
10
|
+
// Strip a trailing target-selector quantifier (e.g. "tick*", "tick+", "tick{2,3}")
|
|
11
|
+
const quantifier = /(?:\*|\+|\{\d+(?:,\d+)?\})$/.exec(rawMethodName)?.[0];
|
|
12
|
+
const methodName = quantifier
|
|
13
|
+
? rawMethodName.slice(0, rawMethodName.length - quantifier.length)
|
|
14
|
+
: rawMethodName;
|
|
15
|
+
if (quantifier && methodName === "") {
|
|
16
|
+
// Bare wildcard selector (e.g. method = "*") matches everything; unverifiable.
|
|
17
|
+
resolvedMembers.push({
|
|
18
|
+
annotation: `@${inj.annotation}`,
|
|
19
|
+
name: rawMethodName,
|
|
20
|
+
line: inj.line,
|
|
21
|
+
status: "skipped"
|
|
22
|
+
});
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const matched = (quantifier === "*" || quantifier === "+")
|
|
26
|
+
? methodNames.some((n) => n.startsWith(methodName))
|
|
27
|
+
: methodNames.includes(methodName);
|
|
28
|
+
if (!matched) {
|
|
11
29
|
const suggestions = suggestSimilar(methodName, methodNames);
|
|
12
30
|
const descriptor = extractMethodDescriptor(inj.method);
|
|
13
31
|
const descriptorHint = descriptor ? ` (descriptor: ${descriptor})` : "";
|
|
@@ -43,13 +61,59 @@ export function validateInjection(inj, targetMembers, targetNames, issues, resol
|
|
|
43
61
|
});
|
|
44
62
|
}
|
|
45
63
|
else {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
// Name matched. If the reference carries a JVM descriptor, require an
|
|
65
|
+
// overload to match it; otherwise a wrong-signature target (e.g. tick(D)V
|
|
66
|
+
// when only tick(I)V exists) is silently reported as resolved.
|
|
67
|
+
const refDescriptor = quantifier ? undefined : extractMethodDescriptor(inj.method);
|
|
68
|
+
const sameNameMembers = [...members.constructors, ...members.methods].filter((member) => member.name === methodName);
|
|
69
|
+
if (refDescriptor &&
|
|
70
|
+
sameNameMembers.length > 0 &&
|
|
71
|
+
!sameNameMembers.some((member) => member.jvmDescriptor === refDescriptor)) {
|
|
72
|
+
const available = [...new Set(sameNameMembers.map((member) => member.jvmDescriptor))];
|
|
73
|
+
const isRemapFailed = remapFailedMembers?.get(targetName)?.has(methodName);
|
|
74
|
+
const isSigFailed = signatureFailedTargets?.has(targetName);
|
|
75
|
+
// Only a healthy mapping lets us trust that the user-written descriptor is
|
|
76
|
+
// byte-equal to the runtime descriptor; otherwise downgrade to a warning.
|
|
77
|
+
const mappingDegraded = Boolean(isRemapFailed) || Boolean(isSigFailed) || healthReport?.overallHealthy === false;
|
|
78
|
+
const resolutionPath = isRemapFailed
|
|
79
|
+
? "member-remap-failed"
|
|
80
|
+
: isSigFailed
|
|
81
|
+
? "source-signature-unavailable"
|
|
82
|
+
: undefined;
|
|
83
|
+
const issueConfidence = mappingDegraded
|
|
84
|
+
? "uncertain"
|
|
85
|
+
: confidence ?? "definite";
|
|
86
|
+
issues.push({
|
|
87
|
+
severity: mappingDegraded ? "warning" : "error",
|
|
88
|
+
kind: "descriptor-mismatch",
|
|
89
|
+
annotation: `@${inj.annotation}`,
|
|
90
|
+
target: `${targetName}#${inj.method}`,
|
|
91
|
+
message: `Method "${methodName}" exists in "${targetName}" but no overload matches descriptor ${refDescriptor} (available: ${available.join(", ")}).${mappingDegraded ? " (mapping degraded; may be a remap artifact)" : ""}`,
|
|
92
|
+
suggestions: available.length > 0 ? available : undefined,
|
|
93
|
+
line: inj.line,
|
|
94
|
+
confidence: issueConfidence,
|
|
95
|
+
confidenceReason: mappingDegraded
|
|
96
|
+
? `Member remap/signature degraded; descriptor mismatch may be a tooling artifact, not a true signature error.`
|
|
97
|
+
: confidenceReason,
|
|
98
|
+
resolutionPath,
|
|
99
|
+
falsePositiveRisk: computeFalsePositiveRisk(healthReport, resolutionPath, issueConfidence)
|
|
100
|
+
});
|
|
101
|
+
resolvedMembers.push({
|
|
102
|
+
annotation: `@${inj.annotation}`,
|
|
103
|
+
name: methodName,
|
|
104
|
+
line: inj.line,
|
|
105
|
+
status: "not-found"
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
resolvedMembers.push({
|
|
110
|
+
annotation: `@${inj.annotation}`,
|
|
111
|
+
name: methodName,
|
|
112
|
+
line: inj.line,
|
|
113
|
+
resolvedTo: `${targetName}#${methodName}`,
|
|
114
|
+
status: "resolved"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
53
117
|
}
|
|
54
118
|
}
|
|
55
119
|
}
|
|
@@ -27,6 +27,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
27
27
|
resolutionPath: "target-mapping-failed",
|
|
28
28
|
falsePositiveRisk: healthReport?.overallHealthy === false ? "high" : "medium"
|
|
29
29
|
});
|
|
30
|
+
addSkippedMembers(parsed, resolvedMembers);
|
|
30
31
|
}
|
|
31
32
|
else if (symbolExistsButSignatureFailed?.has(target.className)) {
|
|
32
33
|
// Symbol exists in mapping graph but getSignature failed — tool limitation, not code issue
|
|
@@ -209,7 +210,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
209
210
|
if (version) {
|
|
210
211
|
assignSuggested(issue, "get-class-source", {
|
|
211
212
|
className: issue.target,
|
|
212
|
-
target: {
|
|
213
|
+
target: { kind: "version", value: version },
|
|
213
214
|
...(mapping ? { mapping } : {}),
|
|
214
215
|
mode: "metadata",
|
|
215
216
|
...classSourceContext
|
|
@@ -236,7 +237,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
236
237
|
if (version) {
|
|
237
238
|
assignSuggested(issue, "get-class-source", {
|
|
238
239
|
className,
|
|
239
|
-
target: {
|
|
240
|
+
target: { kind: "version", value: version },
|
|
240
241
|
...(mapping ? { mapping } : {}),
|
|
241
242
|
mode: "metadata",
|
|
242
243
|
...classSourceContext
|
package/dist/mixin-parser.js
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
/* ------------------------------------------------------------------ */
|
|
6
6
|
/* Regex patterns */
|
|
7
7
|
/* ------------------------------------------------------------------ */
|
|
8
|
-
const CLASS_DECL_RE =
|
|
8
|
+
const CLASS_DECL_RE = /^\s*(?:@[\w$.]+(?:\([^)]*\))?\s+)*(?:public\s+|final\s+|abstract\s+|sealed\s+)*\b(?:class|interface)\s+(\w+)/;
|
|
9
9
|
// import statements for FQCN resolution
|
|
10
10
|
const IMPORT_RE = /^\s*import\s+([\w.]+)\s*;/;
|
|
11
11
|
// @Mixin(Foo.class) or @Mixin({Foo.class, Bar.class}) or @Mixin(value = Foo.class)
|
|
12
12
|
// Also handles @Mixin(value = {Foo.class, Bar.class}, priority = 900)
|
|
13
13
|
const MIXIN_ANNOTATION_START_RE = /^\s*@Mixin\s*\(/;
|
|
14
|
-
|
|
14
|
+
// `$` is a legal Java identifier character (e.g. generated classes like My$Class);
|
|
15
|
+
// without it the capture would start mid-name and yield a wrong target.
|
|
16
|
+
const MIXIN_TARGET_RE = /([\w$][\w.$]*?)\.class/g;
|
|
15
17
|
const MIXIN_PRIORITY_RE = /priority\s*=\s*(\d+)/;
|
|
16
18
|
// String-form targets: @Mixin(targets = "pkg.Class") or @Mixin(targets = {"pkg.A", "pkg.B"})
|
|
17
19
|
const MIXIN_TARGETS_STRING_RE = /targets\s*=\s*(?:\{([^}]+)\}|"([^"]+)")/;
|
|
@@ -55,15 +57,100 @@ function collectMultilineAnnotation(lines, startIndex) {
|
|
|
55
57
|
}
|
|
56
58
|
return { text, endIndex: lines.length - 1 };
|
|
57
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Blank out `/* *\/` block comments and `//` line comments (outside string literals)
|
|
62
|
+
* while preserving line count and line numbers, so the annotation scans never match
|
|
63
|
+
* inside commented-out code.
|
|
64
|
+
*/
|
|
65
|
+
function stripComments(source) {
|
|
66
|
+
let out = "";
|
|
67
|
+
let inBlock = false;
|
|
68
|
+
let inString = false;
|
|
69
|
+
for (let i = 0; i < source.length; i++) {
|
|
70
|
+
const ch = source[i];
|
|
71
|
+
const next = source[i + 1];
|
|
72
|
+
if (inBlock) {
|
|
73
|
+
if (ch === "*" && next === "/") {
|
|
74
|
+
out += " ";
|
|
75
|
+
i++;
|
|
76
|
+
inBlock = false;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
out += ch === "\n" ? "\n" : " ";
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (inString) {
|
|
84
|
+
out += ch;
|
|
85
|
+
if (ch === "\\" && next !== undefined) {
|
|
86
|
+
out += next;
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
else if (ch === '"') {
|
|
90
|
+
inString = false;
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (ch === '"') {
|
|
95
|
+
inString = true;
|
|
96
|
+
out += ch;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (ch === "/" && next === "*") {
|
|
100
|
+
out += " ";
|
|
101
|
+
i++;
|
|
102
|
+
inBlock = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (ch === "/" && next === "/") {
|
|
106
|
+
while (i < source.length && source[i] !== "\n") {
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
i--;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
out += ch;
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
58
116
|
/**
|
|
59
117
|
* Skip past annotations (including multi-line ones) starting at `startIndex`.
|
|
60
118
|
* Returns the index of the first non-annotation line.
|
|
61
119
|
* Lines with inline annotation + declaration (e.g. `@Nullable private int x;`)
|
|
62
120
|
* are treated as declaration lines (not skipped).
|
|
63
121
|
*/
|
|
122
|
+
function isUpperCaseLetter(ch) {
|
|
123
|
+
return ch !== ch.toLowerCase() && ch === ch.toUpperCase();
|
|
124
|
+
}
|
|
64
125
|
function skipAnnotations(lines, startIndex) {
|
|
65
126
|
let idx = startIndex;
|
|
66
|
-
while (idx < lines.length
|
|
127
|
+
while (idx < lines.length) {
|
|
128
|
+
const current = lines[idx];
|
|
129
|
+
// Skip blank lines and comment-only lines that sit between an annotation and the
|
|
130
|
+
// member declaration; otherwise the declaration line is missed and the member dropped.
|
|
131
|
+
if (/^\s*$/.test(current) || /^\s*\/\//.test(current)) {
|
|
132
|
+
idx++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (/^\s*\/\*/.test(current)) {
|
|
136
|
+
if (current.includes("*/")) {
|
|
137
|
+
const afterComment = current.slice(current.indexOf("*/") + 2).trim();
|
|
138
|
+
if (afterComment === "") {
|
|
139
|
+
idx++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
break; // declaration text follows an inline block comment
|
|
143
|
+
}
|
|
144
|
+
idx++;
|
|
145
|
+
while (idx < lines.length && !lines[idx].includes("*/")) {
|
|
146
|
+
idx++;
|
|
147
|
+
}
|
|
148
|
+
idx++; // move past the closing "*/" line
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!/^\s*@/.test(current)) {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
67
154
|
if (lines[idx].includes("(")) {
|
|
68
155
|
// Check if parentheses are unbalanced (multi-line annotation) — always skip
|
|
69
156
|
let depth = 0;
|
|
@@ -106,18 +193,33 @@ const INLINE_ANNOTATION_RE = /\s*@[\w$.]+(?:\([^)]*\))?\s*/g;
|
|
|
106
193
|
function stripInlineAnnotations(line) {
|
|
107
194
|
return line.replace(INLINE_ANNOTATION_RE, " ").trim();
|
|
108
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* JavaBeans `Introspector.decapitalize`, which SpongePowered Mixin's AccessorInfo
|
|
198
|
+
* uses for inflection: leave the name unchanged when its first two characters are
|
|
199
|
+
* both upper case (getURL -> URL, getNBT -> NBT), otherwise lower-case the first
|
|
200
|
+
* character (getId -> id, getHealth -> health).
|
|
201
|
+
*/
|
|
202
|
+
function decapitalize(name) {
|
|
203
|
+
if (name.length === 0) {
|
|
204
|
+
return name;
|
|
205
|
+
}
|
|
206
|
+
if (name.length > 1 && isUpperCaseLetter(name.charAt(1)) && isUpperCaseLetter(name.charAt(0))) {
|
|
207
|
+
return name;
|
|
208
|
+
}
|
|
209
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
210
|
+
}
|
|
109
211
|
function inferAccessorTarget(methodName) {
|
|
110
212
|
const getterMatch = GETTER_PREFIX_RE.exec(methodName);
|
|
111
213
|
if (getterMatch) {
|
|
112
|
-
return
|
|
214
|
+
return decapitalize(getterMatch[1]);
|
|
113
215
|
}
|
|
114
216
|
const setterMatch = SETTER_PREFIX_RE.exec(methodName);
|
|
115
217
|
if (setterMatch) {
|
|
116
|
-
return
|
|
218
|
+
return decapitalize(setterMatch[1]);
|
|
117
219
|
}
|
|
118
220
|
const invokerMatch = INVOKER_PREFIX_RE.exec(methodName);
|
|
119
221
|
if (invokerMatch) {
|
|
120
|
-
return
|
|
222
|
+
return decapitalize(invokerMatch[1]);
|
|
121
223
|
}
|
|
122
224
|
return methodName;
|
|
123
225
|
}
|
|
@@ -125,7 +227,7 @@ function inferAccessorTarget(methodName) {
|
|
|
125
227
|
/* Main parser */
|
|
126
228
|
/* ------------------------------------------------------------------ */
|
|
127
229
|
export function parseMixinSource(source) {
|
|
128
|
-
const lines = source.split(/\r?\n/);
|
|
230
|
+
const lines = stripComments(source).split(/\r?\n/);
|
|
129
231
|
const parseWarnings = [];
|
|
130
232
|
const targets = [];
|
|
131
233
|
const injections = [];
|
|
@@ -279,6 +381,16 @@ export function parseMixinSource(source) {
|
|
|
279
381
|
}
|
|
280
382
|
i = endIndex;
|
|
281
383
|
}
|
|
384
|
+
// Try same-line declaration first (e.g. `@Accessor("x") int getX();`)
|
|
385
|
+
const inlineDecl = stripInlineAnnotations(line);
|
|
386
|
+
const inlineMethodMatch = inlineDecl.includes("(") ? METHOD_DECL_RE.exec(inlineDecl) : null;
|
|
387
|
+
if (inlineMethodMatch) {
|
|
388
|
+
const methodName = inlineMethodMatch[2];
|
|
389
|
+
const targetName = explicitTarget ?? inferAccessorTarget(methodName);
|
|
390
|
+
accessors.push({ annotation, name: methodName, targetName, line: lineNum });
|
|
391
|
+
i++;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
282
394
|
// Find the method declaration following the annotation (skip multi-line annotations)
|
|
283
395
|
let methodLine = i + 1;
|
|
284
396
|
methodLine = skipAnnotations(lines, methodLine);
|
|
@@ -43,8 +43,10 @@ export type GetModClassSourceOutput = {
|
|
|
43
43
|
export declare class ModDecompileService {
|
|
44
44
|
private readonly config;
|
|
45
45
|
private readonly decompileCache;
|
|
46
|
+
private readonly inflightDecompiles;
|
|
46
47
|
constructor(config: Config);
|
|
47
48
|
decompileModJar(input: DecompileModJarInput): Promise<DecompileModJarOutput>;
|
|
48
49
|
getModClassSource(input: GetModClassSourceInput): Promise<GetModClassSourceOutput>;
|
|
49
50
|
private ensureDecompiled;
|
|
51
|
+
private decompileAndCache;
|
|
50
52
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
2
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
4
|
import { isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
4
5
|
import { createError, ERROR_CODES } from "./errors.js";
|
|
@@ -6,10 +7,12 @@ import { log } from "./logger.js";
|
|
|
6
7
|
import { decompileBinaryJar } from "./decompiler/vineflower.js";
|
|
7
8
|
import { resolveVineflowerJar } from "./vineflower-resolver.js";
|
|
8
9
|
import { analyzeModJar } from "./mod-analyzer.js";
|
|
9
|
-
import { validateAndNormalizeJarPath } from "./path-resolver.js";
|
|
10
|
+
import { buildJarSignature, validateAndNormalizeJarPath } from "./path-resolver.js";
|
|
11
|
+
import { sliceToMaxCharsSafe } from "./text-truncate.js";
|
|
10
12
|
const DECOMPILE_TIMEOUT_MS = 300_000;
|
|
11
13
|
function modDecompileCacheKey(jarPath) {
|
|
12
|
-
|
|
14
|
+
const stats = statSync(jarPath);
|
|
15
|
+
return createHash("sha256").update(`${jarPath}|${buildJarSignature(stats)}`).digest("hex");
|
|
13
16
|
}
|
|
14
17
|
function classNameToFilePath(className) {
|
|
15
18
|
return className.replaceAll(".", "/") + ".java";
|
|
@@ -27,6 +30,7 @@ export class ModDecompileService {
|
|
|
27
30
|
config;
|
|
28
31
|
// Cache: jarPath hash → decompiled output dir + file list
|
|
29
32
|
decompileCache = new Map();
|
|
33
|
+
inflightDecompiles = new Map();
|
|
30
34
|
constructor(config) {
|
|
31
35
|
this.config = config;
|
|
32
36
|
}
|
|
@@ -122,7 +126,7 @@ export class ModDecompileService {
|
|
|
122
126
|
}
|
|
123
127
|
// Apply maxChars truncation
|
|
124
128
|
if (input.maxChars != null && input.maxChars > 0 && content.length > input.maxChars) {
|
|
125
|
-
content = content
|
|
129
|
+
content = sliceToMaxCharsSafe(content, input.maxChars);
|
|
126
130
|
charsTruncated = true;
|
|
127
131
|
truncated = true;
|
|
128
132
|
}
|
|
@@ -154,6 +158,20 @@ export class ModDecompileService {
|
|
|
154
158
|
this.decompileCache.set(cacheKey, cached);
|
|
155
159
|
return cached;
|
|
156
160
|
}
|
|
161
|
+
const existingDecompile = this.inflightDecompiles.get(cacheKey);
|
|
162
|
+
if (existingDecompile) {
|
|
163
|
+
return existingDecompile;
|
|
164
|
+
}
|
|
165
|
+
const decompilePromise = this.decompileAndCache(jarPath, cacheKey, warnings);
|
|
166
|
+
this.inflightDecompiles.set(cacheKey, decompilePromise);
|
|
167
|
+
try {
|
|
168
|
+
return await decompilePromise;
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
this.inflightDecompiles.delete(cacheKey);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async decompileAndCache(jarPath, cacheKey, warnings) {
|
|
157
175
|
log("info", "mod-decompile.start", { jarPath });
|
|
158
176
|
const startedAt = Date.now();
|
|
159
177
|
// Analyze mod metadata
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
import { resolveTinyMappingFile } from "./mapping-service.js";
|
|
2
|
+
import { resolveMojangTinyFile } from "./mojang-tiny-mapping-service.js";
|
|
3
|
+
import { remapJar } from "./tiny-remapper-service.js";
|
|
4
|
+
import { resolveTinyRemapperJar } from "./tiny-remapper-resolver.js";
|
|
1
5
|
import type { Config } from "./types.js";
|
|
2
6
|
export interface ModRemapInput {
|
|
3
7
|
inputJar: string;
|
|
4
8
|
outputJar?: string;
|
|
5
9
|
mcVersion?: string;
|
|
6
10
|
targetMapping: "yarn" | "mojang";
|
|
11
|
+
/**
|
|
12
|
+
* Skip the remap cache and force re-resolution of the newest yarn build
|
|
13
|
+
* (bypasses both the output-jar cache hit and the yarn metadata TTL).
|
|
14
|
+
*/
|
|
15
|
+
forceRemap?: boolean;
|
|
7
16
|
}
|
|
8
17
|
export interface ModRemapResult {
|
|
9
18
|
outputJar: string;
|
|
@@ -14,4 +23,14 @@ export interface ModRemapResult {
|
|
|
14
23
|
durationMs: number;
|
|
15
24
|
warnings: string[];
|
|
16
25
|
}
|
|
17
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Injectable external dependencies, used by tests to drive the remap pipeline
|
|
28
|
+
* without spawning Java or downloading mapping jars. Production callers omit it.
|
|
29
|
+
*/
|
|
30
|
+
export interface RemapModJarDeps {
|
|
31
|
+
remapJar?: typeof remapJar;
|
|
32
|
+
resolveTinyMappingFile?: typeof resolveTinyMappingFile;
|
|
33
|
+
resolveMojangTinyFile?: typeof resolveMojangTinyFile;
|
|
34
|
+
resolveTinyRemapperJar?: typeof resolveTinyRemapperJar;
|
|
35
|
+
}
|
|
36
|
+
export declare function remapModJar(input: ModRemapInput, config: Config, deps?: RemapModJarDeps): Promise<ModRemapResult>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { copyFileSync, existsSync, mkdirSync, rmSync, statSync } from "node:fs";
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, statSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { createError, ERROR_CODES, isAppError } from "./errors.js";
|
|
@@ -11,6 +11,13 @@ import { normalizePathForHost } from "./path-converter.js";
|
|
|
11
11
|
import { detectFabricLikeInputNamespace } from "./source-jar-reader.js";
|
|
12
12
|
import { remapJar } from "./tiny-remapper-service.js";
|
|
13
13
|
import { resolveTinyRemapperJar } from "./tiny-remapper-resolver.js";
|
|
14
|
+
// Bumped whenever the remap pipeline changes in a way that makes previously
|
|
15
|
+
// cached output jars wrong. The v1 pipeline mis-mapped intermediary jars to
|
|
16
|
+
// mojang using an obfuscated<->mojang tiny (no intermediary namespace), so any
|
|
17
|
+
// cached mojang output is invalid and must not be reused. v3 adds the resolved
|
|
18
|
+
// mapping identity (yarn build coordinate) to the cache key so a newer yarn
|
|
19
|
+
// build no longer serves the stale jar; the bump invalidates v2 outputs once.
|
|
20
|
+
const REMAP_PIPELINE_VERSION = "v3";
|
|
14
21
|
function normalizeTargetNamespace(target) {
|
|
15
22
|
return target === "yarn" ? "yarn" : "mojang";
|
|
16
23
|
}
|
|
@@ -58,14 +65,18 @@ function copyJarToDestination(sourceJar, destinationJar) {
|
|
|
58
65
|
mkdirSync(dirname(destinationJar), { recursive: true });
|
|
59
66
|
copyFileSync(sourceJar, destinationJar);
|
|
60
67
|
}
|
|
61
|
-
function buildCacheKey(inputJar, fromNamespace, targetNamespace, mcVersion) {
|
|
68
|
+
function buildCacheKey(inputJar, fromNamespace, targetNamespace, mcVersion, mappingIdentity) {
|
|
62
69
|
const stat = statSync(inputJar, { throwIfNoEntry: false });
|
|
63
70
|
const signature = stat ? `${stat.mtimeMs}:${stat.size}` : "unknown";
|
|
64
71
|
return createHash("sha256")
|
|
65
|
-
.update(`${inputJar}|${signature}|${fromNamespace}|${targetNamespace}|${mcVersion}`)
|
|
72
|
+
.update(`${REMAP_PIPELINE_VERSION}|${inputJar}|${signature}|${fromNamespace}|${targetNamespace}|${mcVersion}|${mappingIdentity}`)
|
|
66
73
|
.digest("hex");
|
|
67
74
|
}
|
|
68
|
-
export async function remapModJar(input, config) {
|
|
75
|
+
export async function remapModJar(input, config, deps = {}) {
|
|
76
|
+
const remapJarFn = deps.remapJar ?? remapJar;
|
|
77
|
+
const resolveTinyMappingFileFn = deps.resolveTinyMappingFile ?? resolveTinyMappingFile;
|
|
78
|
+
const resolveMojangTinyFileFn = deps.resolveMojangTinyFile ?? resolveMojangTinyFile;
|
|
79
|
+
const resolveTinyRemapperJarFn = deps.resolveTinyRemapperJar ?? resolveTinyRemapperJar;
|
|
69
80
|
const startedAt = Date.now();
|
|
70
81
|
const warnings = [];
|
|
71
82
|
// 1. Normalize input JAR path
|
|
@@ -111,12 +122,33 @@ export async function remapModJar(input, config) {
|
|
|
111
122
|
});
|
|
112
123
|
}
|
|
113
124
|
const outputJar = resolveOutputJarPath(input, normalizedInput, analysis.modId, analysis.modVersion);
|
|
114
|
-
// 4.
|
|
115
|
-
|
|
125
|
+
// 4. Resolve the mapping identity BEFORE the cache key so the key reflects
|
|
126
|
+
// which mappings produced the jar. Yarn builds are mutable (Fabric publishes
|
|
127
|
+
// new builds per MC version), so the resolved yarn coordinate is the identity;
|
|
128
|
+
// mojang is immutable per release so its version-keyed path string suffices.
|
|
129
|
+
// The yarn resolution is TTL-fenced (see resolveTinyMappingFile), so on the
|
|
130
|
+
// common warm path this is zero-network and stays off the cache-hit fast path.
|
|
131
|
+
let mappingIdentity;
|
|
132
|
+
let resolvedYarnTiny;
|
|
133
|
+
if (fromNamespace === resolvedTargetNamespace) {
|
|
134
|
+
// No remap will run (input already in target namespace); the output is a
|
|
135
|
+
// copy of the input, so no mapping identity is involved.
|
|
136
|
+
mappingIdentity = `copy:${resolvedTargetNamespace}`;
|
|
137
|
+
}
|
|
138
|
+
else if (resolvedTargetNamespace === "yarn") {
|
|
139
|
+
resolvedYarnTiny = await resolveTinyMappingFileFn(mcVersion, "yarn", config.cacheDir, undefined, {
|
|
140
|
+
forceRefresh: input.forceRemap
|
|
141
|
+
});
|
|
142
|
+
mappingIdentity = resolvedYarnTiny.coordinate ?? resolvedYarnTiny.path;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
mappingIdentity = `${mcVersion}-mojang-merged.tiny`;
|
|
146
|
+
}
|
|
147
|
+
const cacheKey = buildCacheKey(normalizedInput, fromNamespace, resolvedTargetNamespace, mcVersion, mappingIdentity);
|
|
116
148
|
const cacheDir = join(config.cacheDir, "remapped-mods");
|
|
117
149
|
mkdirSync(cacheDir, { recursive: true });
|
|
118
150
|
const cachedOutput = join(cacheDir, `${cacheKey}.jar`);
|
|
119
|
-
if (existsSync(cachedOutput)) {
|
|
151
|
+
if (!input.forceRemap && existsSync(cachedOutput)) {
|
|
120
152
|
const cacheHitOutputJar = input.outputJar
|
|
121
153
|
? outputJar
|
|
122
154
|
: cachedOutput;
|
|
@@ -161,57 +193,81 @@ export async function remapModJar(input, config) {
|
|
|
161
193
|
});
|
|
162
194
|
}
|
|
163
195
|
// 5. Resolve tiny-remapper
|
|
164
|
-
const tinyRemapperJar = await
|
|
165
|
-
|
|
166
|
-
let mappingsFile;
|
|
167
|
-
let toNamespace;
|
|
196
|
+
const tinyRemapperJar = await resolveTinyRemapperJarFn(config.cacheDir, config.tinyRemapperJarPath);
|
|
197
|
+
const passes = [];
|
|
168
198
|
if (resolvedTargetNamespace === "yarn") {
|
|
169
|
-
|
|
170
|
-
|
|
199
|
+
// The Fabric yarn v2 tiny declares both intermediary and named, so a single
|
|
200
|
+
// intermediary -> named pass is sufficient.
|
|
201
|
+
// Reuse the coordinate resolved for the cache key (already TTL-fenced).
|
|
202
|
+
const yarnTiny = resolvedYarnTiny ?? await resolveTinyMappingFileFn(mcVersion, "yarn", config.cacheDir, undefined, {
|
|
203
|
+
forceRefresh: input.forceRemap
|
|
204
|
+
});
|
|
205
|
+
passes.push({ mappingsFile: yarnTiny.path, fromNamespace, toNamespace: "named" });
|
|
171
206
|
}
|
|
172
207
|
else {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
208
|
+
// Mojang target. The merged mojang tiny only bridges obfuscated <-> mojang,
|
|
209
|
+
// but a Fabric/Quilt jar is intermediary-named, so a direct intermediary ->
|
|
210
|
+
// mojang remap finds no matching keys and silently no-ops. Remap in two passes:
|
|
211
|
+
// pass 1: intermediary -> official (= obfuscated) via the Fabric intermediary v2 tiny
|
|
212
|
+
// pass 2: obfuscated -> mojang via the merged mojang tiny
|
|
213
|
+
const mojangTiny = await resolveMojangTinyFileFn(mcVersion, config);
|
|
176
214
|
warnings.push(...mojangTiny.warnings);
|
|
215
|
+
if (fromNamespace === "intermediary") {
|
|
216
|
+
const intermediaryTiny = await resolveTinyMappingFileFn(mcVersion, "intermediary", config.cacheDir);
|
|
217
|
+
passes.push({ mappingsFile: intermediaryTiny.path, fromNamespace: "intermediary", toNamespace: "official" });
|
|
218
|
+
passes.push({ mappingsFile: mojangTiny.path, fromNamespace: "obfuscated", toNamespace: "mojang" });
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// Defensive: an already-obfuscated input maps directly. (mojang -> mojang is
|
|
222
|
+
// handled earlier by the copy short-circuit, so this is not normally reached.)
|
|
223
|
+
passes.push({ mappingsFile: mojangTiny.path, fromNamespace: "obfuscated", toNamespace: "mojang" });
|
|
224
|
+
}
|
|
177
225
|
}
|
|
178
226
|
mkdirSync(dirname(outputJar), { recursive: true });
|
|
179
|
-
//
|
|
180
|
-
const tempDir = join(tmpdir(), `mcp-remap-${cacheKey.slice(0, 12)}
|
|
181
|
-
mkdirSync(tempDir, { recursive: true });
|
|
227
|
+
// 7. Use a temporary directory for intermediate work
|
|
228
|
+
const tempDir = mkdtempSync(join(tmpdir(), `mcp-remap-${cacheKey.slice(0, 12)}-`));
|
|
182
229
|
try {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
throw createError({
|
|
198
|
-
code: caughtError.code,
|
|
199
|
-
message: caughtError.message,
|
|
200
|
-
details: {
|
|
201
|
-
...(caughtError.details ?? {}),
|
|
202
|
-
fromMapping: fromNamespace,
|
|
203
|
-
targetMapping: input.targetMapping,
|
|
204
|
-
resolvedTargetNamespace
|
|
205
|
-
}
|
|
230
|
+
let passInput = normalizedInput;
|
|
231
|
+
let tempOutput = "";
|
|
232
|
+
for (let passIndex = 0; passIndex < passes.length; passIndex += 1) {
|
|
233
|
+
const pass = passes[passIndex];
|
|
234
|
+
tempOutput = join(tempDir, `pass-${passIndex}.jar`);
|
|
235
|
+
try {
|
|
236
|
+
await remapJarFn(tinyRemapperJar, {
|
|
237
|
+
inputJar: passInput,
|
|
238
|
+
outputJar: tempOutput,
|
|
239
|
+
mappingsFile: pass.mappingsFile,
|
|
240
|
+
fromNamespace: pass.fromNamespace,
|
|
241
|
+
toNamespace: pass.toNamespace,
|
|
242
|
+
timeoutMs: config.remapTimeoutMs,
|
|
243
|
+
maxMemoryMb: config.remapMaxMemoryMb
|
|
206
244
|
});
|
|
207
245
|
}
|
|
208
|
-
|
|
246
|
+
catch (caughtError) {
|
|
247
|
+
if (isAppError(caughtError)) {
|
|
248
|
+
throw createError({
|
|
249
|
+
code: caughtError.code,
|
|
250
|
+
message: caughtError.message,
|
|
251
|
+
details: {
|
|
252
|
+
...(caughtError.details ?? {}),
|
|
253
|
+
fromMapping: fromNamespace,
|
|
254
|
+
targetMapping: input.targetMapping,
|
|
255
|
+
resolvedTargetNamespace,
|
|
256
|
+
pass: `${pass.fromNamespace}->${pass.toNamespace}`
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
throw caughtError;
|
|
261
|
+
}
|
|
262
|
+
passInput = tempOutput;
|
|
209
263
|
}
|
|
210
264
|
// Copy to final destination and cache
|
|
211
265
|
copyFileSync(tempOutput, outputJar);
|
|
212
266
|
if (outputJar !== cachedOutput) {
|
|
213
267
|
mkdirSync(dirname(cachedOutput), { recursive: true });
|
|
214
|
-
|
|
268
|
+
const tempCachedOutput = `${cachedOutput}.${randomBytes(4).toString("hex")}.tmp`;
|
|
269
|
+
copyFileSync(tempOutput, tempCachedOutput);
|
|
270
|
+
renameSync(tempCachedOutput, cachedOutput);
|
|
215
271
|
}
|
|
216
272
|
const durationMs = Date.now() - startedAt;
|
|
217
273
|
log("info", "remap.pipeline.done", {
|