Skip to content

Commit

Permalink
Added source maps for JS mods
Browse files Browse the repository at this point in the history
  • Loading branch information
olegbl committed Jun 20, 2024
1 parent 0158ef3 commit 5af2077
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 48 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@
"react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10",
"regenerator-runtime": "^0.13.9",
"source-map": "^0.7.4",
"typedoc": "^0.25.13"
},
"devEngines": {
Expand Down
145 changes: 100 additions & 45 deletions src/main/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { InstallationRuntime } from './InstallationRuntime';
import { datamod } from './datamod';
import { Scope, getQuickJSSync } from 'quickjs-emscripten';
import { getConsoleAPI } from './ConsoleAPI';
import { SourceMapConsumer, SourceMapGenerator } from 'source-map';

// keep in sync with ModAPI.tsx
enum Relative {
Expand Down Expand Up @@ -666,14 +667,30 @@ export const BridgeAPI: BridgeAPIImplementation = {
const absoluteFilePath = path.join(getAppPath(), relativeFilePath);
if (existsSync(absoluteFilePath)) {
const result = BridgeAPI.readFile(relativeFilePath, Relative.App);
if (typeof result === 'string') {
return `(function(){\n${result}\n})()`;
if (typeof result !== 'string') {
return createError(
'BridgeAPI.readModCode',
'Failed to read source code.',
result
);
}
return createError(
'BridgeAPI.readModCode',
'Failed to read source code.',
result
);

const code = `(function(){\nconst config = JSON.parse(D2RMM.getConfigJSON());\n${result}\n})()`;

const generator = new SourceMapGenerator({
file: `mods\\${id}\\mod.gen.js`,
sourceRoot: '',
});

code.split('\n').forEach((_line, index) => {
generator.addMapping({
generated: { line: index + 3, column: 0 },
original: { line: index + 1, column: 0 },
source: relativeFilePath,
});
});

return [code, generator.toString()];
}
}

Expand Down Expand Up @@ -834,7 +851,7 @@ export const BridgeAPI: BridgeAPIImplementation = {
''
);

return `
const code = `
function require(id) {
if (require.loadedModules[id] == null) {
require.load(id);
Expand All @@ -851,11 +868,14 @@ require.registeredModules = {};
require.register = function(id, getModule) {
require.registeredModules[id] = getModule;
}
const config = JSON.parse(D2RMM.getConfigJSON());
${modules}
require('./mod');
`;
// TODO: TypeScript sourcemaps
return [code, ''];
} catch (error) {
return createError(
'BridgeAPI.readModCode',
Expand Down Expand Up @@ -991,7 +1011,7 @@ require('./mod');
return result;
},

installMods: (modsToInstall: Mod[], options: IInstallModsOptions) => {
installMods: async (modsToInstall: Mod[], options: IInstallModsOptions) => {
runtime = new InstallationRuntime(
BridgeAPI,
rendererConsole,
Expand All @@ -1015,9 +1035,10 @@ require('./mod');

for (let i = 0; i < runtime.modsToInstall.length; i = i + 1) {
runtime.mod = runtime.modsToInstall[i];
let code: string = '';
let sourceMap: string = '';
try {
rendererConsole.debug(`Mod parsing code...`);
let code: string = '';
if (runtime.mod.info.type === 'data') {
code = datamod;
} else {
Expand All @@ -1028,48 +1049,46 @@ require('./mod');
if (result == null) {
throw new Error('Could not read code from mod.js or mod.ts.');
}
code = result;
}
// this lambda runs synchronously
const scope = new Scope();
try {
rendererConsole.debug(`Mod ${action.toLowerCase()}ing...`);
const vm = scope.manage(getQuickJSSync().newContext());
vm.setProp(
vm.global,
'console',
getConsoleAPI(vm, scope, rendererConsole)
);
vm.setProp(vm.global, 'D2RMM', getModAPI(vm, scope, runtime));
scope.manage(
vm.unwrapResult(
vm.evalCode(
`const config = JSON.parse(D2RMM.getConfigJSON());\n${code}`
)
)
);
runtime!.modsInstalled.push(runtime.mod.id);
rendererConsole.log(`Mod ${action.toLowerCase()}ed successfully.`);
} catch (error) {
if (error instanceof Error) {
rendererConsole.error(
`Mod encountered a runtime error!\n${
error.stack
?.replace(/\s*at <eval>[\s\S]*/m, '')
?.replace(/eval.js/g, `/mods/${runtime.mod.id}/mod.js`) ??
error.message
}`
);
}
[code, sourceMap] = result;
}
scope.dispose();
} catch (error) {
if (error instanceof Error) {
rendererConsole.error(
`Mod encountered a compile error!\n${error.stack}`
);
}
}
const scope = new Scope();
try {
rendererConsole.debug(`Mod ${action.toLowerCase()}ing...`);
const vm = scope.manage(getQuickJSSync().newContext());
vm.setProp(
vm.global,
'console',
getConsoleAPI(vm, scope, rendererConsole)
);
vm.setProp(vm.global, 'D2RMM', getModAPI(vm, scope, runtime));
scope.manage(vm.unwrapResult(vm.evalCode(code)));
runtime!.modsInstalled.push(runtime.mod.id);
rendererConsole.log(`Mod ${action.toLowerCase()}ed successfully.`);
} catch (error) {
if (error instanceof Error) {
let message = error.message;
if (error.stack != null && sourceMap !== '') {
// a constructor that returns a Promise smh
const sourceMapConsumer = await new SourceMapConsumer(sourceMap);
message = applySourceMapToStackTrace(
error.stack
?.replace(/\s*at <eval>[\s\S]*/m, '')
?.replace(/eval.js/g, `mods\\${runtime.mod.id}\\mod.gen.js`),
sourceMapConsumer
);
sourceMapConsumer.destroy();
}
rendererConsole.error(`Mod encountered a runtime error!\n${message}`);
}
}
scope.dispose();
}
runtime.mod = null;

Expand All @@ -1094,7 +1113,7 @@ require('./mod');
},
};

export function initBridgeAPI(mainWindow: BrowserWindow): void {
export async function initBridgeAPI(mainWindow: BrowserWindow): Promise<void> {
// hook up bridge API calls
Object.keys(BridgeAPI).forEach((apiName) => {
const apiCall = BridgeAPI[apiName as keyof typeof BridgeAPI];
Expand Down Expand Up @@ -1123,3 +1142,39 @@ export function initBridgeAPI(mainWindow: BrowserWindow): void {
};
});
}

function applySourceMapToStackTrace(
stackTrace: string,
sourceMap: SourceMapConsumer
): string {
return stackTrace
.split('\n')
.map((stackFrame) => {
const match = stackFrame.match(/at\s+(?:.*)\s+\((.*):(\d+)(?::(\d+))?\)/);
if (match == null) {
return stackFrame;
}

const position = {
source: match[1],
line: parseInt(match[2], 10),
column: parseInt(match[3] ?? '0', 10),
};

const originalPosition = sourceMap.originalPositionFor(position);
if (originalPosition.source == null) {
return stackFrame;
}

return stackFrame
.replace(
`(${position.source}:${position.line})`,
`(${originalPosition.source}:${originalPosition.line})`
)
.replace(
`(${position.source}:${position.line}:${position.column})`,
`(${originalPosition.source}:${originalPosition.line}:${originalPosition.column})`
);
})
.join('\n');
}
2 changes: 1 addition & 1 deletion src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const createWindow = async () => {
mainWindow.removeMenu();

await getQuickJS();
initBridgeAPI(mainWindow);
await initBridgeAPI(mainWindow);

mainWindow.loadURL(resolveHtmlPath('index.html'));

Expand Down
5 changes: 3 additions & 2 deletions src/renderer/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SourceMapConsumer } from 'source-map';
import { JSONData } from './JSON';
import { ModConfig } from './ModConfig';
import { ModConfigValue } from './ModConfigValue';
Expand Down Expand Up @@ -61,7 +62,7 @@ declare global {
installMods: (
modsToInstall: Mod[],
options: IInstallModsOptions
) => string[];
) => Promise<string[]>;
openStorage: (gamePath: string) => boolean | Error;
readDirectory: (
filePath: string
Expand All @@ -72,7 +73,7 @@ declare global {
relative: Relative
) => Buffer | null | Error;
readJson: (filePath: string) => JSONData | Error;
readModCode: (id: string) => string | Error;
readModCode: (id: string) => [string, string] | Error;
readModConfig: (id: string) => JSON;
readModDirectory: () => string[] | Error;
readModInfo: (id: string) => ModConfig;
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9465,6 +9465,11 @@ source-map@^0.7.3, source-map@~0.7.2:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==

source-map@^0.7.4:
version "0.7.4"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==

spawn-command@^0.0.2-1:
version "0.0.2-1"
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
Expand Down

0 comments on commit 5af2077

Please sign in to comment.