UNPKG

@webcontainer/snapshot

Version:

Build filesystem snapshots for the WebContainer API

118 lines (117 loc) 3.65 kB
import fs from 'node:fs/promises'; import { Buffer } from 'node:buffer'; import { join as joinPath } from 'node:path'; import { encode } from '@msgpack/msgpack'; /** * Traverses the folder at `root` and builds a binary snapshot. The returned buffer is suitable to * be loaded by `WebContainer#mount()`. */ export async function snapshot(root) { const controller = new AbortController(); try { return await uncontrolledSnapshot(root, controller.signal); } catch (error) { controller.abort(); throw error; } } async function uncontrolledSnapshot(root, signal) { const nextId = (() => { let counter = 0; return () => counter++; })(); const index = { d: {} }; const tasks = new Map(); { const id = nextId(); const task = readdir(root, { id, folder: index.d, path: root }); tasks.set(id, task); } while (tasks.size > 0) { const result = await Promise.race(tasks.values()); tasks.delete(result.id); switch (result.type) { case 'readdir': { for (const name of result.entries) { const id = nextId(); const path = joinPath(result.path, name); const task = stat(path, { id, path, folder: result.folder, name, }); tasks.set(id, task); } break; } case 'stat': { throwIfUnsupported(result.stats, result.path); if (result.stats.isDirectory()) { const dir = {}; result.folder[result.name] = { d: dir }; const id = nextId(); const task = readdir(result.path, { id, path: result.path, folder: dir, }); tasks.set(id, task); break; } if (result.stats.isFile()) { const file = {}; result.folder[result.name] = { f: file }; const id = nextId(); const task = fs.readFile(result.path, { signal }).then((contents) => { file.c = contents; return { id }; }); tasks.set(id, task); break; } } } } const encoded = encode(index); return Buffer.from(encoded.buffer, encoded.byteOffset, encoded.byteLength); } async function readdir(path, context) { const entries = await fs.readdir(path); return { entries, ...context, type: 'readdir', }; } async function stat(path, context) { const stats = await fs.lstat(path); return { stats, ...context, type: 'stat', }; } function throwIfUnsupported(stat, path) { if (stat.isFile() || stat.isDirectory()) { return; } let unsupported = 'unknown'; if (stat.isSymbolicLink()) { unsupported = 'symbolic link'; } else if (stat.isSocket()) { unsupported = 'symbolic link'; } else if (stat.isFIFO()) { unsupported = 'FIFO'; } else if (stat.isBlockDevice()) { unsupported = 'block device'; } else if (stat.isCharacterDevice()) { unsupported = 'socket'; } throw new Error(`Cannot serialize unsupported file type at '${path}': '${unsupported}'`); }