testPackagesAreAnalyzedByExport
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 }); }