Skip to content

Commit 8a61187

Browse files
fix: use globalThis instead of window for non-browser compatibility (#150)
## Summary - Replace `window` with `globalThis` in the ESM loader so `arboriumHost` works in Node.js, Deno, and workers (not just browsers) - Add `registerGrammar()` API that lets users pre-load grammar modules, bypassing CDN/dynamic-import resolution entirely ## `registerGrammar` usage ```ts // Deno import * as pythonGrammar from "npm:@arborium/python"; import { readFile } from "node:fs/promises"; const wasm = await readFile("node_modules/@arborium/python/grammar_bg.wasm"); const grammar = await registerGrammar(pythonGrammar, wasm); const html = await grammar.highlight("print('hello')"); ``` ## Test plan - [ ] Verify browser behavior unchanged (`globalThis === window` in browsers) - [ ] Verify `registerGrammar` works in Deno with `npm:` specifiers Fixes #148 Fixes #149
1 parent 05fc249 commit 8a61187

File tree

4 files changed

+83
-6
lines changed

4 files changed

+83
-6
lines changed

crates/arborium-host/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
//!
1010
//! ## JS Interface
1111
//!
12-
//! The host expects these functions to be available on `window.arboriumHost`:
12+
//! The host expects these functions to be available on `globalThis.arboriumHost`:
1313
//!
1414
//! ```javascript
15-
//! window.arboriumHost = {
15+
//! globalThis.arboriumHost = {
1616
//! // Check if a language is available (sync, for fast rejection).
1717
//! isLanguageAvailable(language) { ... },
1818
//!

packages/arborium/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export {
99
getConfig,
1010
highlight,
1111
loadGrammar,
12+
registerGrammar,
1213
setConfig,
1314
getAvailableLanguages,
1415
isLanguageAvailable,

packages/arborium/src/loader.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,9 @@ async function loadGrammarPluginInner(
273273
const handleToPlugin = new Map<number, GrammarPlugin>();
274274
let nextHandle = 1;
275275

276-
/** Setup window.arboriumHost for the Rust host to call into */
276+
/** Setup globalThis.arboriumHost for the Rust host to call into */
277277
function setupHostInterface(config: Required<ArboriumConfig>): void {
278-
(window as any).arboriumHost = {
278+
(globalThis as any).arboriumHost = {
279279
/** Check if a language is available (sync) */
280280
isLanguageAvailable(language: string): boolean {
281281
return knownLanguages.has(language) || grammarCache.has(language);
@@ -427,6 +427,82 @@ export async function loadGrammar(
427427
};
428428
}
429429

430+
/**
431+
* Register a pre-loaded grammar module, bypassing CDN resolution.
432+
*
433+
* Use this in Node.js, Deno, or other non-browser environments where
434+
* dynamic `import()` of CDN URLs isn't available.
435+
*
436+
* @example
437+
* ```ts
438+
* // Deno
439+
* import * as pythonGrammar from "npm:@arborium/python";
440+
* import { readFile } from "node:fs/promises";
441+
* const wasm = await readFile("node_modules/@arborium/python/grammar_bg.wasm");
442+
* const grammar = await registerGrammar(pythonGrammar, wasm);
443+
* const html = await grammar.highlight("print('hello')");
444+
* ```
445+
*/
446+
export async function registerGrammar(
447+
jsModule: unknown,
448+
wasmSource: Response | BufferSource | WebAssembly.Module,
449+
configOverrides?: ArboriumConfig,
450+
): Promise<Grammar> {
451+
const config = getConfig(configOverrides);
452+
const module = jsModule as WasmBindgenPlugin;
453+
454+
await module.default({ module_or_path: wasmSource });
455+
456+
const language = module.language_id();
457+
const injectionLanguages = module.injection_languages();
458+
459+
const plugin: GrammarPlugin = {
460+
languageId: language,
461+
injectionLanguages,
462+
module,
463+
parseUtf8: (text: string) => {
464+
const session = module.create_session();
465+
try {
466+
module.set_text(session, text);
467+
const result = module.parse(session);
468+
return {
469+
spans: result.spans || [],
470+
injections: result.injections || [],
471+
};
472+
} catch (e) {
473+
config.logger.error(`[arborium] Parse error:`, e);
474+
return { spans: [], injections: [] };
475+
} finally {
476+
module.free_session(session);
477+
}
478+
},
479+
parseUtf16: (text: string) => {
480+
const session = module.create_session();
481+
try {
482+
module.set_text(session, text);
483+
const result = module.parse_utf16(session);
484+
return {
485+
spans: result.spans || [],
486+
injections: result.injections || [],
487+
};
488+
} catch (e) {
489+
config.logger.error(`[arborium] Parse error:`, e);
490+
return { spans: [], injections: [] };
491+
} finally {
492+
module.free_session(session);
493+
}
494+
},
495+
};
496+
497+
grammarCache.set(language, plugin);
498+
knownLanguages.add(language);
499+
config.logger.debug(`[arborium] Grammar '${language}' registered`);
500+
501+
// loadGrammar will find it in the cache and wrap it as a public Grammar
502+
const grammar = await loadGrammar(language, configOverrides);
503+
return grammar!;
504+
}
505+
430506
/** Get current config, optionally merging with overrides */
431507
export function getConfig(overrides?: Partial<ArboriumConfig>): Required<ArboriumConfig> {
432508
if (overrides) {

xtask/src/generate.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2542,10 +2542,10 @@ Uses wasm-bindgen for JavaScript interop.
25422542
25432543
## How It Works
25442544
2545-
The host expects these functions on `window.arboriumHost`:
2545+
The host expects these functions on `globalThis.arboriumHost`:
25462546
25472547
```javascript
2548-
window.arboriumHost = {
2548+
globalThis.arboriumHost = {
25492549
// Check if a language is available (sync)
25502550
isLanguageAvailable(language) { ... },
25512551

0 commit comments

Comments
 (0)