/** * Build every @wrnexus/* package (JS + .d.ts via tsup) and STAGE a publishable * copy under .publish// with a rewritten manifest: * - main/module/types/exports/bin point at ./dist/* * - workspace:* deps → ^ * - `private` removed, publishConfig.registry set * - runtime assets (ui components/, ui.css) copied alongside dist * * The dev package.json (pointing at src/) is never modified, so in-repo dev and * tests keep working. * * bun run scripts/publish-packages.ts # build + stage all * bun run scripts/publish-packages.ts core db # build + stage only these * * Then publish from the staging dirs (see printed order), or dry-run with npm pack. */ import { build } from "tsup"; import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const repoRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); const packagesDir = join(repoRoot, "packages"); const stageRoot = join(repoRoot, ".publish"); const REGISTRY = "https://registry.npmjs.org/"; // First release is published PRIVATE ("restricted") so we can test before the // world sees it; flip to public with `npm access public @wrnexus/` (or set // WRNEXUS_NPM_ACCESS=public to stage public manifests). Requires the `wrnexus` // npm org to exist and be on a paid plan (private scoped packages need Teams). const ACCESS = process.env.WRNEXUS_NPM_ACCESS === "public" ? "public" : "restricted"; const LICENSE = "MIT"; /** Extra (non-code) files each package must ship, relative to the package root. */ const ASSETS: Record = { "@wrnexus/ui": ["components", "ui.css"], }; interface Pkg { dir: string; name: string; manifest: Record; } function readPackages(filter: string[]): Pkg[] { const out: Pkg[] = []; for (const entry of readdirSync(packagesDir)) { const dir = join(packagesDir, entry); const pj = join(dir, "package.json"); if (!existsSync(pj)) continue; const manifest = JSON.parse(readFileSync(pj, "utf8")); if (!manifest.name?.startsWith("@wrnexus/")) continue; if (filter.length && !filter.includes(entry) && !filter.includes(manifest.name)) continue; out.push({ dir, name: manifest.name, manifest }); } return out; } /** Topologically sort by @wrnexus deps so dependencies publish first. */ function topoSort(pkgs: Pkg[]): Pkg[] { const byName = new Map(pkgs.map((p) => [p.name, p])); const sorted: Pkg[] = []; const seen = new Set(); const visit = (p: Pkg) => { if (seen.has(p.name)) return; seen.add(p.name); for (const dep of Object.keys(p.manifest.dependencies ?? {})) { const d = byName.get(dep); if (d) visit(d); } sorted.push(p); }; pkgs.forEach(visit); return sorted; } /** * Build a tsup entry map { outName: absSourcePath } from every `./x.ts` target * referenced by main/bin/exports. Object entries (unlike arrays) are NOT glob- * expanded by tsup, so absolute Windows paths work. `outName` is the source path * relative to `src/` without extension, matching the js()/dts() manifest mapping. */ function entriesOf(dir: string, m: Record): Record { const map: Record = {}; const add = (v: unknown) => { if (typeof v !== "string" || !v.endsWith(".ts")) return; const rel = v.replace(/^\.?\/?src\//, "").replace(/\.ts$/, ""); map[rel] = join(dir, v.replace(/^\.\//, "")).replace(/\\/g, "/"); }; add(m.main); if (m.bin) Object.values(m.bin).forEach(add); if (m.exports) for (const v of Object.values(m.exports)) add(v); return map; } /** Rewrite the manifest for publishing. */ function publishManifest(m: Record, version: string): Record { const js = (ts: string) => "./dist/" + ts.replace(/^\.?\/?src\//, "").replace(/\.ts$/, ".js"); const dts = (ts: string) => "./dist/" + ts.replace(/^\.?\/?src\//, "").replace(/\.ts$/, ".d.ts"); const out: Record = { name: m.name, version, type: "module", description: m.description ?? `${m.name} — part of the WrNexus framework.`, license: LICENSE, main: m.main ? js(m.main) : "./dist/index.js", module: m.main ? js(m.main) : "./dist/index.js", types: m.main ? dts(m.main) : "./dist/index.d.ts", engines: { bun: ">=1.1.0" }, publishConfig: { registry: REGISTRY, access: ACCESS }, }; // exports: rewrite .ts targets to { types, import }; copy non-.ts (assets) as-is. if (m.exports) { const ex: Record = {}; for (const [key, val] of Object.entries(m.exports)) { if (typeof val === "string" && val.endsWith(".ts")) { ex[key] = { types: dts(val), import: js(val) }; } else { ex[key] = val; // e.g. "./ui.css" } } out.exports = ex; } else { out.exports = { ".": { types: out.types, import: out.main } }; } if (m.bin) { out.bin = {}; for (const [k, v] of Object.entries(m.bin)) out.bin[k] = js(v as string); } if (m.dependencies) { out.dependencies = {}; for (const [dep, range] of Object.entries(m.dependencies)) { out.dependencies[dep] = String(range).startsWith("workspace:") ? `^${version}` : range; } } const files = new Set(["dist"]); for (const a of ASSETS[m.name] ?? []) files.add(a); out.files = [...files]; return out; } async function main() { const filter = process.argv.slice(2); const pkgs = topoSort(readPackages(filter)); if (!pkgs.length) { console.error("No @wrnexus packages matched."); process.exit(1); } rmSync(stageRoot, { recursive: true, force: true }); mkdirSync(stageRoot, { recursive: true }); const order: string[] = []; for (const p of pkgs) { const entries = entriesOf(p.dir, p.manifest); process.stdout.write(`▸ ${p.name} … `); await build({ entry: entries, outDir: join(p.dir, "dist"), format: ["esm"], dts: true, external: [/^@wrnexus\//, /^bun:/, /^node:/], tsconfig: join(repoRoot, "tsconfig.json"), clean: true, splitting: false, sourcemap: false, shims: false, silent: true, target: "esnext", platform: "node", }); // tsup's dts pass can occasionally leak .d.ts files next to source (the // jsxImportSource files). Source is all .ts, so any .d.ts under src/ is a // stray artifact — remove it so it never pollutes the tree. for (const f of readdirSync(join(p.dir, "src"), { recursive: true }) as string[]) { if (typeof f === "string" && f.endsWith(".d.ts")) { rmSync(join(p.dir, "src", f), { force: true }); } } // Stage: dist + assets + manifest + README. const stage = join(stageRoot, p.name.replace("@wrnexus/", "")); mkdirSync(stage, { recursive: true }); cpSync(join(p.dir, "dist"), join(stage, "dist"), { recursive: true }); for (const a of ASSETS[p.name] ?? []) { if (existsSync(join(p.dir, a))) cpSync(join(p.dir, a), join(stage, a), { recursive: true }); } const manifest = publishManifest(p.manifest, p.manifest.version); writeFileSync(join(stage, "package.json"), JSON.stringify(manifest, null, 2) + "\n"); // Ship the package's own README.md if it has one; else a minimal placeholder. const srcReadme = join(p.dir, "README.md"); if (existsSync(srcReadme)) { cpSync(srcReadme, join(stage, "README.md")); } else { writeFileSync( join(stage, "README.md"), `# ${p.name}\n\n${manifest.description}\n\nPart of the WrNexus framework.\n`, ); } order.push(p.name); console.log("staged"); } console.log(`\n✓ Staged ${order.length} package(s) under .publish/ (access: ${ACCESS})`); console.log("Publish order (deps first):"); for (const n of order) { console.log(` npm publish .publish/${n.replace("@wrnexus/", "")} --access ${ACCESS}`); } if (ACCESS === "restricted") { console.log("\nAfter testing, flip every package to public with:"); for (const n of order) console.log(` npm access public ${n}`); } } main().catch((err) => { console.error(err); process.exit(1); });