everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
250 lines (248 loc) • 9.23 kB
JavaScript
const require_run = require('./utils/run.cjs');
let node_fs = require("node:fs");
let node_path = require("node:path");
let effect = require("effect");
let yaml = require("yaml");
let node_fs_promises = require("node:fs/promises");
//#region src/workspace.ts
var DepsBuildFailure = class extends effect.Data.TaggedError("DepsBuildFailure") {
get message() {
return `workspace dependency builds failed:\n${this.failures.map((failure) => `${failure.member.name} (exit ${failure.exitCode})\n${outputTail(failure.output)}`).join("\n\n")}`;
}
};
const workspaceRootCache = /* @__PURE__ */ new Map();
function findWorkspaceRoot(startDir) {
const key = (0, node_path.resolve)(startDir);
const cached = workspaceRootCache.get(key);
if (cached !== void 0) return cached;
const found = discoverWorkspaceRoot(key);
workspaceRootCache.set(key, found);
return found;
}
function discoverWorkspaceRoot(dir) {
let current = dir;
while (true) {
const root = readWorkspaceRootAt(current);
if (root) return root;
const parent = (0, node_path.dirname)(current);
if (parent === current) return null;
current = parent;
}
}
function readWorkspaceRootAt(dir) {
const yamlPath = (0, node_path.join)(dir, "pnpm-workspace.yaml");
if ((0, node_fs.existsSync)(yamlPath)) {
const globs = workspaceGlobsFromYaml((0, yaml.parse)((0, node_fs.readFileSync)(yamlPath, "utf8"))?.packages);
if (globs) return readWorkspaceMembers(dir, globs);
}
const manifestPath = (0, node_path.join)(dir, "package.json");
if (!(0, node_fs.existsSync)(manifestPath)) return null;
const manifest = readManifest(manifestPath);
if (!manifest) return null;
const globs = workspaceGlobs(manifest.workspaces);
if (!globs) return null;
return readWorkspaceMembers(dir, globs);
}
function workspaceGlobsFromYaml(packages) {
if (!Array.isArray(packages)) return null;
const globs = packages.filter((glob) => typeof glob === "string");
return globs.length > 0 ? globs : null;
}
function readWorkspaceMembers(dir, globs) {
const memberDirs = matchMemberDirs(dir, globs);
const drafts = [];
for (const memberDir of memberDirs) {
const memberManifest = readManifest((0, node_path.join)(memberDir, "package.json"));
if (!memberManifest || typeof memberManifest.name !== "string") continue;
drafts.push({
name: memberManifest.name,
dir: memberDir,
manifest: memberManifest
});
}
const names = new Set(drafts.map((draft) => draft.name));
return {
dir,
members: drafts.map((draft) => ({
name: draft.name,
dir: draft.dir,
localDeps: declaredDepNames(draft.manifest).filter((dep) => names.has(dep))
})).sort((a, b) => a.name.localeCompare(b.name))
};
}
function workspaceGlobs(workspaces) {
if (Array.isArray(workspaces)) return workspaces.filter((glob) => typeof glob === "string");
if (workspaces && typeof workspaces === "object") {
const packages = workspaces.packages;
if (Array.isArray(packages)) return packages.filter((glob) => typeof glob === "string");
}
return null;
}
function matchMemberDirs(rootDir, globs) {
const found = /* @__PURE__ */ new Set();
for (const glob of globs) collectMemberDirs(rootDir, glob.split("/"), found);
return [...found].sort();
}
function collectMemberDirs(dir, segments, out) {
const [head, ...rest] = segments;
if (!head) return;
let entries;
try {
entries = (0, node_fs.readdirSync)(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (head !== "*" && head !== entry.name) continue;
const child = (0, node_path.join)(dir, entry.name);
if (rest.length === 0) out.add(child);
else collectMemberDirs(child, rest, out);
}
}
function readManifest(path) {
try {
return JSON.parse((0, node_fs.readFileSync)(path, "utf8"));
} catch {
return null;
}
}
function declaredDepNames(manifest) {
const names = [];
for (const field of ["dependencies", "devDependencies"]) {
const deps = manifest[field];
if (deps && typeof deps === "object") names.push(...Object.keys(deps));
}
return names;
}
function localDepsOf(root, targetDirs) {
const byName = new Map(root.members.map((member) => [member.name, member]));
const byDir = new Map(root.members.map((member) => [member.dir, member]));
const ordered = [];
const seen = /* @__PURE__ */ new Set();
const visit = (member) => {
if (seen.has(member.name)) return;
seen.add(member.name);
for (const dep of member.localDeps) {
const depMember = byName.get(dep);
if (depMember) visit(depMember);
}
ordered.push(member);
};
for (const targetDir of targetDirs) {
const target = byDir.get((0, node_path.resolve)(targetDir));
if (!target) continue;
for (const dep of target.localDeps) {
const depMember = byName.get(dep);
if (depMember) visit(depMember);
}
}
return ordered;
}
async function ensureFreshDeps(startDir, targetDirs, opts) {
const root = findWorkspaceRoot(startDir);
if (!root) return {
rebuilt: [],
fresh: []
};
const closure = localDepsOf(root, targetDirs);
const force = opts?.force ?? false;
const judgements = await Promise.all(closure.map(async (member) => force || await isStaleMember(member)));
const rebuilt = closure.filter((_, index) => judgements[index]);
const fresh = closure.filter((_, index) => !judgements[index]);
const failures = [];
for (const layer of buildLayers(rebuilt)) {
const results = await Promise.allSettled(layer.map((member) => require_run.run("pnpm", ["run", "build"], {
cwd: member.dir,
capture: true,
env: opts?.env
})));
failures.push(...results.flatMap((result, index) => {
const value = result.status === "fulfilled" ? result.value : void 0;
if (value && value.exitCode === 0) return [];
const output = value ? `${value.stdout}\n${value.stderr}` : result.status === "rejected" ? String(result.reason) : "";
return [{
member: layer[index],
exitCode: value ? value.exitCode ?? 1 : 1,
output
}];
}));
if (failures.length > 0) break;
}
if (failures.length > 0) throw new DepsBuildFailure({ failures });
return {
rebuilt,
fresh
};
}
function buildLayers(members) {
const byName = new Map(members.map((member) => [member.name, member]));
const remaining = new Set(members.map((member) => member.name));
const layers = [];
while (remaining.size > 0) {
const layer = members.filter((member) => remaining.has(member.name) && member.localDeps.every((dep) => !byName.has(dep) || !remaining.has(dep)));
if (layer.length === 0) throw new Error(`dependency cycle in workspace closure: ${[...remaining].join(", ")}`);
for (const member of layer) remaining.delete(member.name);
layers.push(layer);
}
return layers;
}
async function isStaleMember(member) {
const oracle = distOracle(member.dir);
if (!oracle) return false;
return isWorkspaceDistStale(member.dir, oracle);
}
function distOracle(memberDir) {
const manifest = readManifest((0, node_path.join)(memberDir, "package.json"));
if (!manifest) return null;
const entry = manifest.exports && typeof manifest.exports === "object" ? manifest.exports["."] : void 0;
if (entry && typeof entry === "object") for (const key of [
"import",
"default",
"require",
"types"
]) {
const value = entry[key];
if (typeof value === "string" && !value.startsWith("./src")) return value;
}
if (typeof manifest.module === "string" && !manifest.module.startsWith("./src")) return manifest.module;
if (typeof manifest.main === "string" && !manifest.main.startsWith("./src")) return manifest.main;
const reportPath = (0, node_path.join)(memberDir, "dist", "build-report.json");
if ((0, node_fs.existsSync)(reportPath)) {
const report = readManifest(reportPath);
if (report && typeof report.entry === "string" && report.entry) return (0, node_path.join)("dist", report.entry);
}
return null;
}
async function newestSourceMtimeMs(dir) {
let newest = 0;
const entries = await (0, node_fs_promises.readdir)(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name === "node_modules" || entry.name === "dist") continue;
const fullPath = (0, node_path.join)(dir, entry.name);
if (entry.isDirectory()) newest = Math.max(newest, await newestSourceMtimeMs(fullPath));
else newest = Math.max(newest, (await (0, node_fs_promises.stat)(fullPath)).mtimeMs);
}
return newest;
}
async function isWorkspaceDistStale(packageDir, distEntry) {
const distPath = (0, node_path.join)(packageDir, distEntry);
if (!(0, node_fs.existsSync)(distPath)) return true;
const [distMtime, srcMtime, pkgMtime] = await Promise.all([
(0, node_fs_promises.stat)(distPath).then((s) => s.mtimeMs),
newestSourceMtimeMs((0, node_path.join)(packageDir, "src")),
(0, node_fs_promises.stat)((0, node_path.join)(packageDir, "package.json")).then((s) => s.mtimeMs)
]);
return Math.max(srcMtime, pkgMtime) > distMtime;
}
function outputTail(output) {
return output.trim().split("\n").slice(-10).join("\n");
}
//#endregion
exports.DepsBuildFailure = DepsBuildFailure;
exports.buildLayers = buildLayers;
exports.ensureFreshDeps = ensureFreshDeps;
exports.findWorkspaceRoot = findWorkspaceRoot;
exports.isWorkspaceDistStale = isWorkspaceDistStale;
exports.localDepsOf = localDepsOf;
//# sourceMappingURL=workspace.cjs.map