rooms-kit / test / helpers / expose.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
 * Load a module from the TypeScript build with its `#private` members aliased
 * onto a public `__t` bag.
 *
 * dev/docs/CLJS.md's method for golden vectors is "splice the real modules into
 * a harness rather than transcribing their logic into a generator". Most of
 * rooms-kit obliges directly — `dist/lib/crypto.js` and friends export
 * everything the vectors need. The state machines do not: the reaction fold
 * (`ChatClient.#applyEntry`), the log's entry projection (`RoomLog.#emit`,
 * `#authorOf`, `#canJoinLocally`) and the whole membership machine
 * (`Net.#verified`, `#onPresence`, `#sweep`, …) are ES2022 hard-private, and
 * `tsc` emits them as native `#names` — unreachable from outside the class.
 *
 * So this loader does the smallest possible mechanical edit to the REAL
 * compiled module: it injects one class field
 *
 *     __t = { m: (...a) => this.#m(...a), … };
 *
 * per requested class, leaving every line of logic untouched, and rewrites the
 * relative import specifiers to absolute file URLs so the patched copy can live
 * outside dist/. Nothing is retyped, so no fixture can encode a misreading of
 * the source.
 *
 * The port has no hard-private members (CLJS has no equivalent; the canon's
 * idiom is `_name` string keys, as in peer-kit), so `test/*.test.mjs` reaches
 * the same methods on the built CLJS dist directly and replays these vectors
 * against them.
 */
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";

const SPEC = /(from\s*|import\s*\(\s*)("|')(\.\.?\/[^"']+)\2/g;

let counter = 0;

/**
 * @param {string} file        absolute path of a module inside dist/
 * @param {string} outDir      writable scratch directory
 * @param {Record<string,string[]>} classes  className -> private member names
 */
export async function loadExposed(file, outDir, classes) {
  let src = readFileSync(file, "utf8");

  // relative specifiers must survive the move out of dist/
  src = src.replace(SPEC, (m, lead, q, spec) => `${lead}${q}${pathToFileURL(resolve(dirname(file), spec)).href}${q}`);

  for (const [cls, members] of Object.entries(classes)) {
    const re = new RegExp(`(class\\s+${cls}\\b[^{]*\\{)`);
    if (!re.test(src)) throw new Error(`expose: class ${cls} not found in ${file}`);
    const bag = members.map((m) => `${m}: (...a) => this.#${m}(...a)`).join(", ");
    src = src.replace(re, `$1\n  __t = { ${bag} };\n`);
  }

  mkdirSync(outDir, { recursive: true });
  const out = join(outDir, `exposed-${counter++}.mjs`);
  writeFileSync(out, src);
  return import(pathToFileURL(out).href);
}

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/rooms-kit.git