This commit is contained in:
Andras Schmelczer 2026-05-31 16:57:03 +01:00
parent 17a96be0fc
commit a0fcf1314a
8 changed files with 234 additions and 335 deletions

3
.gitignore vendored
View file

@ -10,5 +10,8 @@ node_modules
# WebPack build output # WebPack build output
dist dist
# Generated wasm-bindgen bundler + wasm2js output for the React Native build
pkg-rn
# Python virtual environment # Python virtual environment
.venv .venv

View file

@ -80,6 +80,13 @@ console.log(result.text); // "Hi beautiful world"
See the [example website source](examples/website/src/index.ts) for a more complex example, or the [advanced examples document](docs/advanced-ts.md). See the [example website source](examples/website/src/index.ts) for a more complex example, or the [advanced examples document](docs/advanced-ts.md).
#### React Native (Hermes)
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
ships a [`wasm2js`](https://github.com/WebAssembly/binaryen) (pure-JavaScript)
build via its `react-native` entry point.
### Python ### Python
Install via uv or pip: Install via uv or pip:

View file

@ -10,6 +10,7 @@
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"binaryen": "^123.0.0",
"jest": "^30.3.0", "jest": "^30.3.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"reconcile-text": "file:../pkg", "reconcile-text": "file:../pkg",
@ -65,6 +66,7 @@
"version": "7.28.0", "version": "7.28.0",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
@ -1656,6 +1658,7 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -1682,6 +1685,7 @@
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@ -1908,6 +1912,24 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/binaryen": {
"version": "123.0.0",
"resolved": "https://registry.npmjs.org/binaryen/-/binaryen-123.0.0.tgz",
"integrity": "sha512-/hls/a309aZCc0itqP6uhoR+5DsKSlJVfB8Opd2BY9Ndghs84IScTunlyidyF4r2Xe3lQttnfBNIDjaNpj6mTw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"wasm-as": "bin/wasm-as",
"wasm-ctor-eval": "bin/wasm-ctor-eval",
"wasm-dis": "bin/wasm-dis",
"wasm-merge": "bin/wasm-merge",
"wasm-metadce": "bin/wasm-metadce",
"wasm-opt": "bin/wasm-opt",
"wasm-reduce": "bin/wasm-reduce",
"wasm-shell": "bin/wasm-shell",
"wasm2js": "bin/wasm2js"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@ -1950,6 +1972,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@ -3053,6 +3076,7 @@
"integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@jest/core": "30.3.0", "@jest/core": "30.3.0",
"@jest/types": "30.3.0", "@jest/types": "30.3.0",
@ -4936,6 +4960,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -5072,6 +5097,7 @@
"integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8", "@types/estree": "^1.0.8",
@ -5119,6 +5145,7 @@
"version": "6.0.1", "version": "6.0.1",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@discoveryjs/json-ext": "^0.6.1", "@discoveryjs/json-ext": "^0.6.1",
"@webpack-cli/configtest": "^3.0.1", "@webpack-cli/configtest": "^3.0.1",

View file

@ -4,6 +4,7 @@
"description": "Intelligent 3-way text merging with automated conflict resolution", "description": "Intelligent 3-way text merging with automated conflict resolution",
"main": "dist/reconcile.node.js", "main": "dist/reconcile.node.js",
"browser": "dist/reconcile.web.js", "browser": "dist/reconcile.web.js",
"react-native": "dist/reconcile.rn.js",
"keywords": [ "keywords": [
"text editing", "text editing",
"sync", "sync",
@ -31,12 +32,13 @@
"dist/**/*" "dist/**/*"
], ],
"scripts": { "scripts": {
"build": "webpack --mode production", "build": "node scripts/build-rn.mjs && webpack --mode production",
"format": "prettier --write \"./**/*.(ts|scss|json|html)\"", "format": "prettier --write \"./**/*.(ts|scss|json|html)\"",
"test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest" "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"binaryen": "^123.0.0",
"jest": "^30.3.0", "jest": "^30.3.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"reconcile-text": "file:../pkg", "reconcile-text": "file:../pkg",

View file

@ -0,0 +1,95 @@
import { reconcile, reconcileWithHistory, diff, undiff } from './index.rn';
import { installWasmLeakDetector, checkForWasmLeaks } from './wasm-leak-detector';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
installWasmLeakDetector();
afterEach(() => {
const leaks = checkForWasmLeaks();
if (leaks.length > 0) {
throw new Error(
`WASM memory leak: ${leaks.length} object(s) not freed:\n ${leaks.join('\n ')}`
);
}
});
describe('reconcile (wasm2js / React Native build)', () => {
it('call reconcile without cursors', () => {
expect(reconcile('Hello', 'Hello world', 'Hi world').text).toEqual('Hi world');
});
it('call reconcile with cursors', () => {
const result = reconcile(
'Hello',
{
text: 'Hello world',
cursors: [
{
id: 3,
position: 2,
},
],
},
{
text: 'Hi world',
cursors: [
{
id: 4,
position: 0,
},
{ id: 5, position: 3 },
],
}
);
expect(result.text).toEqual('Hi world');
expect(result.cursors).toEqual([
{ id: 3, position: 0 },
{ id: 4, position: 0 },
{ id: 5, position: 3 },
]);
});
it('call reconcileWithHistory', () => {
const result = reconcileWithHistory('Hello', 'Hello world', 'Hi world');
expect(result.text).toEqual('Hi world');
expect(result.history.length).toBeGreaterThan(0);
});
});
describe('test_diff_and_undiff_are_inverse (wasm2js / React Native build)', () => {
const resourcesPath = path.join(__dirname, '../../tests/resources');
const readFileSlice = (fileName: string, start: number, end: number): string => {
const filePath = path.join(resourcesPath, fileName);
const content = fs.readFileSync(filePath, 'utf-8');
const chars = Array.from(content); // Handle unicode properly
return chars.slice(start, Math.min(end, chars.length)).join('');
};
const files = ['pride_and_prejudice.txt', 'room_with_a_view.txt', 'blns.txt'];
const ranges = [{ start: 0, end: 50000 }];
files.forEach((file1) => {
files.forEach((file2) => {
ranges.forEach((range1) => {
ranges.forEach((range2) => {
it(`should diff & undiff ${file1}[${range1.start}..${range1.end}], ${file2}[${range2.start}..${range2.end}] without panic`, () => {
const content1 = readFileSlice(file1, range1.start, range1.end);
const content2 = readFileSlice(file2, range2.start, range2.end);
const changes = diff(content1, content2);
const actual = undiff(content1, changes);
expect(actual).toEqual(content2);
});
});
});
});
});
});

View file

@ -0,0 +1,47 @@
// React Native entrypoint (resolved via the `react-native` package field).
//
// Hermes — the default React Native engine since RN 0.84 / Expo SDK 56 — does
// not expose a runtime `WebAssembly` global, so the normal `new
// WebAssembly.Module(...)` path used by `index.ts` throws
// `ReferenceError: Property 'WebAssembly' doesn't exist`.
//
// Instead we link a wasm2js translation of the module: pure JavaScript that
// needs no `WebAssembly` global and is instantiated synchronously at import
// time. The public API and its synchronous signatures are unchanged, so
// callers need no modification. The `pkg-rn` directory is generated by
// `scripts/build-rn.mjs`.
import {
CursorPosition as wasmCursorPosition,
TextWithCursors as wasmTextWithCursors,
reconcile as wasmReconcile,
reconcileWithHistory as wasmReconcileWithHistory,
diff as wasmDiff,
undiff as wasmUndiff,
} from '../pkg-rn/reconcile_text.js';
import { makeReconcileApi, type WasmBackend } from './core';
const backend: WasmBackend = {
CursorPosition: wasmCursorPosition,
TextWithCursors: wasmTextWithCursors,
reconcile: wasmReconcile,
reconcileWithHistory: wasmReconcileWithHistory,
diff: wasmDiff,
undiff: wasmUndiff,
// The wasm2js module initialises itself at import time, so this is a no-op.
ensureReady() {},
};
export const { reconcile, diff, undiff, reconcileWithHistory } =
makeReconcileApi(backend);
export type {
BuiltinTokenizer,
History,
CursorPosition,
TextWithCursors,
TextWithOptionalCursors,
TextWithCursorsAndHistory,
SpanWithHistory,
} from './core';

View file

@ -1,8 +1,7 @@
import { import {
CursorPosition as wasmCursorPosition, CursorPosition as wasmCursorPosition,
reconcile as wasmReconcile,
TextWithCursors as wasmTextWithCursors, TextWithCursors as wasmTextWithCursors,
SpanWithHistory as wasmSpanWithHistory, reconcile as wasmReconcile,
reconcileWithHistory as wasmReconcileWithHistory, reconcileWithHistory as wasmReconcileWithHistory,
diff as wasmDiff, diff as wasmDiff,
undiff as wasmUndiff, undiff as wasmUndiff,
@ -11,341 +10,40 @@ import {
import wasmBytes from 'reconcile-text/reconcile_text_bg.wasm'; import wasmBytes from 'reconcile-text/reconcile_text_bg.wasm';
// Define the enum values as const arrays to avoid duplication import { makeReconcileApi, type WasmBackend } from './core';
const BUILTIN_TOKENIZERS = ['Character', 'Line', 'Markdown', 'Word'] as const;
const HISTORY_VALUES = [
'Unchanged',
'AddedFromLeft',
'AddedFromRight',
'RemovedFromLeft',
'RemovedFromRight',
] as const;
/**
* Tokenisation strategies for text merging.
*
* These correspond to the built-in tokenizers available in the underlying WASM module.
*/
export type BuiltinTokenizer = (typeof BUILTIN_TOKENIZERS)[number];
/**
* History classification for text spans in merge results.
*
* Indicates the origin of each text span in the merged document.
*/
export type History = (typeof HISTORY_VALUES)[number];
/**
* Represents a text document with associated cursor positions.
*
* This interface is used both as input to reconcile functions (to specify where
* cursors are positioned in the original documents) and as output (with cursors
* automatically repositioned after merging).
*/
export interface TextWithCursors {
/** The document's entire content as a string */
text: string;
/**
* Array of cursor positions within the text. Can be empty if there are no cursors to track.
* Each cursor has a unique ID and position.
*/
cursors: CursorPosition[];
}
/**
* Like `TextWithCursors`, but cursors may be null or undefined (treated as empty).
* Used as input where cursor tracking is optional.
*/
export interface TextWithOptionalCursors {
/** The document's entire content as a string */
text: string;
/**
* Array of cursor positions within the text. Can be null, undefined, or empty
* if there are no cursors to track. Each cursor has a unique ID and position.
*/
cursors: null | undefined | CursorPosition[];
}
/**
* Represents a cursor position within a text document.
*
* Cursors are automatically repositioned during text merging to maintain their
* relative positions as text is inserted, deleted, or modified around them.
*/
export interface CursorPosition {
/** Unique identifier for the cursor (can be any number, must be unique within the document) */
id: number;
/** Character position in the text, 0-based index from the beginning of the document */
position: number;
}
/**
* Represents a merged text document with cursor positions and detailed change history.
*
* This is the return type of `reconcileWithHistory()` and provides complete information
* about how the merge was performed, including which parts of the final text came from
* which source documents.
*/
export interface TextWithCursorsAndHistory {
/** The merged document's entire content */
text: string;
/**
* Array of cursor positions within the merged text. Can be empty if there are no cursors to track.
* All cursors are automatically repositioned from the left and right documents.
*/
cursors: CursorPosition[];
/**
* Detailed provenance information showing the origin of each text span in the result.
* Each span indicates whether it was unchanged, added from left, added from right, etc.
*/
history: SpanWithHistory[];
}
/**
* Represents a span of text in the merged result with its change history.
*
* This shows exactly which source document contributed each piece of text to the
* final merged result. Useful for understanding merge decisions and creating
* visualisations of how documents were combined.
*/
export interface SpanWithHistory {
/** The text content of this span */
text: string;
/** The origin of this text span in the merge result */
history: History;
}
const UNSUPPORTED_TOKENIZER_ERROR = `Unsupported tokenizer, only ${BUILTIN_TOKENIZERS.join(
', '
)} are supported`;
let isInitialised = false; let isInitialised = false;
/** const backend: WasmBackend = {
* Merges three versions of text using intelligent conflict resolution. CursorPosition: wasmCursorPosition,
* TextWithCursors: wasmTextWithCursors,
* This is the primary function for 3-way text merging. Unlike traditional merge tools reconcile: wasmReconcile,
* that produce conflict markers, this function automatically resolves conflicts by reconcileWithHistory: wasmReconcileWithHistory,
* applying both sets of changes where possible. diff: wasmDiff,
* undiff: wasmUndiff,
* @param original - The original/base version of the text that both sides diverged from ensureReady() {
* @param left - The left version of the text (either string or TextWithCursors with cursor positions) if (isInitialised) {
* @param right - The right version of the text (either string or TextWithCursors with cursor positions) return;
* @param tokenizer - The tokenisation strategy: "Word" (default, recommended for prose), }
* "Character" (fine-grained), or "Line" (similar to git merge)
* @returns The reconciled text with automatically repositioned cursor positions
*
* @example
* ```typescript
* const original = "Hello world";
* const left = "Hello beautiful world"; // Added "beautiful"
* const right = "Hi world"; // Changed "Hello" to "Hi"
*
* const result = reconcile(original, left, right);
* console.log(result.text); // "Hi beautiful world"
* ```
*/
export function reconcile(
original: string,
left: string | TextWithOptionalCursors,
right: string | TextWithOptionalCursors,
tokenizer: BuiltinTokenizer = 'Word'
): TextWithCursors {
init();
if (!BUILTIN_TOKENIZERS.includes(tokenizer)) { const wasmBinary = Uint8Array.from(atob(wasmBytes as unknown as string), (c) =>
throw new Error(UNSUPPORTED_TOKENIZER_ERROR); c.charCodeAt(0)
} );
initSync({ module: wasmBinary });
const leftCursor = toWasmTextWithCursors(left); isInitialised = true;
const rightCursor = toWasmTextWithCursors(right); },
};
const result = wasmReconcile(original, leftCursor, rightCursor, tokenizer); export const { reconcile, diff, undiff, reconcileWithHistory } =
makeReconcileApi(backend);
leftCursor.free(); export type {
rightCursor.free(); BuiltinTokenizer,
History,
const jsResult = toTextWithCursors(result); CursorPosition,
result.free(); TextWithCursors,
TextWithOptionalCursors,
return jsResult; TextWithCursorsAndHistory,
} SpanWithHistory,
} from './core';
/**
* Generates a compact diff representation between an original and changed text.
*
* These can be parsed and unpacked using the `undiff` function or the Rust crate's EditedText::from_diff.
* Cursor positions are omitted from the diff result.
*
* This function computes the differences between two versions of text and returns
* a compact representation of those changes.
*
* @param original - The original/base version of the text
* @param changed - The modified version of the text (either string or TextWithCursors with cursor positions)
* @param tokenizer - The tokenisation strategy, which is the same as used in `reconcile`.
* @returns An array of inserts (strings), deletes (negative integers), and retained spans (positive integers).
*/
export function diff(
original: string,
changed: string | TextWithOptionalCursors,
tokenizer: BuiltinTokenizer = 'Word'
): Array<number | string> {
init();
if (!BUILTIN_TOKENIZERS.includes(tokenizer)) {
throw new Error(UNSUPPORTED_TOKENIZER_ERROR);
}
const changedWasm = toWasmTextWithCursors(changed);
const result = wasmDiff(original, changedWasm, tokenizer);
changedWasm.free();
return result.map((item) => (typeof item === 'bigint' ? Number(item) : item));
}
/**
* Applies a compact diff to an original text to reconstruct the changed version.
*
* This function takes an original text and a compact diff representation (as produced
* by the `diff` function) and reconstructs the modified text.
*
* @param original - The original/base version of the text
* @param diff - The compact diff array (inserts as strings, deletes as negative integers, retained spans as positive integers)
* @param tokenizer - The tokenisation strategy, which is the same as used in `reconcile`.
* @returns The reconstructed changed text as a string.
*/
export function undiff(
original: string,
diff: Array<number | bigint | string>,
tokenizer: BuiltinTokenizer = 'Word'
): string {
init();
if (!BUILTIN_TOKENIZERS.includes(tokenizer)) {
throw new Error(UNSUPPORTED_TOKENIZER_ERROR);
}
return wasmUndiff(original, diff, tokenizer);
}
/**
* Merges three versions of text and returns detailed provenance information.
*
* This function behaves like `reconcile()` but also provides
* detailed historical information about the origin of each text span in the result.
* This is valuable for understanding how the merge was performed and which changes
* came from which source.
*
* Note: Computing the history is computationally more expensive than the basic merge.
*
* @param original - The original/base version of the text that both sides diverged from
* @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 tokenizer - The tokenisation strategy: "Word" (default, recommended for prose),
* "Character" (fine-grained), or "Line" (similar to git merge)
* @returns The reconciled text with cursor positions and detailed change history
*
* @example
* ```typescript
* const original = "Hello world";
* const left = "Hello beautiful world";
* const right = "Hi world";
*
* const result = reconcileWithHistory(original, left, right);
* console.log(result.text); // "Hi beautiful world"
* console.log(result.history); // Array of SpanWithHistory objects showing change origins
* ```
*/
export function reconcileWithHistory(
original: string,
left: string | TextWithOptionalCursors,
right: string | TextWithOptionalCursors,
tokenizer: BuiltinTokenizer = 'Word'
): TextWithCursorsAndHistory {
init();
if (!BUILTIN_TOKENIZERS.includes(tokenizer)) {
throw new Error(UNSUPPORTED_TOKENIZER_ERROR);
}
const leftCursor = toWasmTextWithCursors(left);
const rightCursor = toWasmTextWithCursors(right);
const result = wasmReconcileWithHistory(original, leftCursor, rightCursor, tokenizer);
leftCursor.free();
rightCursor.free();
const jsResult = toTextWithCursors(result);
const history = result.history().map(toSpanWithHistory);
result.free();
return {
...jsResult,
history,
};
}
function init() {
if (isInitialised) {
return;
}
const wasmBinary = Uint8Array.from(atob(wasmBytes as unknown as string), (c) =>
c.charCodeAt(0)
);
initSync({ module: wasmBinary });
isInitialised = true;
}
function toWasmTextWithCursors(
text: string | TextWithOptionalCursors
): wasmTextWithCursors {
const isInputString = typeof text === 'string';
const leftText = isInputString ? text : text.text;
const leftCursors = isInputString ? [] : (text.cursors ?? []);
return new wasmTextWithCursors(leftText, leftCursors.map(toWasmCursorPosition));
}
function toWasmCursorPosition({ id, position }: CursorPosition): wasmCursorPosition {
return new wasmCursorPosition(id, position);
}
function toTextWithCursors(textWithCursor: wasmTextWithCursors): TextWithCursors {
const wasmCursors = textWithCursor.cursors();
const cursors = wasmCursors.map(toCursorPosition);
for (const cursor of wasmCursors) {
cursor.free();
}
return {
text: textWithCursor.text(),
cursors,
};
}
function toCursorPosition(cursor: wasmCursorPosition): CursorPosition {
return {
id: cursor.id(),
position: cursor.characterIndex(),
};
}
function toSpanWithHistory(span: wasmSpanWithHistory): SpanWithHistory {
const result = {
text: span.text(),
history: span.history(),
};
span.free();
return result;
}

View file

@ -2,7 +2,6 @@ const path = require('path');
const { merge } = require('webpack-merge'); const { merge } = require('webpack-merge');
const common = { const common = {
entry: './src/index.ts',
optimization: { optimization: {
// the consuming project should take care of minification // the consuming project should take care of minification
minimize: false, minimize: false,
@ -38,8 +37,10 @@ const common = {
}; };
module.exports = [ module.exports = [
// Web build: real WebAssembly, instantiated synchronously from inlined base64.
merge(common, { merge(common, {
target: 'web', target: 'web',
entry: './src/index.ts',
output: { output: {
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
filename: 'reconcile.web.js', filename: 'reconcile.web.js',
@ -50,12 +51,31 @@ module.exports = [
globalObject: 'this', globalObject: 'this',
}, },
}), }),
// Node build: real WebAssembly.
merge(common, { merge(common, {
target: 'node', target: 'node',
entry: './src/index.ts',
output: { output: {
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
filename: 'reconcile.node.js', filename: 'reconcile.node.js',
libraryTarget: 'commonjs2', libraryTarget: 'commonjs2',
}, },
}), }),
// React Native build: wasm2js (pure JS), for Hermes which has no
// `WebAssembly` global. Sources come from `pkg-rn/`
merge(common, {
target: 'web',
entry: './src/index.rn.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'reconcile.rn.js',
library: {
name: 'reconcile',
type: 'umd',
},
globalObject: 'this',
},
}),
]; ];