testPackagesAreAnalyzedByExport

tcmts.cli.npm.testPackagesAreAnalyzedByExport · #utfxfowr · term

Packages, as files under a directory's node_modules: what each export reaches is worked out from their code.

import { exportFacts, resolvePackageSubpath, resolveSpecifier } from "#codebase/tcmts/cli/npm";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

export const testPackagesAreAnalyzedByExport = () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "tcm npm "));
  const write = (rel: string, text: string) => {
    fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
    fs.writeFileSync(path.join(dir, rel), text);
  };
  write(
    "node_modules/lib/package.json",
    JSON.stringify({
      name: "lib",
      type: "module",
      exports: {
        ".": { browser: "./browser.js", default: "./index.js" },
        "./util/*": "./util/*.js",
      },
    }),
  );
  write(
    "node_modules/lib/index.js",
    `import { readFileSync } from "node:fs";
import { helper } from "./helper.js";
export * from "./more.js";
export function pure(x) { const Date = 1; return helper(x) + Date; }
export function load(f) { return readFileSync(f, "utf8"); }
export const stamp = () => Date.now();
export function guarded() { return typeof window === "undefined" ? 0 : window.innerWidth; }
export let counter = 0;
export const bump = () => counter++;
var ONE = 1;
export const one = () => ONE;
const seen = [];
export const remember = (x) => seen.push(x);
export const later = (m) => import(m);
`,
  );
  write("node_modules/lib/helper.js", `export const helper = (x) => x * 2;\n`);
  write(
    "node_modules/lib/more.js",
    `import other from "other";\nexport const fetched = () => fetch("/x");\nexport const viaOther = () => other();\n`,
  );
  write(
    "node_modules/lib/browser.js",
    `export function load() { return localStorage.getItem("x"); }\n`,
  );
  write("node_modules/lib/util/text.js", `export const upper = (s) => s.toUpperCase();\n`);
  write("node_modules/other/package.json", JSON.stringify({ name: "other", main: "main.js" }));
  write(
    "node_modules/other/main.js",
    `const crypto = require("crypto");\nmodule.exports = () => crypto.randomBytes(4);\n`,
  );
  write(
    "node_modules/sideways/package.json",
    JSON.stringify({ name: "sideways", type: "module", exports: "./index.js" }),
  );
  write("node_modules/sideways/index.js", `console.log("loaded");\nexport const id = (x) => x;\n`);
  // browser components, as React has them: a module saying so, and one using a hook
  write(
    "node_modules/ui/package.json",
    JSON.stringify({ name: "ui", type: "module", exports: "./index.js" }),
  );
  write(
    "node_modules/ui/index.js",
    `export { Dialog } from "./dialog.js";\nexport { Toggle } from "./toggle.js";\nexport const Label = (p) => p.children;\n`,
  );
  write(
    "node_modules/ui/dialog.js",
    `/* (c) ui */\n"use client";\nexport const Dialog = (p) => p.children;\n`,
  );
  write(
    "node_modules/ui/toggle.js",
    `import { useState } from "react";\nexport function Toggle() { const [on] = useState(false); return on; }\n`,
  );
  const cache = path.join(dir, "cache");
  const entry = (pkg: string, sub = ".", side: "browser" | "server" = "server") =>
    (
      resolveSpecifier(
        path.join(dir, "app.js"),
        sub === "." ? pkg : `${pkg}/${sub.slice(2)}`,
        side === "browser" ? ["browser", "import", "default"] : ["node", "import", "default"],
      ) as { file: string }
    ).file;
  const facts = (pkg: string, name: string, side: "browser" | "server" = "server", sub = ".") =>
    exportFacts(entry(pkg, sub, side), name, side, cache);
  const clean = { effects: [], node: [], dom: [], client: [], state: false, opaque: [] };
  // through its own declarations and the module it imports (a local named Date isn't the clock)
  assert.deepEqual(facts("lib", "pure"), clean);
  assert.deepEqual(facts("lib", "load"), { ...clean, effects: ["files"], node: ["node:fs"] });
  assert.deepEqual(facts("lib", "stamp").effects, ["clock"]);
  assert.deepEqual(facts("lib", "guarded").dom, ["window"], "a use, past the typeof");
  // state: what something changes (a binding assigned again, an array pushed to); a `var` nothing changes isn't
  assert.equal(facts("lib", "bump").state, true);
  assert.equal(facts("lib", "remember").state, true);
  assert.deepEqual(facts("lib", "one"), clean);
  assert.deepEqual(facts("lib", "later").opaque, ["a computed import()"]);
  // through `export *`, and into another package, CommonJS read whole
  assert.deepEqual(facts("lib", "fetched").effects, ["network"]);
  assert.deepEqual(facts("lib", "viaOther"), {
    ...clean,
    effects: ["random"],
    node: ["node:crypto"],
  });
  // the browser's build of the same export
  assert.deepEqual(facts("lib", "load", "browser"), {
    ...clean,
    effects: ["environment"],
    dom: ["localStorage"],
  });
  // a subpath, by its pattern
  assert.deepEqual(facts("lib", "upper", "server", "./util/text"), clean);
  // importing a module runs it: what its top-level code does counts
  assert.deepEqual(facts("sideways", "id").effects, ["output"]);
  // which exports are browser components: by the directive (after a comment), by a hook; not their neighbours
  assert.deepEqual(facts("ui", "Dialog").client, ['says "use client"']);
  assert.deepEqual(
    facts("ui", "Toggle").client.filter((c) => c.startsWith("uses")),
    ["uses useState"],
  );
  assert.deepEqual(facts("ui", "Label").client, []);
  // the export map, read as Node and bundlers read it
  assert.equal(
    resolvePackageSubpath({ exports: { ".": { import: "./a.mjs", require: "./a.cjs" } } }, ".", [
      "node",
      "import",
    ]),
    "./a.mjs",
  );
  assert.equal(resolvePackageSubpath({ main: "m.js", browser: "b.js" }, ".", ["browser"]), "b.js");
  assert.equal(
    resolvePackageSubpath({ exports: { "./x/*": "./dist/*.js" } }, "./x/y", ["node"]),
    "./dist/y.js",
  );
  fs.rmSync(dir, { recursive: true, force: true });
}

uses