import type { PathWatcherEvent, WebContainer } from '@webcontainer/api'; import { map } from 'nanostores'; import { bufferWatchEvents } from '../../utils/buffer'; import { WORK_DIR } from '../../utils/constants'; const textDecoder = new TextDecoder('utf8', { fatal: true }); interface File { type: 'file'; content: string; } interface Folder { type: 'folder'; } type Dirent = File | Folder; export type FileMap = Record; export class FilesStore { #webcontainer: Promise; files = map({}); constructor(webcontainerPromise: Promise) { this.#webcontainer = webcontainerPromise; this.#init(); } async #init() { const webcontainer = await this.#webcontainer; webcontainer.watchPaths( { include: [`${WORK_DIR}/**`], exclude: ['**/node_modules', '.git'], includeContent: true }, bufferWatchEvents(100, this.#processEventBuffer.bind(this)), ); } #processEventBuffer(events: Array<[events: PathWatcherEvent[]]>) { const watchEvents = events.flat(2); for (const { type, path, buffer } of watchEvents) { // remove any trailing slashes const sanitizedPath = path.replace(/\/+$/g, ''); switch (type) { case 'add_dir': { // we intentionally add a trailing slash so we can distinguish files from folders in the file tree this.files.setKey(sanitizedPath, { type: 'folder' }); break; } case 'remove_dir': { this.files.setKey(sanitizedPath, undefined); for (const [direntPath] of Object.entries(this.files)) { if (direntPath.startsWith(sanitizedPath)) { this.files.setKey(direntPath, undefined); } } break; } case 'add_file': case 'change': { this.files.setKey(sanitizedPath, { type: 'file', content: this.#decodeFileContent(buffer) }); break; } case 'remove_file': { this.files.setKey(sanitizedPath, undefined); break; } case 'update_directory': { // we don't care about these events break; } } } } #decodeFileContent(buffer?: Uint8Array) { if (!buffer || buffer.byteLength === 0) { return ''; } try { return textDecoder.decode(buffer); } catch (error) { console.log(error); return ''; } } }