@webcontainer/snapshot
Version:
Build filesystem snapshots for the WebContainer API
118 lines (117 loc) • 3.65 kB
JavaScript
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}'`);
}