85 lines
2.4 KiB
JavaScript
85 lines
2.4 KiB
JavaScript
import { randomUUID } from 'node:crypto';
|
|
import { access, rename, rm } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
async function exists(candidate) {
|
|
return access(candidate).then(
|
|
() => true,
|
|
() => false
|
|
);
|
|
}
|
|
|
|
function backupPath(target) {
|
|
return path.join(
|
|
path.dirname(target),
|
|
`.${path.basename(target)}.toolbox-backup-${randomUUID()}`
|
|
);
|
|
}
|
|
|
|
export async function publishStagedTargets(
|
|
targets,
|
|
{ force = false, renamePath = rename, removePath = rm } = {}
|
|
) {
|
|
if (!Array.isArray(targets) || targets.length === 0)
|
|
throw new Error('At least one staged publication target is required.');
|
|
|
|
const normalized = targets.map(({ staged, target }) => ({
|
|
staged: path.resolve(staged),
|
|
target: path.resolve(target),
|
|
}));
|
|
if (
|
|
new Set(normalized.map(({ target }) => target)).size !== normalized.length
|
|
)
|
|
throw new Error('Publication targets must be unique.');
|
|
|
|
for (const item of normalized) {
|
|
if (!(await exists(item.staged)))
|
|
throw new Error(`Staged publication input is missing: ${item.staged}`);
|
|
if (!force && (await exists(item.target)))
|
|
throw new Error(`Publication target already exists: ${item.target}`);
|
|
}
|
|
|
|
const backups = [];
|
|
const published = [];
|
|
try {
|
|
for (const item of normalized) {
|
|
if (!(await exists(item.target))) continue;
|
|
const backup = backupPath(item.target);
|
|
await renamePath(item.target, backup);
|
|
backups.push({ target: item.target, backup });
|
|
}
|
|
|
|
for (const item of normalized) {
|
|
await renamePath(item.staged, item.target);
|
|
published.push(item.target);
|
|
}
|
|
} catch (publicationError) {
|
|
const rollbackErrors = [];
|
|
for (const target of [...published].reverse()) {
|
|
try {
|
|
await removePath(target, { recursive: true, force: true });
|
|
} catch (error) {
|
|
rollbackErrors.push(error);
|
|
}
|
|
}
|
|
for (const item of [...backups].reverse()) {
|
|
try {
|
|
await renamePath(item.backup, item.target);
|
|
} catch (error) {
|
|
rollbackErrors.push(error);
|
|
}
|
|
}
|
|
if (rollbackErrors.length > 0) {
|
|
throw new AggregateError(
|
|
[publicationError, ...rollbackErrors],
|
|
'Release publication failed and rollback was incomplete.'
|
|
);
|
|
}
|
|
throw publicationError;
|
|
}
|
|
|
|
for (const { backup } of backups) {
|
|
await removePath(backup, { recursive: true, force: true });
|
|
}
|
|
}
|