Add react-native support #63

Merged
andras merged 6 commits from asch/wasm2js into main 2026-05-31 20:28:20 +01:00
5 changed files with 72 additions and 7 deletions
Showing only changes of commit 2623959c10 - Show all commits

Fix diff
Some checks failed
Check / build (pull_request) Failing after 3m57s

Andras Schmelczer 2026-05-31 19:53:33 +01:00

View file

@ -84,8 +84,8 @@ See the [example website source](examples/website/src/index.ts) for a more compl
React Native's default engine, Hermes, does not expose a runtime `WebAssembly` React Native's default engine, Hermes, does not expose a runtime `WebAssembly`
global, so the WebAssembly build cannot run there. For React Native, the package global, so the WebAssembly build cannot run there. For React Native, the package
ships a [`wasm2js`](https://github.com/WebAssembly/binaryen) (pure-JavaScript) ships a pure-JavaScript build produced by [Binaryen's `wasm2js`](https://github.com/WebAssembly/binaryen)
build via its `react-native` entry point. via its `react-native` entry point.
### Python ### Python

View file

@ -33,7 +33,7 @@
], ],
"scripts": { "scripts": {
"build": "node scripts/build-rn.mjs && webpack --mode production", "build": "node scripts/build-rn.mjs && webpack --mode production",
"format": "prettier --write \"./**/*.(ts|scss|json|html)\"", "format": "prettier --write \"./**/*.(ts|mjs|scss|json|html)\"",
"test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest" "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest"
}, },
"devDependencies": { "devDependencies": {

View file

@ -27,7 +27,7 @@ export interface WasmBackend {
ensureReady(): void; ensureReady(): void;
} }
// Define the enum values as const arrays to avoid duplication // Define the enum values as a const array to avoid duplication
const BUILTIN_TOKENIZERS = ['Character', 'Line', 'Markdown', 'Word'] as const; const BUILTIN_TOKENIZERS = ['Character', 'Line', 'Markdown', 'Word'] as const;
/** /**
@ -148,7 +148,8 @@ export interface ReconcileApi {
* @param left - The left version of the text (either string or TextWithCursors with cursor positions) * @param left - The left version of the text (either string or TextWithCursors with cursor positions)
* @param right - The right version of the text (either string or TextWithCursors with cursor positions) * @param right - The right version of the text (either string or TextWithCursors with cursor positions)
* @param tokenizer - The tokenisation strategy: "Word" (default, recommended for prose), * @param tokenizer - The tokenisation strategy: "Word" (default, recommended for prose),
* "Character" (fine-grained), or "Line" (similar to git merge) * "Character" (fine-grained), "Line" (similar to git merge), or
* "Markdown" (splits on Markdown structure)
* @returns The reconciled text with automatically repositioned cursor positions * @returns The reconciled text with automatically repositioned cursor positions
* *
* @example * @example
@ -219,7 +220,8 @@ export interface ReconcileApi {
* @param left - The left version of the text (either string or TextWithCursors with cursor positions) * @param left - The left version of the text (either string or TextWithCursors with cursor positions)
* @param right - The right version of the text (either string or TextWithCursors with cursor positions) * @param right - The right version of the text (either string or TextWithCursors with cursor positions)
* @param tokenizer - The tokenisation strategy: "Word" (default, recommended for prose), * @param tokenizer - The tokenisation strategy: "Word" (default, recommended for prose),
* "Character" (fine-grained), or "Line" (similar to git merge) * "Character" (fine-grained), "Line" (similar to git merge), or
* "Markdown" (splits on Markdown structure)
* @returns The reconciled text with cursor positions and detailed change history * @returns The reconciled text with cursor positions and detailed change history
* *
* @example * @example
@ -351,7 +353,15 @@ export function makeReconcileApi(backend: WasmBackend): ReconcileApi {
backend.ensureReady(); backend.ensureReady();
assertTokenizer(tokenizer); assertTokenizer(tokenizer);
return backend.undiff(original, diffValue, tokenizer); // The real-WebAssembly backend's `diff` emits BigInt spans, whereas the
// wasm2js (React Native) backend rejects BigInt outright. Normalise to
// plain numbers - exactly as `diff` does on the way out - so a `diff`
// result round-trips through `undiff` identically on every platform.
return backend.undiff(
original,
diffValue.map((item) => (typeof item === 'bigint' ? Number(item) : item)),
tokenizer
);
} }
function reconcileWithHistory( function reconcileWithHistory(

View file

@ -60,6 +60,46 @@ describe('reconcile (wasm2js / React Native build)', () => {
expect(result.text).toEqual('Hi world'); expect(result.text).toEqual('Hi world');
expect(result.history.length).toBeGreaterThan(0); expect(result.history.length).toBeGreaterThan(0);
}); });
it('undiff accepts bigint entries (per the Array<number | bigint | string> type)', () => {
const original = 'Hello world';
const changed = 'Hello cruel world';
// `diff` returns plain numbers; emulate a caller that supplies BigInt, which
// the public signature permits. The wasm2js (React Native) build rejects raw
// BigInt, so the wrapper must normalise it to keep both backends identical.
const withBigints = diff(original, changed).map((item) =>
typeof item === 'number' ? BigInt(item) : item
);
expect(withBigints.some((item) => typeof item === 'bigint')).toBe(true);
expect(undiff(original, withBigints)).toEqual(changed);
});
it('runs every operation with no WebAssembly global (Hermes parity)', () => {
// Hermes exposes no `WebAssembly` global - the entire reason the React
// Native entry point links a wasm2js (pure-JS) build instead of the real
// module. Remove the global to prove the operations never reach for it.
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'WebAssembly');
delete (globalThis as { WebAssembly?: unknown }).WebAssembly;
try {
expect((globalThis as { WebAssembly?: unknown }).WebAssembly).toBeUndefined();
expect(reconcile('Hello', 'Hello world', 'Hi world').text).toEqual('Hi world');
const changes = diff('Hello world', 'Hello cruel world');
expect(undiff('Hello world', changes)).toEqual('Hello cruel world');
expect(
reconcileWithHistory('Hello', 'Hello world', 'Hi world').history.length
).toBeGreaterThan(0);
} finally {
// Restore the global so the leak check and later suites are unaffected.
if (descriptor) {
Object.defineProperty(globalThis, 'WebAssembly', descriptor);
}
}
});
}); });
describe('test_diff_and_undiff_are_inverse (wasm2js / React Native build)', () => { describe('test_diff_and_undiff_are_inverse (wasm2js / React Native build)', () => {

View file

@ -60,6 +60,21 @@ describe('reconcile', () => {
expect(result.text).toEqual('Hi world'); expect(result.text).toEqual('Hi world');
expect(result.history.length).toBeGreaterThan(0); expect(result.history.length).toBeGreaterThan(0);
}); });
it('undiff accepts bigint entries (per the Array<number | bigint | string> type)', () => {
const original = 'Hello world';
const changed = 'Hello cruel world';
// `diff` returns plain numbers; emulate a caller that supplies BigInt, which
// the public signature permits. The wasm2js (React Native) build rejects raw
// BigInt, so the wrapper must normalise it to keep both backends identical.
const withBigints = diff(original, changed).map((item) =>
typeof item === 'number' ? BigInt(item) : item
);
expect(withBigints.some((item) => typeof item === 'bigint')).toBe(true);
expect(undiff(original, withBigints)).toEqual(changed);
});
}); });
describe('test_diff_and_undiff_are_inverse', () => { describe('test_diff_and_undiff_are_inverse', () => {