Make @docmost/git-sync natively consumable by the CommonJS server (and jest):
build to CommonJS (tsconfig module CommonJS, drop type:module, strip .js from
relative imports), and lazy-load the only ESM-only dep (marked) via the dynamic
Function('import()') trick (mirrors docmost-client.loader.ts) with a require()
fallback so vitest's evaluator works too. git-sync tests stay green (314 pass,
3 expected fail).
Add the §13.1 idempotency gate (apps/server .../git-sync-converter-gate.spec.ts):
13 editor-ext docs (paragraphs/headings, marks, links, bullet/ordered/task lists,
blockquote, callouts, code block, hr, table, nested mix) round-trip
content(editor-ext) -> convertProseMirrorToMarkdown -> markdownToProseMirror ->
TiptapTransformer.toYdoc/fromYdoc(tiptapExtensions) -> canonicalize and assert
docsCanonicallyEqual. All green => the vendored converter's docmost-schema is
schema-compatible with editor-ext (no node/mark/attr loss), which the plan §13.1
requires before Phase B. The one intrinsic markdown-image lossiness (width/height
/align can't ride plain ) is isolated in a KNOWN DIVERGENCE block, not
hidden. Server tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
77 lines
2.5 KiB
JavaScript
77 lines
2.5 KiB
JavaScript
"use strict";
|
|
/**
|
|
* Pure helpers extracted from the docmost-sync Phase-0 idempotency harness
|
|
* (`src/roundtrip.ts`). Only the IO-free comparison utilities are vendored —
|
|
* the CLI scaffold (`--fixture`/`--page`/`--corpus`, `loadSettings`, the
|
|
* `DocmostClient` live path and `process.exit`) is NOT vendored (plan §2.1:
|
|
* the roundtrip harness moves into the package's tests, not the engine).
|
|
*/
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.stripBlockIds = stripBlockIds;
|
|
exports.firstDivergence = firstDivergence;
|
|
/**
|
|
* Recursively strip every `attrs.id` from a ProseMirror node tree. Block ids
|
|
* are regenerated by `markdownToProseMirror` (SPEC §11), so they must be
|
|
* ignored when comparing the semantic shape of two documents. Returns a NEW
|
|
* tree; the input is not mutated.
|
|
*/
|
|
function stripBlockIds(node) {
|
|
if (Array.isArray(node)) {
|
|
return node.map(stripBlockIds);
|
|
}
|
|
if (node && typeof node === "object") {
|
|
const out = {};
|
|
for (const key of Object.keys(node)) {
|
|
if (key === "attrs" && node.attrs && typeof node.attrs === "object") {
|
|
// Drop the `id` attr; keep every other attribute.
|
|
const { id, ...rest } = node.attrs;
|
|
void id;
|
|
out.attrs = stripBlockIds(rest);
|
|
}
|
|
else {
|
|
out[key] = stripBlockIds(node[key]);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
return node;
|
|
}
|
|
/**
|
|
* Find the first divergence between two values via a recursive deep compare.
|
|
* Returns a short path + the two differing values, or null if they are equal.
|
|
*/
|
|
function firstDivergence(a, b, path = "$") {
|
|
if (a === b)
|
|
return null;
|
|
const ta = typeof a;
|
|
const tb = typeof b;
|
|
if (ta !== tb || a === null || b === null) {
|
|
return { path, a, b };
|
|
}
|
|
if (ta !== "object") {
|
|
return { path, a, b };
|
|
}
|
|
const aIsArr = Array.isArray(a);
|
|
const bIsArr = Array.isArray(b);
|
|
if (aIsArr !== bIsArr)
|
|
return { path, a, b };
|
|
if (aIsArr) {
|
|
if (a.length !== b.length) {
|
|
return { path: `${path}.length`, a: a.length, b: b.length };
|
|
}
|
|
for (let i = 0; i < a.length; i++) {
|
|
const d = firstDivergence(a[i], b[i], `${path}[${i}]`);
|
|
if (d)
|
|
return d;
|
|
}
|
|
return null;
|
|
}
|
|
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
for (const k of keys) {
|
|
const d = firstDivergence(a[k], b[k], `${path}.${k}`);
|
|
if (d)
|
|
return d;
|
|
}
|
|
return null;
|
|
}
|