Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@
"command": "wit-idl.extractWit",
"title": "Extract WIT",
"category": "WIT"
},
{
"command": "wit-idl.extractCoreWasm",
"title": "Extract Core Wasm",
"category": "WIT"
}
],
"submenus": [
Expand All @@ -190,29 +195,39 @@
{
"submenu": "wit-idl.generateBindings.submenu",
"when": "resourceExtname == .wit || witIdl.isWasmComponent",
"group": "navigation"
"group": "navigation@12"
},
{
"command": "wit-idl.extractWit",
"when": "resourceExtname == .wasm && witIdl.isWasmComponent",
"group": "navigation"
"group": "navigation@10"
},
{
"command": "wit-idl.extractCoreWasm",
"when": "resourceExtname == .wasm && witIdl.isWasmComponent",
"group": "navigation@11"
}
],
"explorer/context": [
{
"command": "wit-idl.syntaxCheck",
"when": "resourceExtname == .wit",
"group": "navigation"
"group": "4_witIdl@10"
},
{
"submenu": "wit-idl.generateBindings.submenu",
"when": "resourceExtname == .wit || witIdl.isWasmComponent",
"group": "navigation"
"command": "wit-idl.extractWit",
"when": "resourceExtname == .wasm && witIdl.isWasmComponent",
"group": "4_witIdl@11"
},
{
"command": "wit-idl.extractWit",
"command": "wit-idl.extractCoreWasm",
"when": "resourceExtname == .wasm && witIdl.isWasmComponent",
"group": "navigation"
"group": "4_witIdl@12"
},
{
"submenu": "wit-idl.generateBindings.submenu",
"when": "resourceExtname == .wit || witIdl.isWasmComponent",
"group": "4_witIdl@13"
}
],
"wit-idl.generateBindings.submenu": [
Expand Down Expand Up @@ -266,6 +281,10 @@
{
"command": "wit-idl.extractWit",
"when": "witIdl.isWasmComponent"
},
{
"command": "wit-idl.extractCoreWasm",
"when": "witIdl.isWasmComponent"
}
]
},
Expand Down
107 changes: 106 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import * as path from "path";
import * as fs from "fs";
import { WitSyntaxValidator } from "./validator.js";
import { isWasmComponentFile } from "./wasmDetection.js";
import { getWitBindgenVersionFromWasm, extractWitFromComponent, generateBindingsFromWasm } from "./wasmUtils.js";
import {
getWitBindgenVersionFromWasm,
extractWitFromComponent,
generateBindingsFromWasm,
extractCoreWasmFromComponent,
} from "./wasmUtils.js";

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

Expand Down Expand Up @@ -369,6 +374,105 @@ export function activate(context: vscode.ExtensionContext) {
}
});

