Add TS wrapper package

This commit is contained in:
Andras Schmelczer 2025-07-04 02:11:16 +01:00
parent 373e7d03f4
commit f0ff720577
6 changed files with 5162 additions and 0 deletions

View file

@ -0,0 +1,3 @@
module.exports = {
preset: "ts-jest/presets/js-with-babel-esm"
};

4905
reconcile-js/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

24
reconcile-js/package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "reconcile",
"version": "0.4.0",
"main": "dist/index.js",
"types": "dist/types/index.d.ts",
"files": [
"dist/*"
],
"scripts": {
"build": "webpack --mode production",
"test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
"reconcile": "file:../pkg",
"ts-jest": "^29.3.4",
"ts-loader": "^9.5.2",
"tslib": "2.8.1",
"typescript": "5.8.3",
"webpack": "^5.99.9",
"webpack-cli": "^6.0.1"
}
}

183
reconcile-js/src/index.ts Normal file
View file

@ -0,0 +1,183 @@
import wasmInit, {
CursorPosition as wasmCursorPosition,
reconcile as wasmReconcile,
TextWithCursors as wasmTextWithCursors,
TextWithHistory as wasmTextWithHistory,
BuiltinTokenizer,
reconcileWithHistory as wasmReconcileWithHistory,
History,
InitInput,
} from "reconcile";
export interface TextWithCursors {
/** The document's entire content */
text: string;
/** List of cursor positions, can be null or undefined if there are no cursors */
cursors: null | undefined | CursorPosition[];
}
/**
* Represents a cursor position with a unique identifier.
*/
export interface CursorPosition {
/** Unique identifier for the cursor */
id: number;
/** Character position in the text, 0-based */
position: number;
}
export interface TextWithCursorsAndHistory {
/** The document's entire content */
text: string;
/** List of cursor positions, can be null or undefined if there are no cursors */
cursors: null | undefined | CursorPosition[];
/** List of operations leading to `text` from the 3 ancestors */
history: TextWithHistory[];
}
export interface TextWithHistory {
/** Span of text associated with the historical opearion */
text: string;
/** Origin of the `text` span */
history: History;
}
/**
* Supported tokenizer types for text processing.
*/
export type Tokenizer = "word" | "character";
let isInitialised = false;
/**
* Initializes the WASM module for text reconciliation.
* Must be called before using any other functions.
*
* The function is idempotent.
*
* @param content - Optional initialization input for the WASM module during testing.
* @returns Promise that resolves when initialization is complete
*/
export async function init(content?: InitInput) {
if (isInitialised) {
return;
}
await wasmInit(content);
isInitialised = true;
}
/**
* Merges three versions of text (original, left, right) and cursor positions.
*
* @param original - The original/base version of the text
* @param left - The left version of the text, either as string or TextWithCursors
* @param right - The right version of the text, either as string or TextWithCursors
* @param tokenizer - The tokenization strategy to use (default: "Word")
* @returns The reconciled text with merged cursor positions
*/
export function reconcile(
original: string,
left: string | TextWithCursors,
right: string | TextWithCursors,
tokenizer: BuiltinTokenizer = "Word"
): TextWithCursors {
const leftCursor = toWasmTextWithCursors(left);
const rightCursor = toWasmTextWithCursors(right);
const result = wasmReconcile(original, leftCursor, rightCursor, tokenizer);
leftCursor.free();
rightCursor.free();
const jsResult = toTextWithCursors(result);
result.free();
return jsResult;
}
/**
* Merges three versions of text and returns the result with historical information.
*
* Calculating the `history` is somewhat more expensive, otherwise this function behaves like `reconcile`.
*
* @param original - The original/base version of the text
* @param left - The left version of the text, either as string or TextWithCursors
* @param right - The right version of the text, either as string or TextWithCursors
* @param tokenizer - The tokenization strategy to use (default: "Word")
* @returns The reconciled text with cursor positions and history of changes
*/
export function reconcileWithHistory(
original: string,
left: string | TextWithCursors,
right: string | TextWithCursors,
tokenizer: BuiltinTokenizer = "Word"
): TextWithCursorsAndHistory {
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(toTextWithHistory);
result.free();
return {
...jsResult,
history,
};
}
function toWasmTextWithCursors(
text: string | TextWithCursors
): 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 {
return {
text: textWithCursor.text(),
cursors: textWithCursor.cursors().map(toCursorPosition),
};
}
function toCursorPosition(cursor: wasmCursorPosition): CursorPosition {
return {
id: cursor.id(),
position: cursor.characterPosition(),
};
}
function toTextWithHistory(
textWithHistory: wasmTextWithHistory
): TextWithHistory {
return {
text: textWithHistory.text(),
history: textWithHistory.history(),
};
}

View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "ESNext",
"target": "ESNext",
"strict": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "bundler",
"declaration": true,
"declarationDir": "./dist/types"
},
"exclude": [
"./dist"
]
}

View file

@ -0,0 +1,33 @@
const path = require("path");
const webpack = require("webpack");
const packageJson = require("./package.json");
module.exports = {
entry: "./src/index.ts",
module: {
rules: [
{
test: /\.ts$/,
use: ["ts-loader"]
},
{
test: /\.wasm$/,
type: "asset/inline"
}
]
},
resolve: {
extensions: [".ts"],
alias: {
root: __dirname,
src: path.resolve(__dirname, "src")
}
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "index.js",
libraryTarget: "commonjs2"
},
};