diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-09-01 14:48:45 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-09-01 14:48:45 +0300 |
| commit | 8a6d697308162874a236c84aac0b0f8c7ac01f83 (patch) | |
| tree | ed7dc7e8c6f50d66bde721fae78576aede929f66 /scripts/docs/snapshot.mjs | |
| parent | b54a78aa510b87d1e3b56c619b72eaeb59e4a2a1 (diff) | |
refactor: separate website presentation from runtime
Diffstat (limited to 'scripts/docs/snapshot.mjs')
| -rw-r--r-- | scripts/docs/snapshot.mjs | 116 |
1 files changed, 116 insertions, 0 deletions
diff --git a/scripts/docs/snapshot.mjs b/scripts/docs/snapshot.mjs new file mode 100644 index 00000000..e9eaf92c --- /dev/null +++ b/scripts/docs/snapshot.mjs @@ -0,0 +1,116 @@ +import { execFileSync } from 'node:child_process'; +import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { dirname, relative, resolve } from 'node:path'; + +export function createGit(repoRoot) { + return function git(args, options = {}) { + return execFileSync('git', args, { + cwd: repoRoot, + encoding: options.binary ? undefined : 'utf8', + maxBuffer: 64 * 1024 * 1024, + stdio: ['ignore', 'pipe', 'pipe'], + }); + }; +} + +export function resolveCommit(git, ref) { + return git(['rev-parse', `${ref}^{commit}`]).trim(); +} + +export function gitPathExists(git, ref, path) { + try { + git(['cat-file', '-e', `${ref}:${path}`]); + return true; + } catch { + return false; + } +} + +export function gitTreesEqual(git, ref, left, right) { + try { + git(['diff', '--quiet', `${ref}:${left}`, `${ref}:${right}`]); + return true; + } catch { + return false; + } +} + +export function listGitEntries(git, ref, root) { + const output = git(['ls-tree', '-r', '-z', ref, '--', root]); + return output + .split('\0') + .filter(Boolean) + .map((line) => { + const separator = line.indexOf('\t'); + const [mode, type, object] = line.slice(0, separator).split(' '); + return { mode, type, object, path: line.slice(separator + 1) }; + }); +} + +export async function extractGitTree(git, ref, sourceRoot, destinationRoot) { + const entries = listGitEntries(git, ref, sourceRoot); + if (entries.length === 0) throw new Error(`${ref}:${sourceRoot} contains no files`); + + await rm(destinationRoot, { recursive: true, force: true }); + for (const entry of entries) { + assertOrdinaryFile(entry, ref); + const relativePath = relative(sourceRoot, entry.path); + assertRelativePath(relativePath, entry.path); + const destination = resolve(destinationRoot, relativePath); + await mkdir(dirname(destination), { recursive: true }); + await writeFile(destination, git(['cat-file', 'blob', entry.object], { binary: true })); + } +} + +export async function compareGitTree(git, ref, sourceRoot, snapshotRoot) { + const expectedEntries = listGitEntries(git, ref, sourceRoot); + if (expectedEntries.length === 0) throw new Error(`${ref}:${sourceRoot} contains no files`); + + const expected = new Map(); + for (const entry of expectedEntries) { + assertOrdinaryFile(entry, ref); + const relativePath = relative(sourceRoot, entry.path); + assertRelativePath(relativePath, entry.path); + expected.set(relativePath, entry); + } + const actualPaths = await listDocumentationPaths(snapshotRoot); + const expectedPaths = [...expected.keys()].sort(); + if (JSON.stringify(actualPaths) !== JSON.stringify(expectedPaths)) { + throw new Error(`${snapshotRoot} file list differs from ${ref}:${sourceRoot}`); + } + + for (const relativePath of expectedPaths) { + const entry = expected.get(relativePath); + const snapshotPath = resolve(snapshotRoot, relativePath); + const expectedContent = git(['cat-file', 'blob', entry.object], { binary: true }); + if (!(await readFile(snapshotPath)).equals(expectedContent)) { + throw new Error(`${relativePath} differs from ${ref}:${entry.path}`); + } + } +} + +export async function listDocumentationPaths(root) { + const paths = []; + async function walk(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = resolve(directory, entry.name); + if (entry.isDirectory()) await walk(path); + else if (entry.isFile()) paths.push(relative(root, path)); + else throw new Error(`${path} is not an ordinary documentation file`); + } + } + await walk(root); + return paths.sort(); +} + +function assertOrdinaryFile(entry, ref) { + if (entry.type !== 'blob' || entry.mode !== '100644') { + throw new Error(`${ref}:${entry.path} is not an ordinary documentation file`); + } +} + +function assertRelativePath(relativePath, originalPath) { + if (!relativePath || relativePath === '..' || relativePath.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) { + throw new Error(`unsafe snapshot path ${originalPath}`); + } +} |
