Add standalone web build & decomp.me integration

This commit is contained in:
Luke Street
2025-03-06 21:53:12 -07:00
parent 4050ec4d95
commit 60dc1d6afe
34 changed files with 1761 additions and 271 deletions
-2
View File
@@ -1,2 +0,0 @@
NODE_ENV=development
DEV_SERVER=true
+1
View File
@@ -0,0 +1 @@
BUILD_TYPE=extension
-2
View File
@@ -1,2 +0,0 @@
NODE_ENV=development
DEV_SERVER=false
+2 -2
View File
@@ -5,13 +5,13 @@
"tasks": [
{
"type": "npm",
"script": "watch",
"script": "extension:dev",
"group": {
"kind": "build",
"isDefault": true
},
"isBackground": true,
"label": "npm:watch",
"label": "npm:extension:dev",
"presentation": {
"group": "watch",
"reveal": "never"
-16
View File
@@ -1,16 +0,0 @@
.vscode/**
.vscode-test/**
out/**
node_modules/**
src/**
webview/**
shared/**
.gitignore
.yarnrc
rsbuild.config.ts
**/tsconfig.json
**/biome.json
**/*.map
**/*.ts
**/.vscode-test.*
.env*
+31 -7
View File
@@ -1,22 +1,46 @@
# objdiff-code
# objdiff-web
> [!WARNING]
> This extension is in early development and is **not yet ready for use**.
Web interface for [objdiff](https://github.com/encounter/objdiff): a local diffing tool for decompilation projects.
This project contains both a standalone web interface and a [Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=decomp-dev.objdiff).
<p align="center">
<img src="./assets/screenshot_symbols.png" alt="Symbols view" width="45%" style="margin-right: 10px" />
<img src="./assets/screenshot_diff.png" alt="Diff view" width="45%" />
</p>
## How It Works
The object parsing and diffing is powered by [objdiff-core](https://github.com/encounter/objdiff/tree/main/objdiff-core), compiled into a [WebAssembly Component](https://component-model.bytecodealliance.org/). The [API](https://github.com/encounter/objdiff/blob/main/objdiff-wasm/wit/objdiff.wit) is defined using [WIT](https://component-model.bytecodealliance.org/design/wit.html).
Performance is a priority, so the web stack is deliberately minimal. The application loads from scratch in under 200ms and diffs large object files in 20ms. The build system, powered by [Rsbuild](https://rsbuild.dev/), produces a production build in under 300ms, with near-instantaneous live reloading during development.
## Setup
Open the project in Visual Studio Code.
- Install dependencies:
```bash
pnpm install
```
- Start dev server using `Ctrl+Shift+B` or by running:
## Run Dev Server
- Start the dev server using:
```bash
pnpm watch
pnpm web:dev
```
- Visit [http://localhost:3000/](http://localhost:3000/). (The port may change if it is already in use.)
## Run Extension
- Open the project in Visual Studio Code.
- Start the extension build using `Ctrl+Shift+B` or by running:
```bash
pnpm extension:dev
```
- Run the extension in debug mode using `F5`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

+27 -93
View File
@@ -6,22 +6,25 @@
"version": "0.1.0",
"repository": {
"type": "git",
"url": "https://github.com/encounter/objdiff-code"
"url": "https://github.com/encounter/objdiff-web"
},
"license": "MIT",
"scripts": {
"build": "rsbuild build",
"watch": "rsbuild build -w --env-mode watch",
"dev": "rsbuild dev --env-mode dev",
"check": "biome check --write",
"format": "biome format --write"
"format": "biome format --write",
"web:build": "rsbuild build",
"web:dev": "rsbuild dev",
"extension:build": "rsbuild build --env-mode extension",
"extension:dev": "rsbuild build -w -m development --env-mode extension",
"extension:package": "pnpm run extension:build && vsce package --no-dependencies",
"extension:publish": "pnpm run extension:build && vsce publish --no-dependencies"
},
"dependencies": {
"@protobuf-ts/runtime": "^2.9.4",
"@vscode/codicons": "^0.0.36",
"clsx": "^2.1.1",
"memoize-one": "^6.0.0",
"objdiff-wasm": "^3.0.0-alpha.1",
"objdiff-wasm": "3.0.0-beta.1",
"picomatch": "^4.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -46,22 +49,18 @@
"@types/vscode-webview": "^1.57.5",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.4.1",
"@vscode/vsce": "^3.2.2",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
},
"main": "./dist/extension.js",
"files": ["LICENSE", "README.md", "package.json", "dist", "media"],
"engines": {
"vscode": "^1.96.0"
},
"browserslist": [
"chrome 128"
],
"categories": [
"Other"
],
"activationEvents": [
"onWebviewPanel:objdiff"
],
"browserslist": ["chrome 128"],
"categories": ["Other"],
"activationEvents": ["onWebviewPanel:objdiff"],
"contributes": {
"commands": [
{
@@ -103,24 +102,14 @@
"type": "string",
"description": "How relocation targets will be diffed in the function view.",
"default": "name_address",
"enum": [
"none",
"name_address",
"data_value",
"all"
],
"enum": ["none", "name_address", "data_value", "all"],
"enumItemLabels": [
"None",
"Name or address",
"Data value",
"Name or address, data value"
],
"enumDescriptions": [
null,
null,
null,
null
]
"enumDescriptions": [null, null, null, null]
},
"objdiff.spaceBetweenArgs": {
"type": "boolean",
@@ -146,24 +135,14 @@
"type": "string",
"description": "ARM architecture version to use for disassembly.",
"default": "auto",
"enum": [
"auto",
"v4t",
"v5te",
"v6k"
],
"enum": ["auto", "v4t", "v5te", "v6k"],
"enumItemLabels": [
"Auto",
"ARMv4T (GBA)",
"ARMv5TE (DS)",
"ARMv6K (3DS)"
],
"enumDescriptions": [
null,
null,
null,
null
]
"enumDescriptions": [null, null, null, null]
},
"objdiff.arm.unifiedSyntax": {
"type": "boolean",
@@ -178,11 +157,7 @@
"objdiff.arm.r9Usage": {
"type": "string",
"default": "generalPurpose",
"enum": [
"generalPurpose",
"sb",
"tr"
],
"enum": ["generalPurpose", "sb", "tr"],
"enumItemLabels": [
"R9 or V6",
"SB (static base)",
@@ -218,24 +193,9 @@
"type": "string",
"description": "MIPS ABI to use for disassembly.",
"default": "auto",
"enum": [
"auto",
"o32",
"n32",
"n64"
],
"enumItemLabels": [
"Auto",
"O32",
"N32",
"N64"
],
"enumDescriptions": [
null,
null,
null,
null
]
"enum": ["auto", "o32", "n32", "n64"],
"enumItemLabels": ["Auto", "O32", "N32", "N64"],
"enumDescriptions": [null, null, null, null]
},
"objdiff.mips.instrCategory": {
"type": "string",
@@ -257,14 +217,7 @@
"R4000 ALLEGREX (PSP)",
"R5900 EE (PS2)"
],
"enumDescriptions": [
null,
null,
null,
null,
null,
null
]
"enumDescriptions": [null, null, null, null, null, null]
}
}
},
@@ -275,24 +228,9 @@
"type": "string",
"description": "x86 disassembly syntax.",
"default": "intel",
"enum": [
"intel",
"gas",
"nasm",
"masm"
],
"enumItemLabels": [
"Intel",
"AT&T",
"NASM",
"MASM"
],
"enumDescriptions": [
null,
null,
null,
null
]
"enum": ["intel", "gas", "nasm", "masm"],
"enumItemLabels": ["Intel", "AT&T", "NASM", "MASM"],
"enumDescriptions": [null, null, null, null]
}
}
}
@@ -340,10 +278,6 @@
]
},
"pnpm": {
"onlyBuiltDependencies": [
"@biomejs/biome",
"core-js",
"esbuild"
]
"onlyBuiltDependencies": ["@biomejs/biome", "core-js", "esbuild"]
}
}
+1361 -5
View File
File diff suppressed because it is too large Load Diff
+45 -18
View File
@@ -1,28 +1,38 @@
import fs from 'node:fs';
import type { ServerResponse } from 'node:http';
import path from 'node:path';
import { type RequestHandler, defineConfig } from '@rsbuild/core';
import {
type RequestHandler,
type RsbuildConfig,
defineConfig,
} from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginTypeCheck } from '@rsbuild/plugin-type-check';
import { pluginTypedCSSModules } from '@rsbuild/plugin-typed-css-modules';
const devServer = process.env.DEV_SERVER === 'true';
export default defineConfig({
// Disable HMR and live reload. Neither the extension nor the
// webview can communicate with the rsbuild dev server.
// Standalone web configuration.
const webConfig: RsbuildConfig = {
source: {
entry: {
index: './webview/index.tsx',
},
},
html: {
title: 'objdiff',
},
plugins: [pluginReact(), pluginTypeCheck(), pluginTypedCSSModules()],
dev: {
hmr: devServer,
liveReload: devServer,
setupMiddlewares: [
(middlewares, _server) => {
if (devServer) {
middlewares.unshift(apiMiddleware);
}
middlewares.unshift(apiMiddleware);
return middlewares;
},
],
},
};
// VS Code extension configuration.
const extensionConfig: RsbuildConfig = {
environments: {
extension: {
source: {
@@ -52,9 +62,9 @@ export default defineConfig({
// VS Code webviews don't have easy access to resources,
// (especially if the extension is running on web) so we
// simply inline everything into the HTML.
dataUriLimit: devServer ? undefined : 1000000000,
inlineScripts: !devServer,
inlineStyles: !devServer,
dataUriLimit: 1000000000,
inlineScripts: true,
inlineStyles: true,
legalComments: 'none',
},
// <script defer> doesn't work with inline scripts,
@@ -66,7 +76,7 @@ export default defineConfig({
},
plugins: [
pluginReact({
fastRefresh: devServer,
fastRefresh: false,
}),
pluginTypedCSSModules(),
],
@@ -76,7 +86,7 @@ export default defineConfig({
// the webview must be self-contained files.
performance: {
chunkSplit: {
strategy: devServer ? undefined : 'all-in-one',
strategy: 'all-in-one',
},
},
// Enable async TypeScript type checking.
@@ -87,16 +97,33 @@ export default defineConfig({
tools: {
rspack: {
output: {
asyncChunks: devServer,
asyncChunks: false,
},
},
},
});
// Disable HMR and live reload. Neither the extension nor the
// webview can communicate with the rsbuild dev server.
dev: {
hmr: false,
liveReload: false,
},
};
const buildType = process.env.BUILD_TYPE;
let config: RsbuildConfig;
if (buildType === 'extension') {
config = extensionConfig;
} else {
config = webConfig;
}
export default defineConfig(config);
const PROJECT_ROOT = '../prime';
// Mock API middleware for development.
const apiMiddleware: RequestHandler = (req, res, next) => {
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
if (!req.url || !req.headers.host || req.method !== 'GET') {
return next();
}
+15 -1
View File
@@ -29,8 +29,22 @@ export type StateMessage = {
projectConfig?: ProjectConfig | null;
};
// decomp.me colors
export type Colors = {
background: string;
};
// decomp.me theme
export type ThemeMessage = {
type: 'theme';
isDark: boolean;
colors: Colors;
codeFont?: string;
codeFontSize?: number;
};
// extension -> webview
export type InboundMessage = StateMessage;
export type InboundMessage = StateMessage | ThemeMessage;
export type ReadyMessage = {
type: 'ready';
+91 -26
View File
@@ -3,7 +3,6 @@
:root {
--code-font-family: var(
--vscode-editor-font-family,
JetBrainsMono Nerd Font,
JetBrains Mono,
Consolas,
"Courier New",
@@ -22,54 +21,87 @@
--ui-font-weight: var(--vscode-font-weight, normal);
--ui-font-size: var(--vscode-font-size, 13px);
--color-green: #00ff00;
--color-red: #f85149;
--color-blue: #add8e6;
--color-muted: var(--vscode-disabledForeground, rgba(204, 204, 204, 0.5));
--color-green: light-dark(#00c300, #00ff00);
--color-red: light-dark(#c80a00, #f85149);
--color-blue: light-dark(#006d90, #add8e6);
--color-muted: var(
--vscode-disabledForeground,
light-dark(rgba(97, 97, 97, 0.5), rgba(204, 204, 204, 0.5))
);
--color-bright: light-dark(black, white);
--panel-background: var(--vscode-panel-background, #181818);
--panel-separator: var(--vscode-menu-separatorBackground, #454545);
--panel-background: var(
--vscode-panel-background,
light-dark(#f8f8f8, #181818)
);
--panel-separator: var(
--vscode-menu-separatorBackground,
light-dark(#d4d4d4, #454545)
);
--foreground: var(--vscode-foreground, #ccc);
--background: var(--vscode-editor-background, #1f1f1f);
--foreground: var(--vscode-foreground, light-dark(#3b3b3b, #ccc));
--background: var(--vscode-editor-background, light-dark(#fff, #1f1f1f));
--list-row-height: calc(var(--code-font-size) * 1.33);
--list-row-hover-background: var(--vscode-list-hoverBackground, #2a2d2e);
--list-row-hover-background: var(
--vscode-list-hoverBackground,
light-dark(#f2f2f2, #2a2d2e)
);
--line-number-foreground: var(--vscode-editorLineNumber-foreground, #6e7681);
--button-background-color: var(--vscode-button-secondaryBackground, #313131);
--button-foreground-color: var(--vscode-button-secondaryForeground, #ccc);
--button-border-color: var(--vscode-button-border, rgba(255, 255, 255, 0.07));
--button-background-color: var(
--vscode-button-secondaryBackground,
light-dark(#e5e5e5, #313131)
);
--button-foreground-color: var(
--vscode-button-secondaryForeground,
light-dark(#3b3b3b, #ccc)
);
--button-border-color: var(
--vscode-button-border,
light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.07))
);
--button-hover-background-color: var(
--vscode-button-secondaryHoverBackground,
#3c3c3c
light-dark(#cccccc, #3c3c3c)
);
--button-active-background-color: var(
--vscode-toolbar-activeBackground,
rgba(99, 102, 103, 0.31)
light-dark(rgba(166, 166, 166, 0.31), rgba(99, 102, 103, 0.31))
);
--button-disabled-foreground-color: var(
--vscode-disabledForeground,
rgba(204, 204, 204, 0.5)
light-dark(rgba(97, 97, 97, 0.5), rgba(204, 204, 204, 0.5))
);
--focus-border-color: var(--vscode-focusBorder, #0078d4);
--focus-border-color: var(--vscode-focusBorder, light-dark(#005fb8, #0078d4));
--input-background-color: var(--vscode-input-background, #313131);
--input-foreground-color: var(--vscode-input-foreground, #ccc);
--input-border-color: var(--vscode-input-border, #3c3c3c);
--input-background-color: var(
--vscode-input-background,
light-dark(#fff, #313131)
);
--input-foreground-color: var(
--vscode-input-foreground,
light-dark(#3b3b3b, #ccc)
);
--input-border-color: var(--vscode-input-border, light-dark(#cecece, #3c3c3c));
--input-placeholder-foreground-color: var(
--vscode-input-placeholderForeground,
#989898
light-dark(#767676, #989898)
);
--checkbox-background-color: var(
--vscode-settings-checkboxBackground,
#313131
light-dark(#f8f8f8, #313131)
);
--checkbox-foreground-color: var(
--vscode-settings-checkboxForeground,
light-dark(#3b3b3b, #ccc)
);
--checkbox-border-color: var(
--vscode-settings-checkboxBorder,
light-dark(#cecece, #3c3c3c)
);
--checkbox-foreground-color: var(--vscode-settings-checkboxForeground, #ccc);
--checkbox-border-color: var(--vscode-settings-checkboxBorder, #3c3c3c);
color-scheme: light dark;
@@ -83,7 +115,7 @@
body {
margin: 0;
padding: 0;
min-height: 100vh;
height: 100vh;
color: var(--foreground);
font-family: var(--ui-font-family);
font-weight: var(--ui-font-weight);
@@ -99,12 +131,45 @@ body {
&.vscode-high-contrast-dark {
color-scheme: dark;
}
&.decomp-me-dark,
&.decomp-me-light {
--code-font-family: var(
--vscode-editor-font-family,
Menlo,
Monaco,
monospace
);
--ui-font-size: .8rem;
}
&.decomp-me-dark {
--panel-background: hsl(300deg 5% 9.5%);
--background: rgb(22, 22, 24);
--foreground: #fff9;
--color-bright: #f9f9f9;
color-scheme: dark;
}
&.decomp-me-light {
--code-font-family: var(
--vscode-editor-font-family,
Menlo,
Monaco,
monospace
);
--ui-font-size: .8rem;
--panel-background: #fdfcfd;
--background: #fdfcfd;
--foreground: rgb(51, 51, 51);
--color-bright: #000000e6;
color-scheme: light;
}
}
#root {
display: flex;
flex-flow: column;
min-height: 100vh;
height: 100%;
overflow: hidden;
}
.loading-root {
+15 -15
View File
@@ -2,12 +2,12 @@ import './App.css';
import type { diff, display } from 'objdiff-wasm';
import { useShallow } from 'zustand/react/shallow';
import FunctionView from './FunctionView';
import SettingsView from './SettingsView';
import SymbolsView from './SymbolsView';
import UnitsView from './UnitsView';
import { useAppStore, useExtensionStore } from './state';
import type { SymbolRefByName } from './state';
import FunctionView from './views/FunctionView';
import SettingsView from './views/SettingsView';
import SymbolsView from './views/SymbolsView';
import UnitsView from './views/UnitsView';
const findSymbol = (
obj: diff.ObjectDiff | undefined,
@@ -51,19 +51,19 @@ const App = () => {
return <div className="loading-root" />;
}
if (result) {
const leftSymbol = findSymbol(result.left, leftSymbolRef);
const rightSymbol = findSymbol(result.right, rightSymbolRef);
if (leftSymbol || rightSymbol) {
return (
<FunctionView diff={result} left={leftSymbol} right={rightSymbol} />
);
}
return <SymbolsView diff={result} />;
}
switch (currentView) {
case 'main':
if (result) {
const leftSymbol = findSymbol(result.left, leftSymbolRef);
const rightSymbol = findSymbol(result.right, rightSymbolRef);
if (leftSymbol || rightSymbol) {
return (
<FunctionView diff={result} left={leftSymbol} right={rightSymbol} />
);
}
return <SymbolsView diff={result} />;
}
if (buildRunning) {
return (
<div className="content">
+1 -1
View File
@@ -1,4 +1,4 @@
import './wasm'; // Must come first
import './util/wasm'; // Must come first
import React from 'react';
import ReactDOM from 'react-dom/client';
-3
View File
@@ -1,3 +0,0 @@
if (window.webviewProps?.resourceRoot) {
__webpack_public_path__ = window.webviewProps.resourceRoot;
}
+28 -6
View File
@@ -1,4 +1,5 @@
import {
type ConfigProperties,
type ProjectConfig,
type Unit,
resolveProjectConfig,
@@ -32,10 +33,6 @@ function sendMessage(data: InboundMessage) {
window.dispatchEvent(new MessageEvent('message', { data }));
}
// async function delay(ms: number) {
// return new Promise((resolve) => setTimeout(resolve, ms));
// }
let resolvedProjectConfig: ProjectConfig | null = null;
async function fetchFile(path: string): Promise<Response> {
@@ -55,6 +52,13 @@ if (serializedLastUnit) {
lastUnit = JSON.parse(serializedLastUnit);
}
let configProperties: ConfigProperties = {};
const serializedConfigProperties = localStorage.getItem('configProperties');
if (serializedConfigProperties) {
configProperties = JSON.parse(serializedConfigProperties);
}
async function handleMessage(msg: OutboundMessage): Promise<void> {
switch (msg.type) {
case 'ready': {
@@ -64,7 +68,7 @@ async function handleMessage(msg: OutboundMessage): Promise<void> {
const out: StateMessage = {
type: 'state',
buildRunning: false,
configProperties: {},
configProperties,
currentUnit: null,
leftObject: null,
rightObject: null,
@@ -128,6 +132,20 @@ async function handleMessage(msg: OutboundMessage): Promise<void> {
sendMessage(out);
break;
}
case 'setConfigProperty': {
if (msg.value === undefined) {
configProperties = { ...configProperties };
delete configProperties[msg.id];
} else {
configProperties = { ...configProperties, [msg.id]: msg.value };
}
localStorage.setItem(
'configProperties',
JSON.stringify(configProperties),
);
sendMessage({ type: 'state', configProperties });
break;
}
default: {
console.warn('Unhandled message', msg);
}
@@ -136,7 +154,11 @@ async function handleMessage(msg: OutboundMessage): Promise<void> {
export const mockVsCode: MyWebviewApi<AppStateSerialized> = {
postMessage: (msg) => {
handleMessage(msg);
if (window.parent === window || msg.type === 'setConfigProperty') {
handleMessage(msg);
} else {
window.parent.postMessage(msg, '*');
}
},
getState: () => state,
setState: (newState) => {

Some files were not shown because too many files have changed in this diff Show More