diff --git a/crates/xtask/src/build_wasm.rs b/crates/xtask/src/build_wasm.rs index fbb231ce..59d6ceb4 100644 --- a/crates/xtask/src/build_wasm.rs +++ b/crates/xtask/src/build_wasm.rs @@ -260,9 +260,12 @@ fn build_wasm(cmd: &mut Command, edit_tsd: bool) -> Result<()> { "undefined, localScope?: any | undefined, handle?: number | undefined): any" ), concat!( - "loadWebAssemblyModule(binary: Uint8Array | WebAssembly.Module, flags: Record,", + "loadWebAssemblyModule(binary: Uint8Array | WebAssembly.Module, flags: { loadAsync: true } & Record,", " libName?: string, localScope?: Record, handle?: number):", - " Promise number>>" + " Promise number>>;\n", + " function loadWebAssemblyModule(binary: Uint8Array | WebAssembly.Module, flags: { loadAsync: false } & Record,", + " libName?: string, localScope?: Record, handle?: number):", + " Record number>" ), ) .replace( diff --git a/lib/binding_web/README.md b/lib/binding_web/README.md index 08a939dc..ebf65378 100644 --- a/lib/binding_web/README.md +++ b/lib/binding_web/README.md @@ -209,6 +209,18 @@ const Parser = require('web-tree-sitter'); })(); ``` +### Loading a pre-compiled WebAssembly module + +In some environments, such as Cloudflare Workers and Vercel Edge Functions, you can import WebAssembly modules directly. +Modules can be loaded synchronously using the `Language.loadSync` method: + +```javascript +import treeSitterJavaScript from 'tree-sitter-javascript.wasm'; +// treeSitterJavaScript is of type `WebAssembly.Module` +const JavaScript = Language.loadSync(treeSitterJavaScript); +parser.setLanguage(JavaScript); +``` + ### Running .wasm in browser `web-tree-sitter` can run in the browser, but there are some common pitfalls. diff --git a/lib/binding_web/lib/web-tree-sitter.d.ts b/lib/binding_web/lib/web-tree-sitter.d.ts index c1e0e0dd..797da885 100644 --- a/lib/binding_web/lib/web-tree-sitter.d.ts +++ b/lib/binding_web/lib/web-tree-sitter.d.ts @@ -23,7 +23,8 @@ declare namespace RuntimeExports { * @param {Object=} localScope * @param {number=} handle */ - function loadWebAssemblyModule(binary: Uint8Array | WebAssembly.Module, flags: Record, libName?: string, localScope?: Record, handle?: number): Promise number>>; + function loadWebAssemblyModule(binary: Uint8Array | WebAssembly.Module, flags: { loadAsync: true } & Record, libName?: string, localScope?: Record, handle?: number): Promise number>>; + function loadWebAssemblyModule(binary: Uint8Array | WebAssembly.Module, flags: { loadAsync: false } & Record, libName?: string, localScope?: Record, handle?: number): Record number>; /** * @param {number} ptr * @param {string} type diff --git a/lib/binding_web/src/language.ts b/lib/binding_web/src/language.ts index 664e355b..99e439b8 100644 --- a/lib/binding_web/src/language.ts +++ b/lib/binding_web/src/language.ts @@ -265,4 +265,21 @@ export class Language { const languageAddress = mod[functionName](); return new Language(INTERNAL, languageAddress); } + + /** + * Load a language synchronously from a pre-compiled WebAssembly module. + * Use this method when you have already compiled the WebAssembly module yourself. + */ + static loadSync(module: WebAssembly.Module): Language { + const mod = C.loadWebAssemblyModule(module, { loadAsync: false }); + const symbolNames = Object.keys(mod); + const functionName = symbolNames.find((key) => LANGUAGE_FUNCTION_REGEX.test(key) && + !key.includes('external_scanner_')); + if (!functionName) { + console.log(`Couldn't find language function in Wasm file. Symbols:\n${JSON.stringify(symbolNames, null, 2)}`); + throw new Error('Language.loadSync failed: no language function found in Wasm file'); + } + const languageAddress = mod[functionName](); + return new Language(INTERNAL, languageAddress); + } } diff --git a/lib/binding_web/test/language.test.ts b/lib/binding_web/test/language.test.ts index bda09a29..f4d251e4 100644 --- a/lib/binding_web/test/language.test.ts +++ b/lib/binding_web/test/language.test.ts @@ -1,13 +1,37 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import helper from './helper'; -import type { LookaheadIterator, Language } from '../src'; +import helper, { type LanguageName } from './helper'; +import { LookaheadIterator, Language } from '../src'; import { Parser } from '../src'; +import { readFile } from 'fs/promises'; let JavaScript: Language; let Rust: Language; +let languageURL: (name: LanguageName) => string; describe('Language', () => { - beforeAll(async () => ({ JavaScript, Rust } = await helper)); + beforeAll(async () => ({ JavaScript, Rust, languageURL } = await helper)); + + describe('.loadSync', () => { + it('loads a language synchronously from a pre-compiled WebAssembly.Module', async () => { + const wasmPath = languageURL('javascript'); + const wasmBytes = await readFile(wasmPath); + const wasmModule = await WebAssembly.compile(wasmBytes); + + const lang = Language.loadSync(wasmModule); + expect(lang.name).toBe('javascript'); + expect(lang.abiVersion).toBe(15); + + // Verify the language actually works by parsing a snippet + const parser = new Parser(); + parser.setLanguage(lang); + const tree = parser.parse('const x = 1;'); + expect(tree).not.toBeNull(); + expect(tree!.rootNode.type).toBe('program'); + expect(tree!.rootNode.childCount).toBe(1); + expect(tree!.rootNode.firstChild!.type).toBe('lexical_declaration'); + parser.delete(); + }); + }); describe('.name, .version', () => { it('returns the name and version of the language', () => {