Skip to content

Commit 58c389e

Browse files
GordonSmithCopilot
andcommitted
feat: add extract core wasm command
Signed-off-by: Gordon Smith <GordonJSmith@gmail.com> Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent a374c58 commit 58c389e

File tree

7 files changed

+271
-13
lines changed

7 files changed

+271
-13
lines changed

package.json

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,11 @@
172172
"command": "wit-idl.extractWit",
173173
"title": "Extract WIT",
174174
"category": "WIT"
175+
},
176+
{
177+
"command": "wit-idl.extractCoreWasm",
178+
"title": "Extract Core Wasm",
179+
"category": "WIT"
175180
}
176181
],
177182
"submenus": [
@@ -190,29 +195,39 @@
190195
{
191196
"submenu": "wit-idl.generateBindings.submenu",
192197
"when": "resourceExtname == .wit || witIdl.isWasmComponent",
193-
"group": "navigation"
198+
"group": "navigation@12"
194199
},
195200
{
196201
"command": "wit-idl.extractWit",
197202
"when": "resourceExtname == .wasm && witIdl.isWasmComponent",
198-
"group": "navigation"
203+
"group": "navigation@10"
204+
},
205+
{
206+
"command": "wit-idl.extractCoreWasm",
207+
"when": "resourceExtname == .wasm && witIdl.isWasmComponent",
208+
"group": "navigation@11"
199209
}
200210
],
201211
"explorer/context": [
202212
{
203213
"command": "wit-idl.syntaxCheck",
204214
"when": "resourceExtname == .wit",
205-
"group": "navigation"
215+
"group": "4_witIdl@10"
206216
},
207217
{
208-
"submenu": "wit-idl.generateBindings.submenu",
209-
"when": "resourceExtname == .wit || witIdl.isWasmComponent",
210-
"group": "navigation"
218+
"command": "wit-idl.extractWit",
219+
"when": "resourceExtname == .wasm && witIdl.isWasmComponent",
220+
"group": "4_witIdl@11"
211221
},
212222
{
213-
"command": "wit-idl.extractWit",
223+
"command": "wit-idl.extractCoreWasm",
214224
"when": "resourceExtname == .wasm && witIdl.isWasmComponent",
215-
"group": "navigation"
225+
"group": "4_witIdl@12"
226+
},
227+
{
228+
"submenu": "wit-idl.generateBindings.submenu",
229+
"when": "resourceExtname == .wit || witIdl.isWasmComponent",
230+
"group": "4_witIdl@13"
216231
}
217232
],
218233
"wit-idl.generateBindings.submenu": [
@@ -266,6 +281,10 @@
266281
{
267282
"command": "wit-idl.extractWit",
268283
"when": "witIdl.isWasmComponent"
284+
},
285+
{
286+
"command": "wit-idl.extractCoreWasm",
287+
"when": "witIdl.isWasmComponent"
269288
}
270289
]
271290
},

src/extension.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import * as path from "path";
33
import * as fs from "fs";
44
import { WitSyntaxValidator } from "./validator.js";
55
import { isWasmComponentFile } from "./wasmDetection.js";
6-
import { getWitBindgenVersionFromWasm, extractWitFromComponent, generateBindingsFromWasm } from "./wasmUtils.js";
6+
import {
7+
getWitBindgenVersionFromWasm,
8+
extractWitFromComponent,
9+
generateBindingsFromWasm,
10+
extractCoreWasmFromComponent,
11+
} from "./wasmUtils.js";
712

813
// Removed: openExtractedWitDocument - we now render a readonly view in a custom editor
914

@@ -369,6 +374,105 @@ export function activate(context: vscode.ExtensionContext) {
369374
}
370375
});
371376