// Implement extractCoreWasm command
const extractCoreWasmCommand = vscode.commands.registerCommand(
"wit-idl.extractCoreWasm",
async (resource?: vscode.Uri) => {
try {
const targetUri: vscode.Uri | undefined = resource ?? vscode.window.activeTextEditor?.document.uri;
if (!targetUri) {
vscode.window.showErrorMessage("No file selected.");
return;
}

const filePath: string = targetUri.fsPath;
if (!filePath.toLowerCase().endsWith(".wasm")) {
vscode.window.showErrorMessage("Selected file is not a .wasm file.");
return;
}

const isComp: boolean = await isWasmComponent(filePath);
if (!isComp) {
vscode.window.showWarningMessage("The selected .wasm is not a WebAssembly component.");
return;
}

// Extract using embedded wasm-utils (no external CLI dependency)
const bytes = await vscode.workspace.fs.readFile(targetUri);
const fileMap = await extractCoreWasmFromComponent(bytes);
const entries = Object.entries(fileMap).sort(([a], [b]) =>
a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" })
);
if (entries.length === 0) {
vscode.window.showWarningMessage("No core wasm modules found in this component.");
return;
}

let [chosenName, chosenContent] = entries[0];
if (entries.length > 1) {
const pick = await vscode.window.showQuickPick(
entries.map(([name]) => name),
{
placeHolder: "Multiple core wasm modules found. Select one to save.",
}
);
if (!pick) {
return;
}
const found = entries.find(([name]) => name === pick);
if (found) {
[chosenName, chosenContent] = found;
}
}

// Now we know which core module is selected, propose a default filename that includes it
const baseName: string = path.basename(filePath, ".wasm");
const defaultFileName: string = `${baseName}.${chosenName}`; // e.g., mycomp.core0.wasm
const defaultDir: string = path.dirname(filePath);
const saveUri = await vscode.window.showSaveDialog({
title: "Save extracted Core Wasm",
defaultUri: vscode.Uri.file(path.join(defaultDir, defaultFileName)),
filters: { WebAssembly: ["wasm"], "All Files": ["*"] },
});
if (!saveUri) {
return;
}

// Write selected core wasm to the chosen path
const data = chosenContent; // already Uint8Array
try {
await vscode.workspace.fs.stat(saveUri);
const choice = await vscode.window.showWarningMessage(
`File already exists: ${saveUri.fsPath}. Overwrite?`,
{ modal: true },
"Overwrite",
"Cancel"
);
if (choice !== "Overwrite") {
return;
}
} catch {
// stat throws if file doesn't exist; continue
}
await vscode.workspace.fs.writeFile(saveUri, data);

vscode.window.showInformationMessage(`Core wasm extracted to ${saveUri.fsPath}`);
try {
await vscode.commands.executeCommand("vscode.open", saveUri);
} catch (openError) {
vscode.window.showWarningMessage(
`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").`
);
}
} catch (error) {
console.error("extractCoreWasm failed:", error);
vscode.window.showErrorMessage(
`Failed to extract core wasm: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);

/**
* Create a binding generation command for a specific language
* @param language - The target language for bindings
Expand Down Expand Up @@ -625,6 +729,7 @@ export function activate(context: vscode.ExtensionContext) {
syntaxCheckWorkspaceCommand,
showVersionCommand,
extractWitCommand,
extractCoreWasmCommand,
generateRustBindingsCommand,
generateCBindingsCommand,
generateCSharpBindingsCommand,
Expand Down
35 changes: 35 additions & 0 deletions src/wasmUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,41 @@ export async function extractWitFromComponent(bytes: Uint8Array): Promise<string
}
}

/**
* Extract core wasm module(s) from a WebAssembly component using the WASM module.
* Returns a filename -> binary bytes map. Empty map on failure or none found.
*/
export async function extractCoreWasmFromComponent(bytes: Uint8Array): Promise<Record<string, Uint8Array>> {
if (!wasmModule) {
await initializeWasm();
}
if (!wasmModule) {
throw new Error("WASM module not initialized");
}

function hasExtractCore(obj: unknown): obj is { extractCoreWasmFromComponent: (data: Uint8Array) => string } {
return typeof (obj as { extractCoreWasmFromComponent?: unknown }).extractCoreWasmFromComponent === "function";
}

const instance = new wasmModule.WitBindgen();
try {
if (!hasExtractCore(instance)) {
throw new Error("extractCoreWasmFromComponent not available in wit-bindgen-wasm module");
}
const json = instance.extractCoreWasmFromComponent(bytes);
const textMap = JSON.parse(json || "{}") as Record<string, string>;
// Decode into binary bytes (latin1-to-bytes convention from WASM side)
const result: Record<string, Uint8Array> = {};
for (const [name, content] of Object.entries(textMap)) {
// Buffer.from with 'latin1' yields the original byte values 0..255
result[name] = Buffer.from(content, "latin1");
}
return result;
} finally {
instance.free();
}
}

/**
* Extract interfaces from WIT content using the WASM module
* @param content - The WIT content to parse
Expand Down
42 changes: 42 additions & 0 deletions tests/commands.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, it, expect } from "vitest";

type Command = { command: string };
type MenuItem = { command?: string; when?: string };
type Contributes = {
commands: Command[];
menus: Record<string, MenuItem[]> & { commandPalette: MenuItem[] };
};

describe("command contributions", () => {
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8")) as unknown as {
contributes: Contributes;
};

it("includes Extract Core Wasm command", () => {
const commands: Command[] = pkg.contributes.commands;
expect(commands.some((c) => c.command === "wit-idl.extractCoreWasm")).toBe(true);
});

it("shows Extract Core Wasm in explorer and editor context menus for wasm components", () => {
const menus = pkg.contributes.menus;
const hasInEditor = (menus["editor/context"] || []).some(
(m: MenuItem) =>
m.command === "wit-idl.extractCoreWasm" && !!m.when && /witIdl\.isWasmComponent/.test(m.when)
);
const hasInExplorer = (menus["explorer/context"] || []).some(
(m: MenuItem) =>
m.command === "wit-idl.extractCoreWasm" && !!m.when && /witIdl\.isWasmComponent/.test(m.when)
);
expect(hasInEditor).toBe(true);
expect(hasInExplorer).toBe(true);
});

it("shows Extract Core Wasm in command palette when component is active", () => {
const palette: MenuItem[] = pkg.contributes.menus.commandPalette;
const entry = palette.find((m: MenuItem) => m.command === "wit-idl.extractCoreWasm");
expect(entry).toBeTruthy();
expect(entry && entry.when ? /witIdl\.isWasmComponent/.test(entry.when) : false).toBe(true);
});
});
23 changes: 19 additions & 4 deletions wit-bindgen-wasm/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions wit-bindgen-wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ wasm-bindgen = "0.2.100"
wee_alloc = { version = "0.4.5", optional = true }
console_error_panic_hook = { version = "0.1.6", optional = true }
serde_json = "1.0"
wasmparser = { version = "0.224", features = ["component-model"] }
wit-parser = "0.236"
wit-bindgen-core = "0.44"
wit-bindgen-c = "0.44"
Expand Down
Loading
Loading