next
Version:
The React Framework
124 lines (123 loc) • 5.05 kB
JavaScript
import * as path from 'node:path';
import { cpSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { z } from 'next/dist/compiled/zod';
import { getGitBranch, getGitCommit, getGitDirty, getGitMessage } from '../../lib/helpers/git';
const MAX_HISTORY = 20;
export const snapshotNameSchema = z.string().min(1).refine((name)=>{
try {
encodeURIComponent(name);
return true;
} catch {
return false;
}
}, 'Invalid analyzer snapshot name');
/** On-disk metadata, mirrored by the analyzer UI. Names are the only snapshot keys. */ export const snapshotMetadataSchema = z.object({
name: snapshotNameSchema,
createdAt: z.string().datetime(),
nextVersion: z.string().optional(),
gitBranch: z.string().optional(),
gitSha: z.string().optional(),
gitShortSha: z.string().optional(),
gitDirty: z.boolean().optional(),
gitMessage: z.string().optional(),
appDirOnly: z.boolean().optional(),
noMangling: z.boolean().optional(),
routeCount: z.number().int().nonnegative()
});
export const historyIndexSchema = z.object({
snapshots: z.array(snapshotMetadataSchema)
});
/** Encode names into one portable path segment, including dots and reserved names. */ export function snapshotDirectory(name) {
return `snapshot-${encodeURIComponent(snapshotNameSchema.parse(name))}`;
}
const DATA_DIRNAME = 'data';
const HISTORY_DIRNAME = 'history';
const METADATA_FILENAME = 'metadata.json';
const HISTORY_INDEX_FILENAME = 'history.json';
/** Save the current analysis and refresh the rolling, newest-first history. */ export function writeAnalyzeSnapshot({ projectDir, analyzeDir, routes, appDirOnly, noMangling, snapshot, maxHistory = MAX_HISTORY }) {
const dataDir = path.join(analyzeDir, DATA_DIRNAME);
const historyDir = path.join(analyzeDir, HISTORY_DIRNAME);
const createdAt = new Date();
mkdirSync(historyDir, {
recursive: true
});
let name;
let snapshotDir;
if (snapshot !== undefined) {
name = snapshotNameSchema.parse(snapshot);
snapshotDir = path.join(historyDir, snapshotDirectory(name));
// Replacement must not merge stale files from the previous capture.
rmSync(snapshotDir, {
recursive: true,
force: true,
maxRetries: 3
});
mkdirSync(snapshotDir);
} else {
const timestamp = createdAt.toISOString().replace(/[:.]/g, '-');
for(let attempt = 0;; attempt++){
name = attempt === 0 ? timestamp : `${timestamp}-${attempt}`;
snapshotDir = path.join(historyDir, snapshotDirectory(name));
try {
// Exclusive reservation prevents timestamp collisions from overwriting data.
mkdirSync(snapshotDir);
break;
} catch (error) {
if (error.code !== 'EEXIST') throw error;
}
}
}
const gitSha = getGitCommit(projectDir);
const metadata = {
name,
createdAt: createdAt.toISOString(),
nextVersion: "16.4.0",
gitBranch: getGitBranch(projectDir),
gitSha,
gitShortSha: gitSha == null ? void 0 : gitSha.slice(0, 7),
gitDirty: getGitDirty(projectDir),
gitMessage: getGitMessage(projectDir),
appDirOnly,
noMangling,
routeCount: routes.length
};
try {
writeFileSync(path.join(dataDir, METADATA_FILENAME), JSON.stringify(metadata, null, 2));
cpSync(dataDir, snapshotDir, {
recursive: true
});
rewriteHistoryIndex(historyDir, maxHistory, name);
return metadata;
} catch (error) {
rmSync(snapshotDir, {
recursive: true,
force: true,
maxRetries: 3
});
throw error;
}
}
/** Read valid snapshot metadata, prune old captures, and rebuild the history index. */ function rewriteHistoryIndex(historyDir, maxHistory, currentName) {
const snapshots = [];
for (const entry of readdirSync(historyDir)){
if (entry === HISTORY_INDEX_FILENAME) continue;
try {
const metadata = snapshotMetadataSchema.parse(JSON.parse(readFileSync(path.join(historyDir, entry, METADATA_FILENAME), 'utf8')));
if (entry === snapshotDirectory(metadata.name)) snapshots.push(metadata);
} catch {
// Unreadable/non-snapshot directories are not part of the history index.
}
}
snapshots.sort((a, b)=>b.createdAt.localeCompare(a.createdAt) || Number(b.name === currentName) - Number(a.name === currentName));
for (const snapshot of snapshots.slice(maxHistory)){
rmSync(path.join(historyDir, snapshotDirectory(snapshot.name)), {
recursive: true,
force: true,
maxRetries: 3
});
}
writeFileSync(path.join(historyDir, HISTORY_INDEX_FILENAME), JSON.stringify({
snapshots: snapshots.slice(0, maxHistory)
}, null, 2));
}
//# sourceMappingURL=snapshot.js.map