377+
// Implement extractCoreWasm command
378+
const extractCoreWasmCommand = vscode.commands.registerCommand(
379+
"wit-idl.extractCoreWasm",
380+
async (resource?: vscode.Uri) => {
381+
try {
382+
const targetUri: vscode.Uri | undefined = resource ?? vscode.window.activeTextEditor?.document.uri;
383+
if (!targetUri) {
384+
vscode.window.showErrorMessage("No file selected.");
385+
return;
386+
}
387+
388+
const filePath: string = targetUri.fsPath;
389+
if (!filePath.toLowerCase().endsWith(".wasm")) {
390+
vscode.window.showErrorMessage("Selected file is not a .wasm file.");
391+
return;
392+
}
393+
394+
const isComp: boolean = await isWasmComponent(filePath);
395+
if (!isComp) {
396+
vscode.window.showWarningMessage("The selected .wasm is not a WebAssembly component.");
397+
return;
398+
}
399+
400+
// Extract using embedded wasm-utils (no external CLI dependency)
401+
const bytes = await vscode.workspace.fs.readFile(targetUri);
402+
const fileMap = await extractCoreWasmFromComponent(bytes);
403+
const entries = Object.entries(fileMap).sort(([a], [b]) =>
404+
a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" })
405+
);
406+
if (entries.length === 0) {
407+
vscode.window.showWarningMessage("No core wasm modules found in this component.");
408+
return;
409+
}
410+
411+
let [chosenName, chosenContent] = entries[0];
412+
if (entries.length > 1) {
413+
const pick = await vscode.window.showQuickPick(
414+
entries.map(([name]) => name),
415+
{
416+
placeHolder: "Multiple core wasm modules found. Select one to save.",
417+
}
418+
);
419+
if (!pick) {
420+
return;
421+
}
422+
const found = entries.find(([name]) => name === pick);
423+
if (found) {
424+
[chosenName, chosenContent] = found;
425+
}
426+
}
427+
428+
// Now we know which core module is selected, propose a default filename that includes it
429+
const baseName: string = path.basename(filePath, ".wasm");
430+
const defaultFileName: string = `${baseName}.${chosenName}`; // e.g., mycomp.core0.wasm
431+
const defaultDir: string = path.dirname(filePath);
432+
const saveUri = await vscode.window.showSaveDialog({
433+
title: "Save extracted Core Wasm",
434+
defaultUri: vscode.Uri.file(path.join(defaultDir, defaultFileName)),
435+
filters: { WebAssembly: ["wasm"], "All Files": ["*"] },
436+
});
437+
if (!saveUri) {
438+
return;
439+
}
440+
441+
// Write selected core wasm to the chosen path
442+
const data = chosenContent; // already Uint8Array
443+
try {
444+
await vscode.workspace.fs.stat(saveUri);
445+
const choice = await vscode.window.showWarningMessage(
446+
`File already exists: ${saveUri.fsPath}. Overwrite?`,
447+
{ modal: true },
448+
"Overwrite",
449+
"Cancel"
450+
);
451+
if (choice !== "Overwrite") {
452+
return;
453+
}
454+
} catch {
455+
// stat throws if file doesn't exist; continue
456+
}
457+
await vscode.workspace.fs.writeFile(saveUri, data);
458+
459+
vscode.window.showInformationMessage(`Core wasm extracted to ${saveUri.fsPath}`);
460+
try {
461+
await vscode.commands.executeCommand("vscode.open", saveUri);
462+
} catch (openError) {
463+
vscode.window.showWarningMessage(
464+
`Core wasm was extracted and saved to ${saveUri.fsPath}, but could not be opened automatically. You can open it manually (e.g., with "Open With... Hex Editor").`
465+
);
466+
}
467+
} catch (error) {
468+
console.error("extractCoreWasm failed:", error);
469+
vscode.window.showErrorMessage(
470+
`Failed to extract core wasm: ${error instanceof Error ? error.message : String(error)}`
471+
);
472+
}
473+
}
474+
);
475+
372476
/**
373477
* Create a binding generation command for a specific language
374478
* @param language - The target language for bindings
@@ -625,6 +729,7 @@ export function activate(context: vscode.ExtensionContext) {
625729
syntaxCheckWorkspaceCommand,
626730
showVersionCommand,
627731
extractWitCommand,
732+
extractCoreWasmCommand,
628733
generateRustBindingsCommand,
629734
generateCBindingsCommand,
630735
generateCSharpBindingsCommand,

src/wasmUtils.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,41 @@ export async function extractWitFromComponent(bytes: Uint8Array): Promise<string
149149
}
150150
}
151151

152+
/**
153+
* Extract core wasm module(s) from a WebAssembly component using the WASM module.
154+
* Returns a filename -> binary bytes map. Empty map on failure or none found.
155+
*/
156+
export async function extractCoreWasmFromComponent(bytes: Uint8Array): Promise<Record<string, Uint8Array>> {
157+
if (!wasmModule) {
158+
await initializeWasm();
159+
}
160+
if (!wasmModule) {
161+
throw new Error("WASM module not initialized");
162+
}
163+
164+
function hasExtractCore(obj: unknown): obj is { extractCoreWasmFromComponent: (data: Uint8Array) => string } {
165+
return typeof (obj as { extractCoreWasmFromComponent?: unknown }).extractCoreWasmFromComponent === "function";
166+
}
167+
168+
const instance = new wasmModule.WitBindgen();
169+
try {
170+
if (!hasExtractCore(instance)) {
171+
throw new Error("extractCoreWasmFromComponent not available in wit-bindgen-wasm module");
172+
}
173+
const json = instance.extractCoreWasmFromComponent(bytes);
174+
const textMap = JSON.parse(json || "{}") as Record<string, string>;
175+
// Decode into binary bytes (latin1-to-bytes convention from WASM side)
176+
const result: Record<string, Uint8Array> = {};
177+
for (const [name, content] of Object.entries(textMap)) {
178+
// Buffer.from with 'latin1' yields the original byte values 0..255
179+
result[name] = Buffer.from(content, "latin1");
180+
}
181+
return result;
182+
} finally {
183+
instance.free();
184+
}
185+
}
186+
152187
/**
153188
* Extract interfaces from WIT content using the WASM module
154189
* @param content - The WIT content to parse

tests/commands.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { readFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
import { describe, it, expect } from "vitest";
4+
5+
type Command = { command: string };
6+
type MenuItem = { command?: string; when?: string };
7+
type Contributes = {
8+
commands: Command[];
9+
menus: Record<string, MenuItem[]> & { commandPalette: MenuItem[] };
10+
};
11+
12+
describe("command contributions", () => {
13+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8")) as unknown as {
14+
contributes: Contributes;
15+
};
16+
17+
it("includes Extract Core Wasm command", () => {
18+
const commands: Command[] = pkg.contributes.commands;
19+
expect(commands.some((c) => c.command === "wit-idl.extractCoreWasm")).toBe(true);
20+
});
21+
22+
it("shows Extract Core Wasm in explorer and editor context menus for wasm components", () => {
23+
const menus = pkg.contributes.menus;
24+
const hasInEditor = (menus["editor/context"] || []).some(
25+
(m: MenuItem) =>
26+
m.command === "wit-idl.extractCoreWasm" && !!m.when && /witIdl\.isWasmComponent/.test(m.when)
27+
);
28+
const hasInExplorer = (menus["explorer/context"] || []).some(
29+
(m: MenuItem) =>
30+
m.command === "wit-idl.extractCoreWasm" && !!m.when && /witIdl\.isWasmComponent/.test(m.when)
31+
);
32+
expect(hasInEditor).toBe(true);
33+
expect(hasInExplorer).toBe(true);
34+
});
35+
36+
it("shows Extract Core Wasm in command palette when component is active", () => {
37+
const palette: MenuItem[] = pkg.contributes.menus.commandPalette;
38+
const entry = palette.find((m: MenuItem) => m.command === "wit-idl.extractCoreWasm");
39+
expect(entry).toBeTruthy();
40+
expect(entry && entry.when ? /witIdl\.isWasmComponent/.test(entry.when) : false).toBe(true);
41+
});
42+
});

wit-bindgen-wasm/Cargo.lock

Lines changed: 19 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

wit-bindgen-wasm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ wasm-bindgen = "0.2.100"
1818
wee_alloc = { version = "0.4.5", optional = true }
1919
console_error_panic_hook = { version = "0.1.6", optional = true }
2020
serde_json = "1.0"
21+
wasmparser = { version = "0.224", features = ["component-model"] }
2122
wit-parser = "0.236"
2223
wit-bindgen-core = "0.44"
2324
wit-bindgen-c = "0.44"

0 commit comments

Comments
 (0)