diff --git a/package-lock.json b/package-lock.json index c4bf322..37a614a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "dependencies": { "@katana-project/zip": "^0.7.1", "@monaco-editor/react": "^4.7.0", - "@run-slicer/vf": "^0.5.0-1.11.2", "@xyflow/react": "^12.10.1", "antd": "^6.3.2", "comlink": "^4.4.2", @@ -19,7 +18,9 @@ "monaco-editor": "^0.55.1", "react": "^19.2.4", "react-dom": "^19.2.4", - "rxjs": "^7.8.2" + "rxjs": "^7.8.2", + "vf-1.11.2": "npm:@run-slicer/vf@^0.5.0-1.11.2", + "vf-1.12.0": "npm:@run-slicer/vf@^0.6.3-1.12.0" }, "devDependencies": { "@playwright/test": "^1.58.2", @@ -162,7 +163,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -3060,12 +3060,6 @@ } } }, - "node_modules/@run-slicer/vf": { - "version": "0.5.0-1.11.2", - "resolved": "https://registry.npmjs.org/@run-slicer/vf/-/vf-0.5.0-1.11.2.tgz", - "integrity": "sha512-8+otfmuAuuEl1bo6LPp+qeR2TpY5pCccLrONK6/reJpCWdrU+g9++j4Eec1fqqv/DRX7nB79Xp9ZwaETR1mhXA==", - "license": "Apache License 2.0, MIT" - }, "node_modules/@sindresorhus/is": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", @@ -3262,7 +3256,6 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3417,7 +3410,6 @@ "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -3428,7 +3420,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3772,7 +3763,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3979,7 +3969,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -4042,8 +4031,7 @@ "version": "1.11.20", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", @@ -4153,7 +4141,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -4715,7 +4702,6 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", - "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -4827,7 +4813,6 @@ "integrity": "sha512-4RuJK2jP08XwqtUu+5yhCbxEauCm6tv2MFHKEMsjbosK2+vy5us82oI3VLuHwbNyZG7ekZA26U2LLHnGR4frIA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "tsgolint": "bin/tsgolint.js" }, @@ -4982,7 +4967,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4992,7 +4976,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5294,7 +5277,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5326,7 +5308,6 @@ "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "pathe": "^2.0.3" } @@ -5371,13 +5352,26 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/vf-1.11.2": { + "name": "@run-slicer/vf", + "version": "0.5.0-1.11.2", + "resolved": "https://registry.npmjs.org/@run-slicer/vf/-/vf-0.5.0-1.11.2.tgz", + "integrity": "sha512-8+otfmuAuuEl1bo6LPp+qeR2TpY5pCccLrONK6/reJpCWdrU+g9++j4Eec1fqqv/DRX7nB79Xp9ZwaETR1mhXA==", + "license": "Apache License 2.0, MIT" + }, + "node_modules/vf-1.12.0": { + "name": "@run-slicer/vf", + "version": "0.6.3-1.12.0", + "resolved": "https://registry.npmjs.org/@run-slicer/vf/-/vf-0.6.3-1.12.0.tgz", + "integrity": "sha512-Sz0HU2ScV1c1rs0Wx4nfhAiigXw08Mvydc3R2kROoIN6iVyAZAr1rNJpWMXabbTfxtu9TmPr9YdByl5aK/PQWQ==", + "license": "Apache License 2.0, MIT" + }, "node_modules/vite": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", @@ -5597,7 +5591,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "bin": { "workerd": "bin/workerd" }, diff --git a/package.json b/package.json index b45ecf3..029728f 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "dependencies": { "@katana-project/zip": "^0.7.1", "@monaco-editor/react": "^4.7.0", - "@run-slicer/vf": "^0.5.0-1.11.2", + "vf-1.11.2": "npm:@run-slicer/vf@^0.5.0-1.11.2", + "vf-1.12.0": "npm:@run-slicer/vf@^0.6.3-1.12.0", "@xyflow/react": "^12.10.1", "antd": "^6.3.2", "comlink": "^4.4.2", diff --git a/src/logic/Decompiler.ts b/src/logic/Decompiler.ts index afd4970..d69e994 100644 --- a/src/logic/Decompiler.ts +++ b/src/logic/Decompiler.ts @@ -4,9 +4,9 @@ import { combineLatest, distinctUntilChanged, from, map, Observable, of, shareReplay, switchMap, tap, throttleTime } from "rxjs"; import { minecraftJar, type MinecraftJar } from "./MinecraftApi"; -import { selectedFile } from "./State"; +import { selectedFile, vineflowerVersion } from "./State"; import { bytecode, displayLambdas } from "./Settings"; -import type { Options } from "./vf"; +import type { Options } from "./vineflower/vineflower"; import type { DecompileResult } from "../workers/decompile/types"; import * as worker from "../workers/decompile/client"; import type { Jar } from "../utils/Jar"; @@ -71,7 +71,7 @@ export async function getClassBytecode(className: string, jar: Jar) { export async function decompileClass(className: string, jar: Jar) { try { decompilerCounter.next(decompilerCounter.value + 1); - return await worker.decompileClass(className, jar); + return await worker.decompileClass(className, jar, vineflowerVersion.value); } finally { decompilerCounter.next(decompilerCounter.value - 1); } diff --git a/src/logic/Permalink.test.ts b/src/logic/Permalink.test.ts index 7037346..51f5ad8 100644 --- a/src/logic/Permalink.test.ts +++ b/src/logic/Permalink.test.ts @@ -151,7 +151,7 @@ describe('Permalink', () => { it('should parse a diff permalink', () => { const state = parsePathToState('1/diff/1.21/1.21.4/net/minecraft/ChatFormatting')!; - expect(state.version).toBe(1); + expect(state.version).toBe(2); expect(state.minecraftVersion).toBe('1.21.4'); expect(state.file).toBe('net/minecraft/ChatFormatting.class'); expect(state.selectedLines).toBe(null); @@ -179,7 +179,7 @@ describe('Permalink', () => { it('should parse a diff permalink without a file', () => { const state = parsePathToState('1/diff/1.21/1.21.4')!; - expect(state.version).toBe(1); + expect(state.version).toBe(2); expect(state.minecraftVersion).toBe('1.21.4'); expect(state.file).toBeUndefined(); expect(state.selectedLines).toBe(null); @@ -197,6 +197,44 @@ describe('Permalink', () => { }); }); + describe('Version 2 URLs (/2/)', () => { + it('should parse a /2/ permalink with version 2', () => { + const state = parsePathToState('2/1.21/net/minecraft/ChatFormatting')!; + + expect(state.version).toBe(2); + expect(state.minecraftVersion).toBe('1.21'); + expect(state.file).toBe('net/minecraft/ChatFormatting.class'); + expect(state.selectedLines).toBe(null); + }); + + it('should parse a /2/ permalink with line numbers', () => { + const state = parsePathToState('2/1.21.4/net/minecraft/server/MinecraftServer#L100-200')!; + + expect(state.version).toBe(2); + expect(state.minecraftVersion).toBe('1.21.4'); + expect(state.file).toBe('net/minecraft/server/MinecraftServer.class'); + expect(state.selectedLines).toEqual({ line: 100, lineEnd: 200 }); + }); + + it('should parse a /2/ diff permalink', () => { + const state = parsePathToState('2/diff/1.21/1.21.4/net/minecraft/ChatFormatting')!; + + expect(state.version).toBe(2); + expect(state.minecraftVersion).toBe('1.21.4'); + expect(state.file).toBe('net/minecraft/ChatFormatting.class'); + expect(state.diff).toEqual({ leftMinecraftVersion: '1.21' }); + }); + + it('should parse a /2/ diff permalink without a file', () => { + const state = parsePathToState('2/diff/1.21/1.21.4')!; + + expect(state.version).toBe(2); + expect(state.minecraftVersion).toBe('1.21.4'); + expect(state.file).toBeUndefined(); + expect(state.diff).toEqual({ leftMinecraftVersion: '1.21' }); + }); + }); + describe('Real-world Examples', () => { it('should parse multiline permalink', () => { const state = parsePathToState('1/1.21.4/net/minecraft/server/MinecraftServer#L250-260')!; @@ -213,7 +251,7 @@ describe('Permalink', () => { it('should parse a real-world diff permalink', () => { const state = parsePathToState('1/diff/1.21.4/1.21.5/net/minecraft/server/MinecraftServer')!; - expect(state.version).toBe(1); + expect(state.version).toBe(2); expect(state.minecraftVersion).toBe('1.21.5'); expect(state.file).toBe('net/minecraft/server/MinecraftServer.class'); expect(state.diff).toEqual({ leftMinecraftVersion: '1.21.4' }); diff --git a/src/logic/Permalink.ts b/src/logic/Permalink.ts index fba5c46..c2e3a87 100644 --- a/src/logic/Permalink.ts +++ b/src/logic/Permalink.ts @@ -1,6 +1,7 @@ import { combineLatest } from "rxjs"; import { resetPermalinkAffectingSettings, supportsPermalinking } from "./Settings"; -import { diffLeftSelectedMinecraftVersion, diffView, selectedFile, selectedLines, selectedMinecraftVersion } from "./State"; +import { diffLeftSelectedMinecraftVersion, diffView, vineflowerVersion, selectedFile, selectedLines, selectedMinecraftVersion } from "./State"; +import { vineflowerVersionToPermalinkVersion } from "./vineflower/versions"; export interface State { version: number; // Allows us to change the permalink structure in the future @@ -16,7 +17,7 @@ export interface State { } const DEFAULT_STATE: State = { - version: 0, + version: 2, minecraftVersion: "", file: undefined, selectedLines: null @@ -51,7 +52,7 @@ export const parsePathToState = (path: string): State | null => { const rightMinecraftVersion = decodeURIComponent(segments[3]); const filePath = segments.slice(4).join('/'); return { - version, + version: DEFAULT_STATE.version, // The diff format didnt change from /1/ to /2/, so we can just blindly upgrade all diff permalinks to the new decompiler. minecraftVersion: rightMinecraftVersion, file: filePath ? filePath + (filePath.endsWith('.class') ? '' : '.class') : undefined, selectedLines: null, @@ -117,14 +118,16 @@ if (typeof window !== "undefined") { selectedFile, selectedLines, supportsPermalinking, - diffView + diffView, + vineflowerVersion ]).subscribe(([ minecraftVersion, diffLeftMinecraftVersion, file, selectedLines, supported, - diffView + diffView, + vineflowerVersion ]) => { if (!file && !diffView) { document.title = "mcsrc.dev"; @@ -146,7 +149,7 @@ if (typeof window !== "undefined") { return; } - let url = '/1/'; + let url = `/${vineflowerVersionToPermalinkVersion(vineflowerVersion)}/`; if (diffView) { url += `diff/${diffLeftMinecraftVersion}/${minecraftVersion}`; diff --git a/src/logic/State.ts b/src/logic/State.ts index d06b158..2130c1e 100644 --- a/src/logic/State.ts +++ b/src/logic/State.ts @@ -2,6 +2,7 @@ import { BehaviorSubject } from "rxjs"; import { pairwise } from "rxjs/operators"; import { Tab, CodeTab } from "./tabs"; import { getInitialState } from "./Permalink"; +import { DEFAULT_VERSION, getVersionFromPermalink, type Version } from "./vineflower/versions"; const initialState = getInitialState(); @@ -17,6 +18,7 @@ export const openTabs = new BehaviorSubject(initialTab ? [initialTab] : [ export const tabHistory = new BehaviorSubject(initialState.file ? [initialState.file] : []); export const searchQuery = new BehaviorSubject(""); export const referencesQuery = new BehaviorSubject(""); +export const vineflowerVersion = new BehaviorSubject(getVersionFromPermalink(initialState.version)); export interface SelectedLines { line: number; @@ -28,8 +30,10 @@ export const diffView = new BehaviorSubject(!!initialState.diff); export const diffLeftSelectedMinecraftVersion = new BehaviorSubject(initialState.diff?.leftMinecraftVersion ?? null); // Reset selected lines when file changes (skip initial emission to preserve permalink selection) +// Also reset the permalink version back to the latest selectedFile.pipe(pairwise()).subscribe(([previousFile, currentFile]) => { if (previousFile !== currentFile) { selectedLines.next(null); + vineflowerVersion.next(DEFAULT_VERSION); } }); diff --git a/src/logic/vf.ts b/src/logic/vf.ts deleted file mode 100644 index adc596e..0000000 --- a/src/logic/vf.ts +++ /dev/null @@ -1,36 +0,0 @@ -import wasmPath from "@run-slicer/vf/vf.wasm?url"; -import { load } from "@run-slicer/vf/vf.wasm-runtime.js"; -import type * as vf from "@run-slicer/vf"; - -export type * from "@run-slicer/vf"; - -let runtime: typeof vf | null = null; -let runtimePreferWasm = true; - -export async function loadRuntime(preferWasm: boolean) { - if (!runtime || runtimePreferWasm !== preferWasm) { - runtimePreferWasm = preferWasm; - console.log(`Loading VineFlower ${preferWasm ? "WASM" : "JavaScript"} runtime`); - - let loadJs = !preferWasm; - if (preferWasm) { - try { - const { exports } = await load(wasmPath, { noAutoImports: true }); - runtime = exports; - loadJs = false; - } catch (e) { - console.warn("Failed to load WASM module (non-compliant browser?), falling back to JS implementation", e); - loadJs = true; - } - } - - if (loadJs) { - runtime = await import("@run-slicer/vf/vf.runtime.js"); - } - } -} - -export const decompile: typeof vf.decompile = async (name, options) => { - if (!runtime) throw "No runtime loaded"; - return await runtime.decompile(name, options); -}; diff --git a/src/logic/vineflower/versions.ts b/src/logic/vineflower/versions.ts new file mode 100644 index 0000000..fb997c3 --- /dev/null +++ b/src/logic/vineflower/versions.ts @@ -0,0 +1,22 @@ +export const DEFAULT_VERSION = "1.12.0"; +export type Version = "1.11.2" | "1.12.0"; + +export function getVersionFromPermalink(permalinkVersion: number): Version { + switch (permalinkVersion) { + case 1: + return "1.11.2"; + case 2: + return "1.12.0"; + default: + return DEFAULT_VERSION; + } +} + +export function vineflowerVersionToPermalinkVersion(vineflowerVersion: Version): number { + switch (vineflowerVersion) { + case "1.11.2": + return 1; + case "1.12.0": + return 2; + } +} \ No newline at end of file diff --git a/src/logic/vineflower/vf-1.11.2.ts b/src/logic/vineflower/vf-1.11.2.ts new file mode 100644 index 0000000..54701a7 --- /dev/null +++ b/src/logic/vineflower/vf-1.11.2.ts @@ -0,0 +1,43 @@ +import wasmPath from "vf-1.11.2/vf.wasm?url"; +import { load } from "vf-1.11.2/vf.wasm-runtime.js"; +import type * as vf from "vf-1.11.2"; + +let runtime: typeof vf | null = null; +let runtimePreferWasm = true; + +export async function loadRuntime(preferWasm: boolean) { + if (!runtime || runtimePreferWasm !== preferWasm) { + console.log(`Loading VineFlower 1.11.2 ${preferWasm ? "WASM" : "JavaScript"} runtime (previous: ${runtime ? (runtimePreferWasm ? "WASM" : "JS") : "none"})`); + + if (preferWasm) { + try { + const { exports } = await load(wasmPath, { noAutoImports: true }); + runtime = exports; + runtimePreferWasm = preferWasm; + console.log("VineFlower 1.11.2 WASM runtime loaded successfully"); + return; + } catch (e) { + console.warn("Failed to load WASM module (non-compliant browser?), falling back to JS implementation", e); + if (runtime) { + console.log("VineFlower 1.11.2 keeping existing JS runtime"); + return; + } + } + } + + try { + runtime = await import("vf-1.11.2/vf.runtime.js"); + console.log("VineFlower 1.11.2 JS runtime loaded successfully"); + } catch (e) { + throw new Error(`Failed to load JS runtime: ${e}`); + } + runtimePreferWasm = preferWasm; + } else { + console.log(`VineFlower 1.11.2 reusing existing ${runtimePreferWasm ? "WASM" : "JS"} runtime`); + } +} + +export const decompile: typeof vf.decompile = async (name: string | string[], options?: vf.Config) => { + if (!runtime) throw new Error("No runtime loaded"); + return await runtime.decompile(name, options); +}; diff --git a/src/logic/vineflower/vf-1.12.0.ts b/src/logic/vineflower/vf-1.12.0.ts new file mode 100644 index 0000000..78e1cbb --- /dev/null +++ b/src/logic/vineflower/vf-1.12.0.ts @@ -0,0 +1,43 @@ +import wasmPath from "vf-1.12.0/vf.wasm?url"; +import { load } from "vf-1.12.0/vf.wasm-runtime.js"; +import type * as vf from "vf-1.12.0"; + +let runtime: typeof vf | null = null; +let runtimePreferWasm = true; + +export async function loadRuntime(preferWasm: boolean) { + if (!runtime || runtimePreferWasm !== preferWasm) { + console.log(`Loading VineFlower 1.12.0 ${preferWasm ? "WASM" : "JavaScript"} runtime (previous: ${runtime ? (runtimePreferWasm ? "WASM" : "JS") : "none"})`); + + if (preferWasm) { + try { + const { exports } = await load(wasmPath, { noAutoImports: true }); + runtime = exports; + runtimePreferWasm = preferWasm; + console.log("VineFlower 1.12.0 WASM runtime loaded successfully"); + return; + } catch (e) { + console.warn("Failed to load WASM module (non-compliant browser?), falling back to JS implementation", e); + if (runtime) { + console.log("VineFlower 1.12.0 keeping existing JS runtime"); + return; + } + } + } + + try { + runtime = await import("vf-1.12.0/vf.runtime.js"); + console.log("VineFlower 1.12.0 JS runtime loaded successfully"); + } catch (e) { + throw new Error(`Failed to load JS runtime: ${e}`); + } + runtimePreferWasm = preferWasm; + } else { + console.log(`VineFlower 1.12.0 reusing existing ${runtimePreferWasm ? "WASM" : "JS"} runtime`); + } +} + +export const decompile: typeof vf.decompile = async (name: string | string[], options?: vf.Config) => { + if (!runtime) throw new Error("No runtime loaded"); + return await runtime.decompile(name, options); +}; diff --git a/src/logic/vineflower/vineflower.ts b/src/logic/vineflower/vineflower.ts new file mode 100644 index 0000000..e4c1957 --- /dev/null +++ b/src/logic/vineflower/vineflower.ts @@ -0,0 +1,62 @@ +import { type Version, DEFAULT_VERSION } from "./versions"; + +export async function loadRuntime(preferWasm: boolean, version: Version) { + if (version === "1.11.2") { + const vf = await import("./vf-1.11.2"); + return await vf.loadRuntime(preferWasm); + } else if (version === "1.12.0") { + const vf = await import("./vf-1.12.0"); + return await vf.loadRuntime(preferWasm); + } + + throw new Error(`Unsupported Vineflower version: ${version}`); +} + +export async function decompile(version: Version, name: string | string[], options?: Config) { + if (version === "1.11.2") { + const vf = await import("./vf-1.11.2"); + return await vf.decompile(name, options); + } else if (version === "1.12.0") { + const vf = await import("./vf-1.12.0"); + return await vf.decompile(name, options); + } + + throw new Error(`Unsupported Vineflower version: ${version}`); +} + +// Abstracted types from @run-slicer/vf to support multiple versions. +export type Options = Record; + +export interface TokenCollector { + start: (content: string) => void; + visitClass: (start: number, length: number, declaration: boolean, name: string) => void; + visitField: (start: number, length: number, declaration: boolean, className: string, name: string, descriptor: string) => void; + visitMethod: (start: number, length: number, declaration: boolean, className: string, name: string, descriptor: string) => void; + visitParameter: (start: number, length: number, declaration: boolean, className: string, methodName: string, methodDescriptor: string, index: number, name: string) => void; + visitLocal: (start: number, length: number, declaration: boolean, className: string, methodName: string, methodDescriptor: string, index: number, name: string) => void; + end: () => void; +} + +export type LogLevel = "trace" | "info" | "warn" | "error"; + +export interface Logger { + writeMessage: (level: LogLevel, message: string, error?: unknown) => void; + startProcessingClass?: (className: string) => void; + endProcessingClass?: () => void; + startReadingClass?: (className: string) => void; + endReadingClass?: () => void; + startClass?: (className: string) => void; + endClass?: () => void; + startMethod?: (methodName: string) => void; + endMethod?: () => void; + startWriteClass?: (className: string) => void; + endWriteClass?: () => void; +} + +export interface Config { + source?: (name: string) => Promise; + resources?: string[]; + options?: Options; + tokenCollector?: TokenCollector; + logger?: Logger; +} \ No newline at end of file diff --git a/src/types/vf-runtime.d.ts b/src/types/vf-runtime.d.ts index 00f472f..d8bd746 100644 --- a/src/types/vf-runtime.d.ts +++ b/src/types/vf-runtime.d.ts @@ -1,10 +1,29 @@ -declare module "@run-slicer/vf/vf.wasm-runtime.js" { +declare module "vf-1.11.2/vf.wasm-runtime.js" { export function load( wasmPath: string, options?: { noAutoImports?: boolean; } ): Promise<{ exports: typeof import("@run-slicer/vf"); }>; } -declare module "@run-slicer/vf/vf.runtime.js" { +declare module "vf-1.11.2/vf.runtime.js" { export * from "@run-slicer/vf"; } + +declare module "vf-1.11.2" { + export * from "@run-slicer/vf"; +} + +declare module "vf-1.12.0/vf.wasm-runtime.js" { + export function load( + wasmPath: string, + options?: { noAutoImports?: boolean; } + ): Promise<{ exports: typeof import("@run-slicer/vf"); }>; +} + +declare module "vf-1.12.0/vf.runtime.js" { + export * from "@run-slicer/vf"; +} + +declare module "vf-1.12.0" { + export * from "@run-slicer/vf"; +} \ No newline at end of file diff --git a/src/ui/DecompilerVersionWarning.tsx b/src/ui/DecompilerVersionWarning.tsx new file mode 100644 index 0000000..26e7d93 --- /dev/null +++ b/src/ui/DecompilerVersionWarning.tsx @@ -0,0 +1,32 @@ +import { theme, Tooltip } from "antd"; +import { useObservable } from "../utils/UseObservable"; +import { vineflowerVersion } from "../logic/State"; +import { DEFAULT_VERSION } from "../logic/vineflower/versions"; + +export const DecompilerVersionWarning = () => { + const { token } = theme.useToken(); + const version = useObservable(vineflowerVersion); + const isNonDefaultVersion = version !== DEFAULT_VERSION; + + if (!isNonDefaultVersion) return null; + + return ( + +
+ Note: Using legacy decompiler (Vineflower {version}) +
+
+ ); +}; diff --git a/src/ui/FileList.tsx b/src/ui/FileList.tsx index 5d46005..5a6f915 100644 --- a/src/ui/FileList.tsx +++ b/src/ui/FileList.tsx @@ -14,6 +14,7 @@ import { selectedFile, referencesQuery } from '../logic/State'; import { compactPackages } from '../logic/Settings'; import { jarIndex, type ClassData } from '../workers/jar-index/client'; import { ClassDataIcon, JavaIcon, PackageIcon } from './intellij-icons'; +import { DEFAULT_VERSION, vineflowerVersionToPermalinkVersion } from '../logic/vineflower/versions'; const classData: Observable | null> = jarIndex.pipe( switchMap(jarIndex => from(jarIndex.getClassData()).pipe( @@ -151,7 +152,7 @@ const getMenuItems = ( const packagePath = path.replace(/\//g, '.').replace('.class', ''); const filename = path.split('/').pop() || ''; const linkPath = path.replace('.class', ''); - const link = jar ? `https://mcsrc.dev/1/${jar.version}/${linkPath}` : ''; + const link = jar ? `https://mcsrc.dev/${vineflowerVersionToPermalinkVersion(DEFAULT_VERSION)}/${jar.version}/${linkPath}` : ''; const renderLabel = (title: string, value: string) => (
diff --git a/src/ui/FilepathHeader.tsx b/src/ui/FilepathHeader.tsx index 2b1d399..88d5218 100644 --- a/src/ui/FilepathHeader.tsx +++ b/src/ui/FilepathHeader.tsx @@ -3,6 +3,7 @@ import { useObservable } from "../utils/UseObservable"; import { getDiffChanges } from "../logic/Diff"; import { combineLatest, map } from "rxjs"; import { selectedFile, diffView } from "../logic/State"; +import { DecompilerVersionWarning } from "./DecompilerVersionWarning"; const changeInfoObs = combineLatest([selectedFile, getDiffChanges(), diffView]).pipe( map(([file, changes, isDiff]) => { @@ -22,34 +23,42 @@ export const FilepathHeader = () => { width: "100%", boxSizing: "border-box", alignItems: "center", - justifyContent: "left", + justifyContent: "space-between", padding: ".25rem 1rem", fontFamily: token.fontFamily, }}>
- {info.replace(".class", "").split("/").map((path, i, arr) => ( - - {path} - {i < arr.length - 1 && /} - - ))} -
- {changeInfo && ( -
- {changeInfo.deletions !== undefined && changeInfo.deletions > 0 && ( - -{changeInfo.deletions} - )} - {changeInfo.additions !== undefined && changeInfo.additions > 0 && ( - +{changeInfo.additions} - )} +
+ {info.replace(".class", "").split("/").map((path, i, arr) => ( + + {path} + {i < arr.length - 1 && /} + + ))}
- )} + {changeInfo && ( +
+ {changeInfo.deletions !== undefined && changeInfo.deletions > 0 && ( + -{changeInfo.deletions} + )} + {changeInfo.additions !== undefined && changeInfo.additions > 0 && ( + +{changeInfo.additions} + )} +
+ )} +
+
); }; diff --git a/src/ui/JarDecompilerModal.tsx b/src/ui/JarDecompilerModal.tsx index 949ce7e..30bb930 100644 --- a/src/ui/JarDecompilerModal.tsx +++ b/src/ui/JarDecompilerModal.tsx @@ -6,6 +6,7 @@ import { BooleanOption, NumberOption } from "./SettingsModal"; import { decompilerSplits, decompilerThreads, MAX_THREADS, preferWasmDecompiler } from "../logic/Settings"; import { decompileEntireJar, deleteCache, type DecompileEntireJarTask } from "../workers/decompile/client"; import { minecraftJar } from "../logic/MinecraftApi"; +import { DEFAULT_VERSION } from "../logic/vineflower/versions"; const modalOpen = new BehaviorSubject(false); @@ -26,7 +27,7 @@ export const JarDecompilerModal = () => { modalOpen.next(false); if (!jar) return; - const task = decompileEntireJar(jar.jar, { + const task = decompileEntireJar(jar.jar, DEFAULT_VERSION, { threads: decompilerThreads.value, splits: decompilerSplits.value, logger(progress, current, total) { diff --git a/src/workers/decompile/client.ts b/src/workers/decompile/client.ts index 4c63b74..f1bc7dd 100644 --- a/src/workers/decompile/client.ts +++ b/src/workers/decompile/client.ts @@ -1,8 +1,9 @@ import * as Comlink from "comlink"; -import type * as vf from "../../logic/vf"; +import type * as vf from "../../logic/vineflower/vineflower"; import { DecompileJar, type DecompileResult } from "./types"; import type { Jar } from "../../utils/Jar"; import type { DecompileWorker } from "./worker"; +import { DEFAULT_VERSION, type Version } from "../../logic/vineflower/versions"; function createWorker() { const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module", name: "decompiler" }); @@ -13,6 +14,7 @@ type WorkerInstance = ReturnType; const MAX_THREADS = navigator.hardwareConcurrency || 4; let workers: WorkerInstance[] = []; let preferWasmRuntime = true; +let version: Version = DEFAULT_VERSION; async function ensureWorkers(count: number) { count = Math.min(count, MAX_THREADS); @@ -22,7 +24,7 @@ async function ensureWorkers(count: number) { { length: count - workers.length }, () => createWorker()); - await Promise.all(newWorkers.map(w => w.loadVFRuntime(preferWasmRuntime))); + await Promise.all(newWorkers.map(w => w.loadVFRuntime(preferWasmRuntime, version))); workers.push(...newWorkers); } @@ -48,6 +50,13 @@ export async function setRuntime(preferWasm: boolean) { workers = []; } +async function setVersion(newVersion: Version) { + if (version === newVersion) return; + version = newVersion; + await Promise.all(workers.map(w => w.scheduleClose())); + workers = []; +} + export async function setOptions(options: vf.Options) { const sab = new SharedArrayBuffer(Uint32Array.BYTES_PER_ELEMENT); const state = new Uint32Array(sab); @@ -72,7 +81,7 @@ export type DecompileEntireJarTask = { stop: () => void; }; -export function decompileEntireJar(jar: Jar, options?: DecompileEntireJarOptions): DecompileEntireJarTask { +export function decompileEntireJar(jar: Jar, version: Version, options?: DecompileEntireJarOptions): DecompileEntireJarTask { const sab = new SharedArrayBuffer(Uint32Array.BYTES_PER_ELEMENT); const state = new Uint32Array(sab); state[0] = 0; @@ -92,6 +101,7 @@ export function decompileEntireJar(jar: Jar, options?: DecompileEntireJarOptions options.logger!(classNames[i], ++current, classNames.length); }) : undefined; + await setVersion(version); await ensureWorkers(optThreads); const result = await Promise.all((workers .slice(0, optThreads)) @@ -109,7 +119,7 @@ export function decompileEntireJar(jar: Jar, options?: DecompileEntireJarOptions }; } -export async function decompileClass(className: string, jar: Jar): Promise { +export async function decompileClass(className: string, jar: Jar, version: Version): Promise { className = className.replace(".class", ""); const entry = jar.entries[`${className}.class`]; @@ -119,8 +129,10 @@ export async function decompileClass(className: string, jar: Jar): Promise | undefined = undefined; #promiseCount = 0; + #version: Version; promiseCount = () => this.#promiseCount; async schedule(fn: () => Promise): Promise { @@ -27,14 +29,16 @@ export class DecompileWorker { db = new Dexie("decompiler") as Dexie & { options: EntityTable, - results3: Table, + results4: Table, }; - constructor() { - this.db.version(4).stores({ + constructor(version: Version = DEFAULT_VERSION) { + this.#version = version; + this.db.version(5).stores({ options: "key", - results3: "[className+checksum+language]", + results4: "[className+checksum+language+version]", // clear old data + results3: null, results2: null, results: null, }); @@ -67,19 +71,21 @@ export class DecompileWorker { } if (changed || notVisited.size > 0) { - await this.db.results3.clear(); + await this.db.results4.clear(); } await this.db.options.clear(); await this.db.options.bulkAdd(Object.entries(options).map(([k, v]) => ({ key: k, value: v }))); }); - loadVFRuntime = (preferWasm: boolean) => this.schedule(() => - vf.loadRuntime(preferWasm)); + loadVFRuntime = (preferWasm: boolean, version: Version) => this.schedule(() => { + this.#version = version; + return vf.loadRuntime(preferWasm, this.#version); + }); clear = (): Promise => this.schedule(async () => { - const count = await this.db.results3.count(); - await this.db.results3.clear(); + const count = await this.db.results4.count(); + await this.db.results4.clear(); return count; }); @@ -118,9 +124,9 @@ export class DecompileWorker { const checksum = jar.proxy[className]?.checksum; if (!checksum) continue; - const dbCount = await this.db.results3 - .where("[className+checksum+language]") - .equals([className, checksum, "java"]) + const dbCount = await this.db.results4 + .where("[className+checksum+language+version]") + .equals([className, checksum, "java", this.#version]) .count(); if (dbCount >= 1) { @@ -152,7 +158,7 @@ export class DecompileWorker { try { const jar = new DecompileJar(await openJar(jarName, jarBlob)); const checksum = jar.proxy[className]?.checksum; - const dbResult = await this.db.results3.get([className, checksum, "java"]); + const dbResult = await this.db.results4.get([className, checksum, "java", this.#version]); if (dbResult) return dbResult; const result = await this.#decompile(jar.classes, [className], jar.proxy); @@ -164,7 +170,8 @@ export class DecompileWorker { checksum: 0, source: `// Error during decompilation: ${(e as Error).message}`, tokens: [], - language: "java" + language: "java", + version: this.#version }; } }); @@ -180,7 +187,7 @@ export class DecompileWorker { let currentTokens: Token[] | undefined; let currentClassName: string | undefined; - const sources = await vf.decompile(classNames, { + const sources = await vf.decompile(this.#version, classNames, { source: async (name) => { const data = await classData[name]?.data; @@ -242,11 +249,12 @@ export class DecompileWorker { const res: DecompileResult[] = []; for (const [className, source] of Object.entries(sources)) { const checksum = classData[className]?.checksum ?? 0; - const tokens = allTokens[source] ?? []; + const sourceStr = source as string; + const tokens = allTokens[sourceStr] ?? []; const importRegex = /^\s*import\s+(?!static\b)([^\s;]+)\s*;/gm; - let match = null; - while ((match = importRegex.exec(source)) !== null) { + let match: RegExpExecArray | null = null; + while ((match = importRegex.exec(sourceStr)) !== null) { const importPath = match[1].replaceAll('.', '/'); if (importPath.endsWith('*')) { continue; @@ -264,27 +272,27 @@ export class DecompileWorker { } tokens.sort((a, b) => a.start - b.start); - res.push({ className, checksum, source, tokens, language: "java" }); + res.push({ className, checksum, source: sourceStr, tokens, language: "java", version: this.#version }); } - await this.db.results3.bulkPut(res); + await this.db.results4.bulkPut(res); return res; } #indexer = new JarIndexer(); getClassBytecode = (className: string, checksum: number, classData: ArrayBufferLike[]): Promise => this.schedule(async () => { - let result = await this.db.results3.get([className, checksum, "bytecode"]); + let result = await this.db.results4.get([className, checksum, "bytecode", this.#version]); if (result) return result; try { const bytecode = await this.#indexer.getBytecode(classData); - result = { className, checksum, source: bytecode, tokens: [], language: "bytecode" }; + result = { className, checksum, source: bytecode, tokens: [], language: "bytecode", version: this.#version }; } catch (e) { console.error(`Error during bytecode retrieval of class '${className}':`, e); - result = { className, checksum, source: `// Error during bytecode retrieval: ${(e as Error).message}`, tokens: [], language: "bytecode" }; + result = { className, checksum, source: `// Error during bytecode retrieval: ${(e as Error).message}`, tokens: [], language: "bytecode", version: this.#version }; } - await this.db.results3.put(result); + await this.db.results4.put(result); return result; }); } diff --git a/tests/permalink.spec.ts b/tests/permalink.spec.ts index 52b4589..e2f312b 100644 --- a/tests/permalink.spec.ts +++ b/tests/permalink.spec.ts @@ -6,7 +6,10 @@ test.describe('Permalinks and Line Highlighting', () => { await setupTest(page); }); - test('Permalink with line range highlights multiple lines (new format)', async ({ page }) => { + test('Permalink with line range highlights multiple lines (/1/ format)', async ({ page }) => { + const consoleLogs: string[] = []; + page.on('console', msg => consoleLogs.push(msg.text())); + await page.goto('/1/26.1-snapshot-1/net/minecraft/SystemReport#L87-90'); await waitForDecompiledContent(page, 'class SystemReport'); @@ -14,6 +17,24 @@ test.describe('Permalinks and Line Highlighting', () => { const editor = page.locator('.monaco-editor'); const highlightedLines = editor.locator('.highlighted-line'); await expect(highlightedLines.first()).toBeVisible(); + + await expect(page.getByText('Note: Using legacy decompiler (Vineflower 1.11.2)')).toBeVisible(); + expect(consoleLogs.some(log => log.includes('Loading VineFlower 1.11.2'))).toBe(true); + }); + + test('Permalink with line range highlights multiple lines (/2/ format)', async ({ page }) => { + const consoleLogs: string[] = []; + page.on('console', msg => consoleLogs.push(msg.text())); + + await page.goto('/2/26.1-snapshot-1/net/minecraft/SystemReport#L87-90'); + + await waitForDecompiledContent(page, 'class SystemReport'); + + const editor = page.locator('.monaco-editor'); + const highlightedLines = editor.locator('.highlighted-line'); + await expect(highlightedLines.first()).toBeVisible(); + + expect(consoleLogs.some(log => log.includes('Loading VineFlower 1.12.0'))).toBe(true); }); test('Permalink with line range highlights multiple lines (old hash format)', async ({ page }) => { @@ -41,7 +62,7 @@ test.describe('Permalinks and Line Highlighting', () => { // Wait for URL to update await page.waitForTimeout(10); const urlAfterFirstClick = page.url(); - expect(urlAfterFirstClick).toMatch(/\/1\/.*#L\d+$/); + expect(urlAfterFirstClick).toMatch(/\/2\/.*#L\d+$/); // Shift-click on a different line to create range await lineNumbers.nth(5).click({ modifiers: ['Shift'] }); @@ -50,7 +71,7 @@ test.describe('Permalinks and Line Highlighting', () => { await page.waitForTimeout(10); // Check that URL now contains a line range (new path-based format) - expect(page.url()).toMatch(/\/1\/.*#L\d+-\d+$/); + expect(page.url()).toMatch(/\/2\/.*#L\d+-\d+$/); expect(page.url()).not.toEqual(urlAfterFirstClick); // Check that lines are highlighted @@ -58,7 +79,10 @@ test.describe('Permalinks and Line Highlighting', () => { await expect(highlightedLine.first()).toBeVisible(); }); - test('Diff permalink restores left and right versions and opens diff view', async ({ page }) => { + test('Diff permalink restores left and right versions and opens diff view (/1/ format)', async ({ page }) => { + const consoleLogs: string[] = []; + page.on('console', msg => consoleLogs.push(msg.text())); + await page.goto('/1/diff/26.1-mock-1/26.1-mock-2/net/minecraft/client/renderer/LevelRenderer'); const diffEditor = page.locator('.monaco-diff-editor'); @@ -74,5 +98,30 @@ test.describe('Permalinks and Line Highlighting', () => { await expect(decompilingMessage).toBeHidden(); await expect(diffEditor).toContainText('net.minecraft.client.renderer'); + + expect(consoleLogs.some(log => log.includes('Loading VineFlower 1.12.0'))).toBe(true); + }); + + test('Diff permalink restores left and right versions and opens diff view (/2/ format)', async ({ page }) => { + const consoleLogs: string[] = []; + page.on('console', msg => consoleLogs.push(msg.text())); + + await page.goto('/2/diff/26.1-mock-1/26.1-mock-2/net/minecraft/client/renderer/LevelRenderer'); + + const diffEditor = page.locator('.monaco-diff-editor'); + await expect(diffEditor).toBeVisible(); + + const leftVersionSelect = page.locator('.ant-select').nth(0); + const rightVersionSelect = page.locator('.ant-select').nth(1); + + await expect(leftVersionSelect).toContainText('26.1-mock-1'); + await expect(rightVersionSelect).toContainText('26.1-mock-2'); + + const decompilingMessage = page.getByText('Decompiling...'); + await expect(decompilingMessage).toBeHidden(); + + await expect(diffEditor).toContainText('net.minecraft.client.renderer'); + + expect(consoleLogs.some(log => log.includes('Loading VineFlower 1.12.0'))).toBe(true); }); }); diff --git a/tests/test-utils.ts b/tests/test-utils.ts index eff0cf7..607392f 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -77,7 +77,12 @@ async function setupNetworkMocking(page: Page) { export async function setupTest(page: Page) { await setupNetworkMocking(page); - await page.addInitScript(() => { + const isWebKit = page.context().browser()?.browserType().name() === 'webkit'; + await page.addInitScript((preferWasm) => { localStorage.setItem('setting_eula', 'true'); - }); + if (!preferWasm) { + // Use JS runtime to avoid WASM compatibility issues in WebKit + localStorage.setItem('setting_prefer_wasm_decompiler', 'false'); + } + }, !isWebKit); }