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
461 changes: 225 additions & 236 deletions package-lock.json

Large diffs are not rendered by default.

29 changes: 20 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,30 +75,41 @@
},
"dependencies": {},
"devDependencies": {
"@bytecodealliance/jco": "1.17.1",
"@bytecodealliance/jco": "1.17.5",
"@eslint/css": "1.0.0",
"@eslint/js": "10.0.1",
"@types/node": "25.5.0",
"@types/vscode": "1.99.0",
"@typescript-eslint/parser": "8.57.1",
"@vitest/coverage-v8": "4.1.0",
"@vitest/ui": "4.1.0",
"@typescript-eslint/parser": "8.57.2",
"@vitest/coverage-v8": "4.1.1",
"@vitest/ui": "4.1.1",
"@vscode/vsce": "3.7.1",
"esbuild": "0.27.4",
"esbuild-node-externals": "1.20.1",
"eslint": "10.0.3",
"eslint": "10.1.0",
"eslint-plugin-prettier": "5.5.5",
"globals": "17.4.0",
"npm-run-all": "4.1.5",
"ovsx": "0.10.9",
"ovsx": "0.10.10",
"prettier": "3.8.1",
"rimraf": "6.1.3",
"typescript": "5.9.3",
"typescript-eslint": "8.57.1",
"vitest": "4.1.0",
"typescript": "6.0.2",
"typescript-eslint": "8.57.2",
"vitest": "4.1.1",
"vscode-tmgrammar-test": "0.1.3",
"wit-bindgen-wasm": "file:wit-bindgen-wasm/pkg"
},
"overrides": {
"typescript-eslint": {
"typescript": "$typescript"
},
"@typescript-eslint/eslint-plugin": {
"typescript": "$typescript"
},
"@typescript-eslint/parser": {
"typescript": "$typescript"
}
},
"contributes": {
"colors": [
{
Expand Down
7 changes: 6 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,12 @@ export function activate(context: vscode.ExtensionContext) {

const outputPath = outputUri[0].fsPath;

const bindingFiles = await generateBindingsFromWasm(witContent, language);
const bindingFiles = await generateBindingsFromWasm(
witContent,
language,
undefined,
diagDoc?.uri.fsPath ?? targetUri.fsPath
);

const fileEntries = Object.entries(bindingFiles);
const errorFile = fileEntries.find(([filename]) => filename === "error.txt");
Expand Down
2 changes: 1 addition & 1 deletion src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class WitSyntaxValidator {
public async validate(path: string, content: string): Promise<ReturnType<typeof extractErrorInfo> | null> {
try {
// Use the enhanced WASM-based WIT validation with detailed error reporting
const validationResult = await validateWitSyntaxDetailedFromWasm(content);
const validationResult = await validateWitSyntaxDetailedFromWasm(content, path);

if (validationResult.valid) {
return null;
Expand Down
160 changes: 152 additions & 8 deletions src/wasmUtils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,137 @@
import { readdir, readFile } from "node:fs/promises";
import type { Dirent } from "node:fs";
import path from "node:path";

/**
* Cached reference to the witValidator interface from the WASM component module.
* The jco-transpiled module self-initializes via top-level await,
* so the module is ready to use once the dynamic import resolves.
*/
let witValidatorApi: typeof import("wit-bindgen-wasm").witValidator | null = null;

interface PreparedSourceContext {
sourcePath?: string;
sourceFilesJson?: string;
}

function normalizeSourcePath(sourcePath?: string): string | undefined {
const trimmedPath = sourcePath?.trim();
if (!trimmedPath) {
return undefined;
}

return path.resolve(trimmedPath);
}

function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
return error instanceof Error && "code" in error;
}

async function readDirectoryEntries(directoryPath: string): Promise<Array<Dirent>> {
try {
return await readdir(directoryPath, { encoding: "utf8", withFileTypes: true });
} catch (error: unknown) {
if (isErrnoException(error) && error.code === "ENOENT") {
return [];
}

throw error;
}
}

async function collectWitFilePathsRecursively(directoryPath: string, filePaths: Array<string>): Promise<void> {
const entries = await readDirectoryEntries(directoryPath);
for (const entry of entries) {
const entryPath = path.join(directoryPath, entry.name);
if (entry.isDirectory()) {
await collectWitFilePathsRecursively(entryPath, filePaths);
continue;
}

if (entry.isFile() && entry.name.toLowerCase().endsWith(".wit")) {
filePaths.push(entryPath);
}
}
}

async function readWitFilesWithConcurrency(filePaths: Array<string>, target: Record<string, string>): Promise<void> {
if (filePaths.length === 0) {
return;
}

const maxConcurrency = 8;
let currentIndex = 0;

const worker = async (): Promise<void> => {
while (true) {
const index = currentIndex;
if (index >= filePaths.length) {
return;
}

currentIndex += 1;
const filePath = filePaths[index];
try {
const contents = await readFile(filePath, "utf8");
target[filePath] = contents;
} catch (error: unknown) {
if (isErrnoException(error) && (error.code === "ENOENT" || error.code === "EACCES")) {
// Skip files that are missing or not accessible without failing the whole operation.
continue;
}

throw error;
}
}
};

const workerCount = Math.min(maxConcurrency, filePaths.length);
const workers: Array<Promise<void>> = [];
for (let i = 0; i < workerCount; i += 1) {
workers.push(worker());
}

await Promise.all(workers);
}

async function collectWitContext(sourceDirectory: string): Promise<Record<string, string>> {
const sourceFiles: Record<string, string> = {};
const filePaths: Array<string> = [];

const entries = await readDirectoryEntries(sourceDirectory);
for (const entry of entries) {
const entryPath = path.join(sourceDirectory, entry.name);
if (entry.isDirectory()) {
if (entry.name === "deps") {
await collectWitFilePathsRecursively(entryPath, filePaths);
}
continue;
}

if (entry.isFile() && entry.name.toLowerCase().endsWith(".wit")) {
filePaths.push(entryPath);
}
}

await readWitFilesWithConcurrency(filePaths, sourceFiles);
return sourceFiles;
}

async function prepareSourceContext(content: string, sourcePath?: string): Promise<PreparedSourceContext> {
const normalizedSourcePath = normalizeSourcePath(sourcePath);
if (!normalizedSourcePath) {
return {};
}

const sourceFiles = await collectWitContext(path.dirname(normalizedSourcePath));
sourceFiles[normalizedSourcePath] = content;

return {
sourcePath: normalizedSourcePath,
sourceFilesJson: JSON.stringify(sourceFiles),
};
}

/**
* Initialize the WASM module by dynamically importing it.
* The jco-transpiled component handles WASM loading internally.
Expand Down Expand Up @@ -68,9 +195,10 @@ export async function isWitFileExtensionFromWasm(filename: string): Promise<bool
* @param content - The WIT content to validate
* @returns Promise that resolves to true if the syntax is valid
*/
export async function validateWitSyntaxFromWasm(content: string): Promise<boolean> {
export async function validateWitSyntaxFromWasm(content: string, sourcePath?: string): Promise<boolean> {
const api = await getApi();
return api.validateWitSyntax(content);
const preparedSource = await prepareSourceContext(content, sourcePath);
return api.validateWitSyntax(content, preparedSource.sourcePath, preparedSource.sourceFilesJson);
}

/**
Expand Down Expand Up @@ -122,11 +250,19 @@ export async function extractInterfacesFromWasm(content: string): Promise<string
export async function generateBindingsFromWasm(
content: string,
language: string,
worldName?: string
worldName?: string,
sourcePath?: string
): Promise<Record<string, string>> {
const api = await getApi();
const jsonResult = api.generateBindings(content, language, worldName);
return JSON.parse(jsonResult);
const preparedSource = await prepareSourceContext(content, sourcePath);
const jsonResult = api.generateBindings(
content,
language,
worldName,
preparedSource.sourcePath,
preparedSource.sourceFilesJson
);
return JSON.parse(jsonResult) as Record<string, string>;
}

/**
Expand All @@ -143,8 +279,16 @@ export interface WitValidationResult {
* @param content - The WIT content to validate
* @returns Promise that resolves to detailed validation results
*/
export async function validateWitSyntaxDetailedFromWasm(content: string): Promise<WitValidationResult> {
export async function validateWitSyntaxDetailedFromWasm(
content: string,
sourcePath?: string
): Promise<WitValidationResult> {
const api = await getApi();
const resultJson = api.validateWitSyntaxDetailed(content);
return JSON.parse(resultJson);
const preparedSource = await prepareSourceContext(content, sourcePath);
const resultJson = api.validateWitSyntaxDetailed(
content,
preparedSource.sourcePath,
preparedSource.sourceFilesJson
);
return JSON.parse(resultJson) as WitValidationResult;
}
8 changes: 4 additions & 4 deletions tests/bindings-generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe("Bindings Generation for All Languages", () => {

supportedLanguages.forEach(({ lang, extension, expectedContent, minLength }) => {
it(`should generate actual code stubs for ${lang}`, () => {
const resultJson = witValidator.generateBindings(TEST_WIT, lang, undefined);
const resultJson = witValidator.generateBindings(TEST_WIT, lang, undefined, undefined, undefined);
const result = JSON.parse(resultJson);

// Verify files were generated
Expand Down Expand Up @@ -66,7 +66,7 @@ describe("Bindings Generation for All Languages", () => {
});

it(`should not generate only README files for ${lang}`, () => {
const resultJson = witValidator.generateBindings(TEST_WIT, lang, undefined);
const resultJson = witValidator.generateBindings(TEST_WIT, lang, undefined, undefined, undefined);
const result = JSON.parse(resultJson);

// Verify that not all files are README files
Expand All @@ -90,7 +90,7 @@ describe("Bindings Generation for All Languages", () => {
const results: Record<string, Record<string, string>> = {};

for (const { lang } of supportedLanguages) {
const resultJson = witValidator.generateBindings(TEST_WIT, lang, undefined);
const resultJson = witValidator.generateBindings(TEST_WIT, lang, undefined, undefined, undefined);
results[lang] = JSON.parse(resultJson);
}

Expand Down Expand Up @@ -149,7 +149,7 @@ world app {
`;

for (const { lang, extension } of supportedLanguages) {
const resultJson = witValidator.generateBindings(complexWit, lang, undefined);
const resultJson = witValidator.generateBindings(complexWit, lang, undefined, undefined, undefined);
const result = JSON.parse(resultJson);

expect(Object.keys(result).length).toBeGreaterThan(0);
Expand Down
11 changes: 11 additions & 0 deletions tests/grammar/issue-121/wit/a.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package local:demo;

world my-world {
import host;

export another-interface;
}

interface host {
// ...
}
3 changes: 3 additions & 0 deletions tests/grammar/issue-121/wit/b.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
interface another-interface {
// ...
}
Loading
Loading