import { constants } from 'node:fs'; import { lstat, mkdir, open, readFile, readdir } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const scriptPath = fileURLToPath(import.meta.url); const defaultRepositoryRoot = resolve(dirname(scriptPath), '..'); const isErrno = (error, code) => typeof error === 'object' && error !== null && 'code' in error && error.code === code; const directoryIdentity = (stats) => ({ dev: stats.dev, ino: stats.ino }); const hasSameDirectoryIdentity = (left, right) => left.dev === right.dev && left.ino === right.ino; const assertNonSymlinkDirectory = (stats) => { if (stats.isSymbolicLink() || !stats.isDirectory()) { throw new Error('Prepared asset path must be a non-symlink directory.'); } }; export const assertPreparedAssetDirectoryIdentity = async (preparedAssetsPath, expectedIdentity) => { const current = await lstat(preparedAssetsPath); assertNonSymlinkDirectory(current); if (!hasSameDirectoryIdentity(directoryIdentity(current), expectedIdentity)) { throw new Error('Prepared asset directory identity changed during validation.'); } }; const assertOpenedDirectoryIdentity = async (preparedAssetsPath, expectedIdentity) => { const noFollowDirectoryFlags = constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0); let handle; try { handle = await open(preparedAssetsPath, noFollowDirectoryFlags); } catch (error) { if (isErrno(error, 'ELOOP')) { throw new Error('Prepared asset path must be a non-symlink directory.'); } if (isErrno(error, 'EINVAL') || isErrno(error, 'ENOTSUP') || isErrno(error, 'ENOSYS')) return; throw error; } try { const opened = await handle.stat(); assertNonSymlinkDirectory(opened); if (!hasSameDirectoryIdentity(directoryIdentity(opened), expectedIdentity)) { throw new Error('Prepared asset directory identity changed during validation.'); } } finally { await handle.close(); } }; const readEmptyManifest = async (repositoryRoot) => { const manifestPath = resolve(repositoryRoot, 'content/public/asset-manifest.json'); const manifest = JSON.parse(await readFile(manifestPath, 'utf8')); if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest) || Object.keys(manifest).length !== 1 || !Array.isArray(manifest.assets) || manifest.assets.length !== 0) { throw new Error('Phase 1 asset manifest must contain exactly zero assets.'); } }; const ensureEmptyPreparedAssetDirectory = async (repositoryRoot) => { const preparedAssetsPath = resolve(repositoryRoot, 'inputs/prepared-assets'); let initialStats; try { initialStats = await lstat(preparedAssetsPath); } catch (error) { if (!isErrno(error, 'ENOENT')) throw error; await mkdir(preparedAssetsPath, { recursive: true }); initialStats = await lstat(preparedAssetsPath); } assertNonSymlinkDirectory(initialStats); const initialIdentity = directoryIdentity(initialStats); await assertOpenedDirectoryIdentity(preparedAssetsPath, initialIdentity); await assertPreparedAssetDirectoryIdentity(preparedAssetsPath, initialIdentity); const members = await readdir(preparedAssetsPath); await assertPreparedAssetDirectoryIdentity(preparedAssetsPath, initialIdentity); if (members.length !== 0) { throw new Error('Prepared asset directory must be empty during Phase 1.'); } }; export const materializeEmptyAssetSet = async (repositoryRoot) => { const resolvedRepositoryRoot = resolve(repositoryRoot); await readEmptyManifest(resolvedRepositoryRoot); await ensureEmptyPreparedAssetDirectory(resolvedRepositoryRoot); }; if (process.argv[1] !== undefined && resolve(process.argv[1]) === scriptPath) { await materializeEmptyAssetSet(defaultRepositoryRoot); }