@napi-rs/cli
Version:
Cli tools for napi-rs
994 lines (993 loc) • 1.89 MB
JavaScript
#!/usr/bin/env node
import { builtinModules, createRequire } from "node:module";
import { Cli, Command, Option } from "clipanion";
import { access, chmod, copyFile, cp, link, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rename, rm, rmdir, stat, symlink, unlink, writeFile } from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import path, { basename, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
import * as colors from "colorette";
import { underline, yellow } from "colorette";
import { createDebug } from "obug";
import { exec, execFile, execSync, spawn, spawnSync } from "node:child_process";
import { AsyncLocalStorage } from "node:async_hooks";
import fs, { constants, existsSync, lstatSync, mkdirSync, promises, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
import { createHash, randomUUID } from "node:crypto";
import { performance } from "node:perf_hooks";
import { setTimeout } from "node:timers";
import { setTimeout as setTimeout$1 } from "node:timers/promises";
import { Comparator, Range, minVersion, subset } from "semver";
import { isNil, merge, omit, omitBy, pick, sortBy } from "es-toolkit";
import { dump, load } from "js-yaml";
import * as typanion from "typanion";
import { isDeepStrictEqual } from "node:util";
import { Octokit } from "@octokit/rest";
import { checkbox, confirm, input, select } from "@inquirer/prompts";
//#region src/def/artifacts.ts
var BaseArtifactsCommand = class extends Command {
static paths = [["artifacts"]];
static usage = Command.Usage({ description: "Copy artifacts from Github Actions into npm packages and ready to publish" });
cwd = Option.String("--cwd", process.cwd(), { description: "The working directory of where napi command will be executed in, all other paths options are relative to this path" });
configPath = Option.String("--config-path,-c", { description: "Path to `napi` config json file" });
packageJsonPath = Option.String("--package-json-path", "package.json", { description: "Path to `package.json`" });
outputDir = Option.String("--output-dir,-o,-d", "./artifacts", { description: "Path to the folder where all built `.node` files put, same as `--output-dir` of build command" });
npmDir = Option.String("--npm-dir", "npm", { description: "Path to the folder where the npm packages put" });
buildOutputDir = Option.String("--build-output-dir", { description: "Path to the build output dir, only needed when targets contain a WASI target" });
getOptions() {
return {
cwd: this.cwd,
configPath: this.configPath,
packageJsonPath: this.packageJsonPath,
outputDir: this.outputDir,
npmDir: this.npmDir,
buildOutputDir: this.buildOutputDir
};
}
};
function applyDefaultArtifactsOptions(options) {
return {
cwd: process.cwd(),
packageJsonPath: "package.json",
outputDir: "./artifacts",
npmDir: "npm",
...options
};
}
//#endregion
//#region src/utils/log.ts
const debugFactory = (namespace) => {
const debug = createDebug(`napi:${namespace}`, { formatters: { i(v) {
return colors.green(v);
} } });
debug.info = (...args) => console.error(colors.black(colors.bgGreen(" INFO ")), ...args);
debug.warn = (...args) => console.error(colors.black(colors.bgYellow(" WARNING ")), ...args);
debug.error = (...args) => console.error(colors.white(colors.bgRed(" ERROR ")), ...args.map((arg) => arg instanceof Error ? arg.stack ?? arg.message : arg));
return debug;
};
const debug$10 = debugFactory("utils");
//#endregion
//#region package.json
var version$1 = "3.8.2";
//#endregion
//#region src/utils/misc.ts
const readFileAsync = readFile;
const writeFileAsync = writeFile;
const unlinkAsync = unlink;
const mkdirAsync = mkdir;
const statAsync = stat;
const readdirAsync = readdir;
const reconciliationTails = /* @__PURE__ */ new Map();
const reconciliationLockName = ".napi-rs-filesystem-reconciliation";
const reconciliationReclaimMarker = ".reclaim.";
const reconciliationCandidateMarker = ".candidate.";
const reconciliationRetiredMarker = ".retired.";
const reconciliationMetadataExtension = ".swp";
const reconciliationLockKind = "napi-rs-filesystem-reconciliation-lock";
const reconciliationReclaimKind = "napi-rs-filesystem-reconciliation-reclaim";
const reconciliationStateVersion = 1;
const reconciliationLockAcquisitionTimeout = 12e4;
const reconciliationLockCleanupTimeout = 5e3;
const reconciliationLockCleanupRetryInterval = 250;
const reconciliationMetadataMaximumSize = 64 * 1024;
const processIncarnationCommandTimeout = 2e3;
const incompleteProcessExecutionIdentityCacheDuration = processIncarnationCommandTimeout;
const processIncarnationObservationCacheDuration = 1e3;
const fileSystemTransactionJournalName = ".napi-rs-filesystem-transaction.swp";
const fileSystemTransactionCandidateMarker = ".candidate.";
const fileSystemTransactionRetiredMarker = ".retired.";
const fileSystemTransactionOwnerName = "owner.json";
const fileSystemTransactionStateName = "state.json";
const fileSystemTransactionKind = "napi-rs-filesystem-transaction";
const legacyFileSystemTransactionStateVersion = 1;
const previousFileSystemTransactionStateVersion = 2;
const fileSystemTransactionStateVersion = 3;
const fileSystemTransactionStateMaximumSize = 16 * 1024 * 1024;
const fileSystemTransactionMaximumEntries = 1e5;
const fileSystemTransactionCleanupTimeout = 5e3;
const fileSystemTransactionCleanupInitialRetryDelay = 10;
const fileSystemTransactionCleanupMaximumRetryDelay = 250;
const processIncarnationObservations = /* @__PURE__ */ new Map();
let currentProcessIncarnation;
let currentProcessIncarnationProbe;
let linuxBootId;
const fileSystemReconciliationCapability = new AsyncLocalStorage();
async function writeFileAtomic(path, data, options) {
await mkdir(dirname(path), { recursive: true });
while (true) {
const temporaryPath = atomicTemporaryPath(path);
const exclusiveOptions = typeof options === "string" ? {
encoding: options,
flag: "wx"
} : {
...options,
flag: "wx"
};
try {
await writeFile(temporaryPath, data, exclusiveOptions);
} catch (error) {
if (error.code === "EEXIST") continue;
throw error;
}
let committed = false;
try {
await syncFile(temporaryPath);
await rename(temporaryPath, path);
await syncDirectory(dirname(path));
committed = true;
return;
} finally {
if (!committed) await unlinkFileIfExists(temporaryPath);
}
}
}
async function copyFileAtomic(source, destination, mode) {
await mkdir(dirname(destination), { recursive: true });
while (true) {
const temporaryPath = atomicTemporaryPath(destination);
try {
await copyFile(source, temporaryPath, constants.COPYFILE_EXCL);
} catch (error) {
if (error.code === "EEXIST") continue;
throw error;
}
let committed = false;
try {
if (mode !== void 0) await chmod(temporaryPath, mode);
await syncFile(temporaryPath);
await rename(temporaryPath, destination);
await syncDirectory(dirname(destination));
committed = true;
return;
} finally {
if (!committed) await unlinkFileIfExists(temporaryPath);
}
}
}
async function withFileSystemReconciliation(path, operation) {
const localKey = resolve(path);
const previous = reconciliationTails.get(localKey) ?? Promise.resolve();
let release;
const current = new Promise((resolveCurrent) => {
release = resolveCurrent;
});
const tail = previous.catch(() => {}).then(() => current);
reconciliationTails.set(localKey, tail);
const releaseCrossProcessLocks = [];
let operationFailed = false;
let operationError;
let result;
try {
await previous.catch(() => {});
const identities = await resolveReconciliationLockIdentities(path);
const acquisitionDeadline = createReconciliationLockDeadline(reconciliationLockAcquisitionTimeout);
for (const identity of identities) releaseCrossProcessLocks.push(await acquireReconciliationLock(identity, acquisitionDeadline));
await recoverFileSystemTransaction(identities[0].anchorPath);
const currentCapability = fileSystemReconciliationCapability.getStore();
const roots = new Set(currentCapability === null || currentCapability === void 0 ? void 0 : currentCapability.roots);
roots.add(fileSystemReconciliationCapabilityRoot(identities[0].anchorPath));
result = await fileSystemReconciliationCapability.run({ roots }, operation);
} catch (error) {
operationFailed = true;
operationError = error;
}
const releaseErrors = [];
for (let index = releaseCrossProcessLocks.length - 1; index >= 0; index--) try {
await releaseCrossProcessLocks[index]();
} catch (error) {
releaseErrors.push(error);
}
release();
if (reconciliationTails.get(localKey) === tail) reconciliationTails.delete(localKey);
if (operationFailed) {
if (releaseErrors.length > 0) throw new AggregateError([operationError, ...releaseErrors], "Filesystem reconciliation operation and lock release both failed", { cause: operationError });
throw operationError;
}
if (releaseErrors.length === 1) throw releaseErrors[0];
if (releaseErrors.length > 1) throw new AggregateError(releaseErrors, "Multiple filesystem reconciliation locks could not be released", { cause: releaseErrors[0] });
return result;
}
function fileSystemReconciliationCapabilityRoot(path) {
const resolvedPath = resolve(path);
return process.platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath;
}
function getPackageReconciliationRoot(cwd, packageJsonPath = "package.json") {
return dirname(resolve(cwd, packageJsonPath));
}
function resolvePackageReconciliationPaths(cwd, packageJsonPath = "package.json", managedPaths = []) {
const canonicalCwd = canonicalizeManagedPackagePath(cwd);
const requestedPackageJsonPath = resolve(cwd, packageJsonPath);
const canonicalPackageJsonPath = canonicalizeManagedPackagePath(requestedPackageJsonPath);
const canonicalPackageRoot = canonicalizeManagedPackagePath(dirname(requestedPackageJsonPath));
const canonicalManagedPaths = managedPaths.map((path) => canonicalizeManagedPackagePath(resolve(cwd, path)));
if (!managedPackagePathIsWithin(canonicalPackageRoot, canonicalPackageJsonPath)) throw new Error(`Package manifest escapes its package root: ${requestedPackageJsonPath}`);
if (!(managedPackagePathIsWithin(canonicalPackageRoot, canonicalCwd) || managedPackagePathIsWithin(canonicalCwd, canonicalPackageRoot))) throw managedPackageBoundaryError(canonicalCwd, canonicalPackageRoot, canonicalManagedPaths);
const discoveryBoundary = managedPackagePathIsWithin(canonicalCwd, canonicalPackageRoot) ? canonicalCwd : canonicalPackageRoot;
let candidate = canonicalPackageRoot;
while (true) {
if (hasPackageWorkspaceBoundaryMarker(candidate) && canonicalManagedPaths.every((path) => managedPackagePathIsWithin(candidate, path))) return {
boundary: candidate,
cwd: canonicalCwd,
managedPaths: canonicalManagedPaths,
packageJsonPath: canonicalPackageJsonPath,
packageRoot: canonicalPackageRoot
};
if (candidate === discoveryBoundary) break;
const parent = dirname(candidate);
if (parent === candidate || !managedPackagePathIsWithin(discoveryBoundary, parent)) break;
candidate = parent;
}
if (dirname(canonicalPackageRoot) !== canonicalPackageRoot && canonicalManagedPaths.every((path) => managedPackagePathIsWithin(canonicalPackageRoot, path))) return {
boundary: canonicalPackageRoot,
cwd: canonicalCwd,
managedPaths: canonicalManagedPaths,
packageJsonPath: canonicalPackageJsonPath,
packageRoot: canonicalPackageRoot
};
throw managedPackageBoundaryError(canonicalCwd, canonicalPackageRoot, canonicalManagedPaths);
}
function getPackageReconciliationRoots({ boundary, cwd, packageRoot }) {
const roots = [];
const rootIdentities = /* @__PURE__ */ new Set();
for (const root of [
packageRoot,
boundary,
cwd
]) {
const resolvedRoot = resolve(root);
const identity = fileSystemReconciliationCapabilityRoot(resolvedRoot);
if (rootIdentities.has(identity)) continue;
if (roots.some((existingRoot) => !managedPackagePathIsWithin(existingRoot, resolvedRoot) && !managedPackagePathIsWithin(resolvedRoot, existingRoot))) throw new Error(`Package reconciliation roots must be nested: ${roots.join(", ")}, ${resolvedRoot}`);
rootIdentities.add(identity);
roots.push(resolvedRoot);
}
return roots;
}
async function withPackageFileSystemReconciliation(paths, operation) {
const roots = getPackageReconciliationRoots(paths);
const acquire = (index) => {
if (index === roots.length) return operation();
return withFileSystemReconciliation(roots[index], () => acquire(index + 1));
};
return acquire(0);
}
function managedPackagePathIsWithin(root, path) {
const relativePath = relative(resolve(root), resolve(path));
return relativePath === "" || !isAbsolute(relativePath) && relativePath !== ".." && !relativePath.startsWith(`..${sep}`);
}
function canonicalizeManagedPackagePath(path) {
let current = resolve(path);
const missingSegments = [];
while (true) try {
return join(realpathSync.native(current), ...missingSegments);
} catch (error) {
if (error.code !== "ENOENT") throw error;
const parent = dirname(current);
if (parent === current) return join(current, ...missingSegments);
missingSegments.unshift(basename(current));
current = parent;
}
}
function hasPackageWorkspaceBoundaryMarker(directory) {
if (existsSync(join(directory, "pnpm-workspace.yaml"))) return true;
const manifestPath = join(directory, "package.json");
if (!existsSync(manifestPath)) return false;
try {
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
return Array.isArray(manifest.workspaces) || typeof manifest.workspaces === "object" && manifest.workspaces !== null && Array.isArray(manifest.workspaces.packages);
} catch {
return false;
}
}
function managedPackageBoundaryError(cwd, packageRoot, managedPaths) {
return /* @__PURE__ */ new Error(`Managed package paths must stay within the project or workspace boundary discovered from ${cwd}: ${[packageRoot, ...managedPaths].join(", ")}`);
}
async function commitFileSystemTransaction(root, writes, removals) {
const requestedTransactionRoot = resolve(root);
for (const path of [...writes.flatMap(({ destination, removeBeforeWrite }) => removeBeforeWrite ? [destination, removeBeforeWrite] : [destination]), ...removals]) assertFileSystemTransactionPathIsNotReserved(requestedTransactionRoot, resolveTransactionPath(requestedTransactionRoot, path));
const capability = fileSystemReconciliationCapability.getStore();
if (capability) {
const transactionRoot = await canonicalizeReconciliationPath(requestedTransactionRoot);
if (capability.roots.has(fileSystemReconciliationCapabilityRoot(transactionRoot))) return commitFileSystemTransactionUnlocked(requestedTransactionRoot, transactionRoot, writes, removals);
}
return withFileSystemReconciliation(requestedTransactionRoot, async () => {
const transactionRoot = await canonicalizeReconciliationPath(requestedTransactionRoot);
await commitFileSystemTransactionUnlocked(requestedTransactionRoot, transactionRoot, writes, removals);
});
}
async function commitFileSystemTransactionUnlocked(requestedTransactionRoot, transactionRoot, writes, removals) {
const transactionWrites = await Promise.all(writes.map(async (write) => {
const destination = await resolveCanonicalTransactionPath(requestedTransactionRoot, transactionRoot, write.destination);
await mkdir(dirname(destination), { recursive: true });
const stableDestination = await resolveCanonicalTransactionPath(requestedTransactionRoot, transactionRoot, write.destination);
if (!pathsHaveEquivalentPlatformSpelling(destination, stableDestination)) throw new Error(`Filesystem transaction destination parent changed while it was created: ${write.destination}`);
return {
destination: stableDestination,
mode: write.mode,
removeBeforeWrite: write.removeBeforeWrite ? await resolveCanonicalTransactionPath(requestedTransactionRoot, transactionRoot, write.removeBeforeWrite) : void 0,
source: write.source
};
}));
const destinationKeys = /* @__PURE__ */ new Set();
for (const { destination } of transactionWrites) {
const destinationKey = fileSystemReconciliationCapabilityRoot(destination);
if (destinationKeys.has(destinationKey)) throw new Error(`Duplicate canonical filesystem transaction destination: ${destination}`);
destinationKeys.add(destinationKey);
}
const writesByDestination = new Map(transactionWrites.map((write) => [write.destination, write]));
const resolvedRemovals = await Promise.all(removals.map((path) => resolveCanonicalTransactionPath(requestedTransactionRoot, transactionRoot, path)));
const preWriteRemovals = new Set(transactionWrites.flatMap(({ removeBeforeWrite }) => removeBeforeWrite ? [removeBeforeWrite] : []));
const affected = /* @__PURE__ */ new Set([
...writesByDestination.keys(),
...resolvedRemovals,
...preWriteRemovals
]);
const journalRoot = fileSystemTransactionJournalPath(transactionRoot);
for (const path of affected) assertFileSystemTransactionPathIsNotReserved(transactionRoot, path);
const affectedStats = /* @__PURE__ */ new Map();
for (const path of affected) {
const stats = await lstatIfExists$1(path, { bigint: true });
if (stats && !stats.isFile()) throw new Error(`Filesystem transaction path is not a regular file: ${path}`);
affectedStats.set(path, stats);
}
const replacementAliasParents = /* @__PURE__ */ new Map();
const findReplacementAlias = (path) => {
const parent = replacementAliasParents.get(path);
if (parent === void 0) {
replacementAliasParents.set(path, path);
return path;
}
if (parent === path) return path;
const root = findReplacementAlias(parent);
replacementAliasParents.set(path, root);
return root;
};
const joinReplacementAliases = (left, right) => {
const leftRoot = findReplacementAlias(left);
const rightRoot = findReplacementAlias(right);
if (leftRoot !== rightRoot) replacementAliasParents.set(rightRoot, leftRoot);
};
for (const { destination, removeBeforeWrite } of transactionWrites) {
if (!removeBeforeWrite) continue;
const replacementStats = affectedStats.get(removeBeforeWrite);
if (!replacementStats) throw new Error(`Filesystem transaction replacement source does not exist: ${removeBeforeWrite}`);
const destinationStats = affectedStats.get(destination);
if (destinationStats) {
if (!await pathsReferToSameDirectoryEntry(removeBeforeWrite, destination, replacementStats, destinationStats)) throw new Error(`Filesystem transaction replacement destination is occupied: ${destination}`);
joinReplacementAliases(removeBeforeWrite, destination);
}
}
const replacementAliasBackups = /* @__PURE__ */ new Map();
for (const { removeBeforeWrite } of transactionWrites) {
if (!removeBeforeWrite || !replacementAliasParents.has(removeBeforeWrite)) continue;
const root = findReplacementAlias(removeBeforeWrite);
if (!replacementAliasBackups.has(root)) replacementAliasBackups.set(root, removeBeforeWrite);
}
const replacementBackupPaths = new Set(replacementAliasBackups.values());
const parentIdentities = /* @__PURE__ */ new Map();
for (const path of affected) {
const parent = dirname(path);
if (!parentIdentities.has(parent)) parentIdentities.set(parent, await captureTransactionParentIdentity(transactionRoot, parent));
}
const { path: candidateRoot, stats: candidateStats, token: transactionToken } = await createFileSystemTransactionCandidate(transactionRoot);
const candidateBackupRoot = join(candidateRoot, "backups");
const candidateInputRoot = join(candidateRoot, "inputs");
const publishedBackupRoot = join(journalRoot, "backups");
const backups = /* @__PURE__ */ new Map();
let preserveJournalRoot = false;
let journal;
let committed = false;
let published = false;
let transactionError;
try {
await writeFileAtomic(join(candidateRoot, fileSystemTransactionOwnerName), JSON.stringify({
kind: fileSystemTransactionKind,
token: transactionToken,
version: fileSystemTransactionStateVersion
}), { mode: 420 });
await syncDirectory(transactionRoot);
await Promise.all([mkdir(candidateBackupRoot), mkdir(candidateInputRoot)]);
const preparedWrites = [];
const snapshots = /* @__PURE__ */ new Map();
for (let index = 0; index < transactionWrites.length; index++) {
const write = transactionWrites[index];
let snapshot = snapshots.get(write.source);
if (!snapshot) {
const inputName = String(snapshots.size);
const inputPath = join(candidateInputRoot, inputName);
snapshot = {
final: fileSystemTransactionFileContents(await snapshotFileSystemTransactionInput(write.source, inputPath)),
input: join("inputs", inputName)
};
snapshots.set(write.source, snapshot);
}
preparedWrites.push({
destination: write.destination,
final: {
hash: snapshot.final.hash,
mode: write.mode ?? snapshot.final.mode
},
input: snapshot.input,
removeBeforeWrite: write.removeBeforeWrite
});
}
for (let index = 0; index < preparedWrites.length; index++) {
const write = preparedWrites[index];
const prepared = fileSystemTransactionArtifactPath(write.destination, transactionToken, index, "prepared");
if (await lstatIfExists$1(prepared) !== void 0) throw new Error(`Filesystem transaction artifact path already exists for ${write.destination}`);
write.prepared = prepared;
}
const artifactPaths = /* @__PURE__ */ new Map();
let artifactIndex = 0;
for (const path of affected) {
const retired = fileSystemTransactionArtifactPath(path, transactionToken, artifactIndex, "retired");
const rollbackRetired = fileSystemTransactionArtifactPath(path, transactionToken, artifactIndex, "rollback");
artifactIndex += 1;
if (await lstatIfExists$1(retired) !== void 0 || await lstatIfExists$1(rollbackRetired) !== void 0) throw new Error(`Filesystem transaction artifact path already exists for ${path}`);
artifactPaths.set(path, {
retired,
rollbackRetired
});
}
const journalParentForPath = (path) => {
const parentIdentity = parentIdentities.get(dirname(path));
if (!parentIdentity) throw new Error(`Filesystem transaction parent metadata was not captured for ${path}`);
return {
canonicalParent: fileSystemTransactionRelativePath(transactionRoot, parentIdentity.canonicalParent, "Transaction canonical parent", true),
dev: parentIdentity.dev,
identityPath: fileSystemTransactionRelativePath(transactionRoot, parentIdentity.identityPath, "Transaction parent identity", true),
ino: parentIdentity.ino
};
};
const preparingJournal = {
entries: preparedWrites.map((write) => ({
final: write.final,
parent: journalParentForPath(write.destination),
path: fileSystemTransactionRelativePath(transactionRoot, write.destination, "Transaction path"),
prepared: fileSystemTransactionRelativePath(transactionRoot, write.prepared, "Transaction prepared replacement")
})),
phase: "preparing",
token: transactionToken,
version: fileSystemTransactionStateVersion
};
await writeFileSystemTransactionJournal(candidateRoot, preparingJournal);
journal = preparingJournal;
let preparedIdentityCount = 0;
let preparedIdentityPublicationClosed = false;
let resolvePreparedIdentityPublication;
let rejectPreparedIdentityPublication;
const preparedIdentityPublication = new Promise((resolve, reject) => {
resolvePreparedIdentityPublication = resolve;
rejectPreparedIdentityPublication = reject;
});
preparedIdentityPublication.catch(() => {});
const failPreparedIdentityPublication = (error) => {
if (!preparedIdentityPublicationClosed) {
preparedIdentityPublicationClosed = true;
rejectPreparedIdentityPublication(error);
}
};
const recordPreparedIdentity = (index, identity) => {
if (!preparedIdentityPublicationClosed) {
const entry = preparingJournal.entries[index];
if (!(entry === null || entry === void 0 ? void 0 : entry.final)) {
var _preparedWrites$index;
failPreparedIdentityPublication(/* @__PURE__ */ new Error(`Filesystem transaction preparing metadata was not captured for ${(_preparedWrites$index = preparedWrites[index]) === null || _preparedWrites$index === void 0 ? void 0 : _preparedWrites$index.destination}`));
} else {
entry.final = {
...entry.final,
...identity
};
preparedIdentityCount += 1;
if (preparedIdentityCount === preparedWrites.length) {
preparedIdentityPublicationClosed = true;
writeFileSystemTransactionJournal(candidateRoot, preparingJournal).then(resolvePreparedIdentityPublication, rejectPreparedIdentityPublication);
}
}
}
return preparedIdentityPublication;
};
const preparations = preparedWrites.map(async (write, index) => {
try {
await assertTransactionParentUnchanged(transactionRoot, write.destination, parentIdentities);
write.final = await prepareFileSystemTransactionReplacement(join(candidateInputRoot, basename(write.input)), write.prepared, write.final, () => assertTransactionParentUnchanged(transactionRoot, write.destination, parentIdentities), (identity) => recordPreparedIdentity(index, identity));
} catch (error) {
failPreparedIdentityPublication(error);
throw error;
}
});
try {
await Promise.all(preparations);
} catch (error) {
await Promise.allSettled(preparations);
throw error;
}
const preparedWritesByDestination = new Map(preparedWrites.map((write) => [write.destination, write]));
let backupIndex = 0;
for (const path of affected) {
if (replacementAliasParents.has(path) && !replacementBackupPaths.has(path)) continue;
const stats = affectedStats.get(path);
if (!stats) continue;
await assertTransactionParentUnchanged(transactionRoot, path, parentIdentities);
const backupName = String(backupIndex++);
const state = await snapshotFileSystemTransactionInput(path, join(candidateBackupRoot, backupName), Number(stats.mode & 4095n), stats);
backups.set(path, {
path: join(publishedBackupRoot, backupName),
state
});
}
const backupForPath = (path) => {
const direct = backups.get(path);
if (direct) return direct;
if (!replacementAliasParents.has(path)) return;
const aliasBackupPath = replacementAliasBackups.get(findReplacementAlias(path));
return aliasBackupPath ? backups.get(aliasBackupPath) : void 0;
};
const finalWriteForPath = (path) => {
const direct = preparedWritesByDestination.get(path);
if (direct) return direct;
if (!replacementAliasParents.has(path)) return;
const aliasRoot = findReplacementAlias(path);
return preparedWrites.find((write) => replacementAliasParents.has(write.destination) && findReplacementAlias(write.destination) === aliasRoot);
};
const preparedJournal = {
entries: [...affected].map((path) => {
const originalStats = affectedStats.get(path);
const backup = backupForPath(path);
const finalWrite = finalWriteForPath(path);
const artifacts = artifactPaths.get(path);
if (!artifacts) throw new Error(`Filesystem transaction metadata was not captured for ${path}`);
if (finalWrite && !finalWrite.prepared) throw new Error(`Filesystem transaction replacement was not prepared for ${path}`);
return {
backup: backup ? fileSystemTransactionRelativePath(transactionRoot, backup.path, "Transaction backup") : void 0,
final: finalWrite === null || finalWrite === void 0 ? void 0 : finalWrite.final,
original: originalStats && backup ? backup.state : void 0,
parent: journalParentForPath(path),
path: fileSystemTransactionRelativePath(transactionRoot, path, "Transaction path"),
prepared: finalWrite ? fileSystemTransactionRelativePath(transactionRoot, finalWrite.prepared, "Transaction prepared replacement") : void 0,
retired: fileSystemTransactionRelativePath(transactionRoot, artifacts.retired, "Transaction retirement path"),
rollbackRetired: fileSystemTransactionRelativePath(transactionRoot, artifacts.rollbackRetired, "Transaction rollback retirement path")
};
}),
phase: "prepared",
token: transactionToken,
version: fileSystemTransactionStateVersion
};
await writeFileSystemTransactionJournal(candidateRoot, preparedJournal);
journal = preparedJournal;
const journalEntriesByPath = new Map(preparedJournal.entries.map((entry) => [resolveFileSystemTransactionRelativePath(transactionRoot, entry.path, "Transaction path"), entry]));
await Promise.all([syncDirectory(candidateBackupRoot), syncDirectory(candidateInputRoot)]);
await syncDirectory(candidateRoot);
if (await lstatIfExists$1(journalRoot)) throw new Error(`Filesystem transaction recovery state already exists: ${journalRoot}`);
await rename(candidateRoot, journalRoot);
published = true;
await syncDirectory(transactionRoot);
if (!fileSystemTransactionStateMatches(candidateStats, await lstatIfExists$1(journalRoot, { bigint: true }))) throw new Error(`Filesystem transaction recovery state changed during publication: ${journalRoot}`);
if ((await readFileSystemTransactionOwner(transactionRoot)).token !== transactionToken) throw new Error(`Filesystem transaction recovery state owner changed during publication: ${journalRoot}`);
for (const path of preWriteRemovals) {
const entry = journalEntriesByPath.get(path);
if (!entry) throw new Error(`Filesystem transaction journal omitted path: ${path}`);
await removeFileSystemTransactionPath(transactionRoot, path, parentIdentities, entry.original, resolveFileSystemTransactionRelativePath(transactionRoot, entry.retired, "Transaction retirement path"), "before pre-write removal");
}
for (const write of preparedWrites) {
const { destination } = write;
const entry = journalEntriesByPath.get(destination);
if (!entry || !entry.final || !entry.prepared || !entry.retired) throw new Error(`Filesystem transaction journal omitted write destination: ${destination}`);
await commitFileSystemTransactionReplacement(transactionRoot, destination, parentIdentities, write.removeBeforeWrite && replacementAliasParents.has(write.removeBeforeWrite) && replacementAliasParents.has(destination) && findReplacementAlias(write.removeBeforeWrite) === findReplacementAlias(destination) ? void 0 : entry.original, resolveFileSystemTransactionRelativePath(transactionRoot, entry.prepared, "Transaction prepared replacement"), entry.final, resolveFileSystemTransactionRelativePath(transactionRoot, entry.retired, "Transaction retirement path"));
}
for (const path of affected) if (!writesByDestination.has(path) && !preWriteRemovals.has(path)) {
const entry = journalEntriesByPath.get(path);
if (!entry) throw new Error(`Filesystem transaction journal omitted path: ${path}`);
await removeFileSystemTransactionPath(transactionRoot, path, parentIdentities, entry.original, resolveFileSystemTransactionRelativePath(transactionRoot, entry.retired, "Transaction retirement path"), "before transaction removal");
}
journal = {
...journal,
phase: "committed"
};
await writeFileSystemTransactionJournal(journalRoot, journal);
committed = true;
} catch (error) {
transactionError = error;
if (published && journal && !committed) {
const rollbackErrors = [];
for (const rollbackError of await rollbackFileSystemTransaction(transactionRoot, journal)) rollbackErrors.push(rollbackError);
if (rollbackErrors.length > 0) {
preserveJournalRoot = true;
transactionError = new AggregateError([error, ...rollbackErrors], `Filesystem transaction failed and rollback was incomplete; recovery state is preserved at ${journalRoot}`, { cause: error });
}
}
}
let cleanupError;
if (!preserveJournalRoot) try {
if (published) {
if (journal) await cleanupFileSystemTransactionArtifacts(transactionRoot, journal);
await removeFileSystemTransactionJournal(transactionRoot, transactionToken, candidateStats);
} else {
if (journal) await cleanupUnpublishedFileSystemTransactionArtifacts(transactionRoot, journal);
await removeFileSystemTransactionCandidate(transactionRoot, candidateRoot, candidateStats);
}
} catch (error) {
cleanupError = error;
}
if (transactionError !== void 0) {
if (cleanupError !== void 0) throw new AggregateError([transactionError, cleanupError], `Filesystem transaction failed and its recovery state could not be removed from ${journalRoot}`, { cause: transactionError });
throw transactionError;
}
if (cleanupError !== void 0) debug$10.warn(`Filesystem transaction committed but recovery-state cleanup failed at ${journalRoot}: ${errorMessage(cleanupError)}`);
}
async function pathsReferToSameDirectoryEntry(left, right, leftStats, rightStats) {
if (resolve(left) === resolve(right)) return true;
if (!leftStats.isFile() || !rightStats.isFile() || !statIdentitiesMatch(leftStats, rightStats)) return false;
if (process.platform !== "darwin" && process.platform !== "win32") return false;
const [leftParent, rightParent] = await Promise.all([realpath(dirname(left)), realpath(dirname(right))]);
if (!pathsHaveEquivalentPlatformSpelling(leftParent, rightParent)) return false;
const entries = await readdir(leftParent);
const leftName = basename(left);
const rightName = basename(right);
return !(leftName !== rightName && entries.includes(leftName) && entries.includes(rightName));
}
async function lstatIfExists$1(path, options) {
try {
return (options === null || options === void 0 ? void 0 : options.bigint) ? await lstat(path, { bigint: true }) : await lstat(path);
} catch (error) {
if (error.code === "ENOENT") return;
throw error;
}
}
/**
* Exact 64-bit dev/ino identity equality between two bigint stat observations.
* Every ownership or continuity decision in the reconciliation and transaction
* subsystems must compare identity through this helper (or on the decimal
* strings derived from a bigint stat), never on the lossy Number
* `Stats.dev`/`Stats.ino` fields: Windows NTFS file IDs and volume serials
* exceed Number.MAX_SAFE_INTEGER, so two distinct filesystem objects can
* collapse onto the same JS double and defeat the check.
*/
function statIdentitiesMatch(expected, current) {
return current !== void 0 && expected.dev === current.dev && expected.ino === current.ino;
}
async function readReconciliationMetadata(path, label) {
let handle;
try {
handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
} catch (error) {
if (error.code === "ELOOP") throw reconciliationPathCollisionError(path, `is not a regular ${label} file`);
throw error;
}
try {
const stats = await handle.stat({ bigint: true });
if (!stats.isFile()) throw reconciliationPathCollisionError(path, `is not a regular ${label} file`);
const content = Buffer.allocUnsafe(65537);
let offset = 0;
while (offset < content.length) {
const { bytesRead } = await handle.read(content, offset, content.length - offset, offset);
if (bytesRead === 0) break;
offset += bytesRead;
}
if (offset > reconciliationMetadataMaximumSize) throw reconciliationPathCollisionError(path, `exceeds the maximum ${label} size`);
return {
content: content.subarray(0, offset).toString("utf8"),
stats
};
} finally {
await handle.close();
}
}
async function canonicalizeReconciliationPath(path) {
let current = resolve(path);
const missingSegments = [];
while (true) try {
return join(await realpath(current), ...missingSegments);
} catch (error) {
if (error.code !== "ENOENT") throw error;
const parent = dirname(current);
if (parent === current) return join(current, ...missingSegments);
missingSegments.unshift(basename(current));
current = parent;
}
}
async function resolveReconciliationLockIdentities(path) {
const requestedPath = resolve(path);
const anchorPath = await canonicalizeReconciliationPath(path);
let anchorStats;
try {
anchorStats = await lstat(anchorPath, { bigint: true });
} catch (error) {
if (error.code === "ENOENT") throw reconciliationAnchorError(anchorPath, "does not exist", "ENOENT");
throw error;
}
if (!anchorStats.isDirectory()) throw reconciliationAnchorError(anchorPath, "is not a directory", "ENOTDIR");
const guardKeys = /* @__PURE__ */ new Set();
if (await directoryIsWritable(dirname(anchorPath))) guardKeys.add(anchorPath);
let currentPath = requestedPath;
while (true) {
const currentStats = await lstatIfExists$1(currentPath);
if (currentStats === null || currentStats === void 0 ? void 0 : currentStats.isSymbolicLink()) {
if (await directoryIsWritable(dirname(currentPath))) guardKeys.add(currentPath);
}
const parent = dirname(currentPath);
if (parent === currentPath) break;
currentPath = parent;
}
const pathIdentities = await Promise.all([...guardKeys].map(async (key) => {
const lockRootPath = await realpath(dirname(key));
const lockRootStats = await lstat(lockRootPath, { bigint: true });
if (!lockRootStats.isDirectory()) throw reconciliationPathCollisionError(lockRootPath, "is not a lock namespace directory");
return {
anchorPath,
dev: anchorStats.dev,
ino: anchorStats.ino,
key,
lockRootDev: lockRootStats.dev,
lockRootIno: lockRootStats.ino,
lockRootPath,
reportTopologyChange: key === anchorPath,
requestedPath
};
}));
const anchorParentPath = await realpath(dirname(anchorPath));
const anchorParentStats = await lstat(anchorParentPath, { bigint: true });
const anchorParentIsShared = (anchorParentStats.mode & 512n) === 0n && await directoryIsWritable(anchorParentPath);
const objectLockRootPath = anchorParentIsShared ? anchorParentPath : anchorPath;
const objectLockRootStats = anchorParentIsShared ? anchorParentStats : anchorStats;
if (!anchorParentIsShared && !await directoryIsWritable(anchorPath)) throw reconciliationPathCollisionError(anchorPath, "does not provide a writable lock namespace");
const objectIdentity = {
anchorPath,
dev: anchorStats.dev,
ino: anchorStats.ino,
key: `inode:${anchorStats.ino}`,
lockRootDev: objectLockRootStats.dev,
lockRootIno: objectLockRootStats.ino,
lockRootPath: objectLockRootPath,
reportTopologyChange: !guardKeys.has(anchorPath),
requestedPath
};
return [...pathIdentities, objectIdentity].sort((left, right) => {
const leftPath = reconciliationLockPath(left);
const rightPath = reconciliationLockPath(right);
return leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0;
});
}
async function directoryIsWritable(path) {
try {
await access(path, constants.W_OK);
return true;
} catch {
return false;
}
}
async function assertReconciliationRequestedPathUnchanged(identity) {
let currentPath;
try {
currentPath = await canonicalizeReconciliationPath(identity.requestedPath);
} catch {
throw reconciliationAnchorError(identity.requestedPath, "changed after lock identity was captured", "ESTALE");
}
const currentStats = await lstatIfExists$1(currentPath, { bigint: true });
if (!(currentStats === null || currentStats === void 0 ? void 0 : currentStats.isDirectory()) || !statIdentitiesMatch(identity, currentStats)) throw reconciliationAnchorError(identity.requestedPath, "changed after lock identity was captured", "ESTALE");
}
async function assertReconciliationAnchorUnchanged(identity) {
let anchorStats;
try {
anchorStats = await lstat(identity.anchorPath, { bigint: true });
} catch (error) {
if (error.code === "ENOENT") throw reconciliationAnchorError(identity.anchorPath, "changed after lock identity was captured", "ESTALE");
throw error;
}
if (!anchorStats.isDirectory() || !statIdentitiesMatch(identity, anchorStats)) throw reconciliationAnchorError(identity.anchorPath, "changed after lock identity was captured", "ESTALE");
}
async function assertReconciliationLockRootUnchanged(identity) {
const lockRootStats = await lstatIfExists$1(identity.lockRootPath, { bigint: true });
if (!(lockRootStats === null || lockRootStats === void 0 ? void 0 : lockRootStats.isDirectory()) || !statIdentitiesMatch({
dev: identity.lockRootDev,
ino: identity.lockRootIno
}, lockRootStats)) throw reconciliationAnchorError(identity.lockRootPath, "changed after lock identity was captured", "ESTALE");
}
function reconciliationAnchorError(anchorPath, message, code) {
const error = /* @__PURE__ */ new Error(`Filesystem reconciliation root ${anchorPath} ${message}`);
error.code = code;
error.path = anchorPath;
return error;
}
function reconciliationLockPath(identity) {
return join(identity.lockRootPath, `${reconciliationLockName}.${createHash("sha256").update(identity.key).digest("hex")}${reconciliationMetadataExtension}`);
}
function reconciliationReclaimPath(identity) {
return join(identity.lockRootPath, `${reconciliationLockName}${reconciliationReclaimMarker}${createHash("sha256").update(identity.key).digest("hex")}${reconciliationMetadataExtension}`);
}
function reconciliationCandidateName(path, token) {
return `${basename(path, extname(path))}${reconciliationCandidateMarker}${token}${reconciliationMetadataExtension}`;
}
function reconciliationCandidatePath(path, token) {
return join(dirname(path), reconciliationCandidateName(path, token));
}
function reconciliationRetiredPath(path) {
return join(dirname(path), `${basename(path, extname(path))}${reconciliationRetiredMarker}${randomUUID()}${reconciliationMetadataExtension}`);
}
async function acquireReconciliationLock(identity, acquisitionDeadline) {
const { key } = identity;
const [incarnation, executionIdentity] = await Promise.all([getCurrentProcessIncarnation(), getCurrentProcessExecutionIdentity()]);
const lockPath = reconciliationLockPath(identity);
await assertReconciliationAnchorUnchanged(identity);
await assertReconciliationRequestedPathUnchanged(identity);
await assertReconciliationLockRootUnchanged(identity);
await removeStaleReconciliationCandidates(identity, lockPath, isReconciliationLockOwner);
await removeStaleReconciliationCandidates(identity, reconciliationReclaimPath(identity), isReconciliationReclaimOwner);
while (true) {
assertReconciliationLockAcquisitionTimeRemaining(acquisitionDeadline, key);
await assertReconciliationAnchorUnchanged(identity);
await assertReconciliationRequestedPathUnchanged(identity);
await assertReconciliationLockRootUnchanged(identity);
await waitForReconciliationReclaim(identity, acquisitionDeadline);
const token = randomUUID();
const owner = {
candidate: reconciliationCandidateName(lockPath, token),
createdAt: Date.now(),
boot: executionIdentity.boot,
bootSession: executionIdentity.bootSession,
incarnation,
key,
kind: reconciliationLockKind,
machine: executionIdentity.machine,
namespace: executionIdentity.namespace,
pid: process.pid,
token,
version: reconciliationStateVersion
};
let lockStats;
try {
lockStats = await createExclusiveReconciliationMetadata(identity, lockPath, owner);
} catch (error) {
if (isReconciliationCandidateCollisionError(error)) continue;
if (!await reconciliationMetadataAlreadyExists(lockPath, error)) throw error;
const state = await inspectReconciliationLock(identity, lockPath);
if (state === null || state === void 0 ? void 0 : state.unverifiableReason) {
await waitForUnverifiableReconciliationLock(identity, lockPath, state, acquisitionDeadline);
continue;
}
if ((state === null || state === void 0 ? void 0 : state.owner) && state.stale && await tryReclaimStaleReconciliationLock(identity, lockPath, state)) continue;
await delayReconciliationLockRetry(acquisitionDeadline, key);
continue;
}
try {
await waitForReconciliationReclaim(identity, acquisitionDeadline);
if (!await reconciliationLockIsOwnedBy(identity, lockPath, lockStats, token)) throw new Error(`Lost filesystem reconciliation lock ownership before initialization: ${key}`);
return maintainReconciliationLock(identity, lockPath, lockStats, token);
} catch (error) {
await cleanupFailedReconciliationLock(identity, lockPath, lockStats, token);
throw error;
}
}
}
async function createExclusiveReconciliationMetadata(identity, path, owner) {
const candidatePath = reconciliationCandidatePath(path, owner.token);
if (owner.candidate !== basename(candidatePath)) throw new Error(`Invalid filesystem reconciliation candidate name: ${owner.candidate}`);
await assertReconciliationLockRootUnchanged(identity);
let handle;
try {
handle = await open(candidatePath, "wx", 420);
} catch (error) {
if (error.code === "EEXIST") throw reconciliationCandidateCollisionError(candidatePath);
throw error;
}
const createdStats = await handle.stat({ bigint: true });
let published = false;
try {
if (process.platform !== "win32") await handle.chmod(420);
await handle.writeFile(JSON.stringify(owner), "utf8");
await handle.sync();
await handle.close();
await assertReconciliationLockRootUnchanged(identity);
try {
await link(candidatePath, path);
published = true;
} catch (error) {
if (error.code === "EEXIST") throw reconciliationMetadataExistsError(path);
if (isUnsupportedReconciliationHardLinkError(error)) throw reconciliationHardLinkRequiredError(path, error);
throw error;
}
await assertReconciliationLockRootUnchanged(identity);
if (!await reconciliationPathMatches(path, createdStats)) throw new Error(`Lost filesystem reconciliation metadata: ${path}`);
} catch (error) {
await handle.close().catch(() => {});
try {
await assertReconciliationLockRootUnchanged(identity);
if (published) await retireReconciliationPathIfMatches(identity, path, createdStats, async (retiredPath) => await readFile(retiredPath, "utf8") === JSON.stringify(owner));
await removeReconciliationCandidateIfMatches(identity, candidatePath, createdStats);
} catch (cleanupError) {
if (!isReconciliationAnchorChangedError(cleanupError)) debug$10.warn(`Failed to clean up filesystem reconciliation metadata: ${errorMessage(cleanupError)}`);
}
throw error;
}
try {
await removeReconciliationCandidateIfMatches(identity, candidatePath, createdStats);
} catch (cleanupError) {
if (!isReconciliationAnchorChangedError(cleanupError)) debug$10.warn(`Failed to remove filesystem reconciliation publication candidate: ${errorMessage(cleanupError)}`);
}
return createdStats;
}
async function cleanupFailedReconciliationLock(identity, lockPath, lockStats, token) {
try {
await waitForReconciliationReclaim(identity, createReconciliationLockDeadline(reconciliationLockCleanupTimeout));
} catch (cleanupError) {
debug$10.warn(`Failed to wait for filesystem reconciliation reclaim during acquisition cleanup: ${errorMessage(cleanupError)}`);
if (isReconciliationAnchorChangedError(cleanupError) || isReconciliationPathCollisionError(cleanupError)) return;
scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token);
return;
}
try {
if (await removeOwnedReconciliationLock(identity, lockPath, lockStats, token) === "blocked") scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token);
} catch (cleanupError) {
debug$10.warn(`Failed to clean up filesystem reconciliation lock after acquisition failure: ${errorMessage(cleanupError)}`);
if (!isReconciliationAnchorChangedError(cleanupError) && !isReconciliationPathCollisionError(cleanupError)) scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token);
}
}
function scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token) {
setTimeout(() => {
retryFailedReconciliationLockCleanup(identity, lockPath, lockStats, token).then((complete) => {
if (!complete) scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token);
}).catch((cleanupError) => {
debug$10.warn(`Failed to retry filesystem reconciliation lock cleanup: ${errorMessage(cleanupError)}`);
if (!isReconciliationAnchorChangedError(cleanupError) && !isReconciliationPathCollisionError(cleanupError)) scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token);
});
}, reconciliationLockCleanupRetryInterval).unref();
}
async function retryFailedReconciliationLockCleanup(identity, lockPath, lockStats, token) {
await assertReconciliationLockRootUnchanged(identity);
if (await pathExistsAsync(reconciliationReclaimPath(identity))) {
await removeStaleReconciliationReclaim(identity);
if (await pathExistsAsync(reconciliationReclaimPath(identity))) return false;
}
return await removeOwnedReconciliationLock(identity, lockPath, lockStats, token) !== "blocked";
}
function isReconciliationAnchorChangedError(error) {
return error.code === "ESTALE";
}
function maintainReconciliationLock(identity, lockPath, lockStats, token) {
const { key } = identity;
return async () => {
let anchorError;
if (identity.reportTopologyChange) try {
await assertReconciliationAnchorUnchanged(identity);
await assertReconciliationRequestedPathUnchanged(identity);
} catch (error) {
anchorError = error;
}
await assertReconciliationLockRootUnchanged(identity);
try {
await waitForReconciliationReclaim(identity, createReconciliationLockDeadline(reconciliationLockCleanupTimeout));
} catch (error) {
if (!isReconciliationAnchorChangedError(error) && !isReconciliationPathCollisionError(error)) scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token);
throw error;
}
let cleanupResult;
try {
cleanupResult = await removeOwnedReconciliationLock(identity, lockPath, lockStats, token);
} catch (error) {
if (!isReconciliationAnchorChangedError(error) && !isReconciliationPathCollisionError(error)) scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token);
throw error;
}
if (cleanupResult === "blocked") {
scheduleFailedReconciliationLockCleanup(identity, lockPath, lockStats, token);
const error = /* @__PURE__ */ new Error(`Filesystem reconciliation lock cleanup was blocked by a reclaimer: ${key}`);
error.code = "EBUSY";
throw error;
}
if (anchorError !== void 0) throw anchorError;