Last active
June 24, 2024 04:28
-
-
Save okikio/f239ffaeb76c51c1b2d29672963c8d24 to your computer and use it in GitHub Desktop.
How to integrate into the esbuild wasm file system built into the package. Think esbuild-wasm-fs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Based off of https://github.com/esbuild/esbuild.github.io/blob/main/src/try/fs.ts | |
// GitHub Permalink: https://github.com/esbuild/esbuild.github.io/blob/d405aa5f06ef359430344ca7fcbbc9bc8acf620e/src/try/fs.ts | |
// This file contains a hack to get the "esbuild-wasm" package to run in the | |
// browser with file system support. Although there is no API for this, it | |
// can be made to work anyway by pretending that node's "fs" API is present. | |
import { decode, encode } from "./encode-decode"; | |
const enum Kind { | |
File, | |
Directory, | |
} | |
const enum StatsMode { | |
IFREG = 0o100000, | |
IFDIR = 0o40000, | |
} | |
type Entry = File | Directory | |
interface Metadata { | |
inode_: number | |
ctime_: Date | |
mtime_: Date | |
} | |
interface File extends Metadata { | |
kind_: Kind.File | |
content_: Uint8Array | |
} | |
interface Directory extends Metadata { | |
kind_: Kind.Directory | |
children_: Map<string, Entry> | |
} | |
class Stats { | |
declare dev: number | |
declare ino: number | |
declare mode: number | |
declare nlink: number | |
declare uid: number | |
declare gid: number | |
declare rdev: number | |
declare size: number | |
declare blksize: number | |
declare blocks: number | |
declare atimeMs: number | |
declare mtimeMs: number | |
declare ctimeMs: number | |
declare birthtimeMs: number | |
declare atime: Date | |
declare mtime: Date | |
declare ctime: Date | |
declare birthtime: Date | |
constructor(entry: Entry) { | |
const blksize = 4096 | |
const size = entry.kind_ === Kind.File ? entry.content_.length : 0 | |
const mtimeMs = entry.mtime_.getTime() | |
const ctimeMs = entry.ctime_.getTime() | |
this.dev = 1 | |
this.ino = entry.inode_ | |
this.mode = entry.kind_ === Kind.File ? StatsMode.IFREG : StatsMode.IFDIR | |
this.nlink = 1 | |
this.uid = 1 | |
this.gid = 1 | |
this.rdev = 0 | |
this.size = size | |
this.blksize = blksize | |
this.blocks = (size + (blksize - 1)) & (blksize - 1) | |
this.atimeMs = mtimeMs | |
this.mtimeMs = mtimeMs | |
this.ctimeMs = ctimeMs | |
this.birthtimeMs = ctimeMs | |
this.atime = entry.mtime_ | |
this.mtime = entry.mtime_ | |
this.ctime = entry.ctime_ | |
this.birthtime = entry.ctime_ | |
} | |
isDirectory(): boolean { | |
return this.mode === StatsMode.IFDIR | |
} | |
isFile(): boolean { | |
return this.mode === StatsMode.IFREG | |
} | |
} | |
interface Handle { | |
entry_: Entry | |
offset_: number | |
} | |
const EBADF = errorWithCode('EBADF') | |
const EINVAL = errorWithCode('EINVAL') | |
const EISDIR = errorWithCode('EISDIR') | |
const ENOENT = errorWithCode('ENOENT') | |
const ENOTDIR = errorWithCode('ENOTDIR') | |
const handles = new Map<number, Handle>() | |
export let root: Directory = createDirectory() | |
let nextFD = 3 | |
let nextInode = 1 | |
export let stderrSinceReset = '' | |
// The "esbuild-wasm" package overwrites "fs.writeSync" with this value | |
let esbuildWriteSync: ( | |
fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null | |
) => void | |
// The "esbuild-wasm" package overwrites "fs.read" with this value | |
let esbuildRead: ( | |
fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, | |
callback: (err: Error | null, count: number, buffer: Uint8Array) => void, | |
) => void | |
function writeSync( | |
fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, | |
): void { | |
if (fd <= 2) { | |
if (fd === 2) writeToStderr(buffer, offset, length) | |
else esbuildWriteSync(fd, buffer, offset, length, position) | |
} else { | |
throw EINVAL | |
} | |
} | |
function read( | |
fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, | |
callback: (err: Error | null, count: number, buffer: Uint8Array) => void, | |
): void { | |
if (fd <= 2) { | |
esbuildRead(fd, buffer, offset, length, position, callback) | |
} else { | |
const handle = handles.get(fd) | |
if (!handle) { | |
callback(EBADF, 0, buffer) | |
} else if (handle.entry_.kind_ === Kind.Directory) { | |
callback(EISDIR, 0, buffer) | |
} else { | |
const content = handle.entry_.content_ | |
if (position !== null && position !== -1) { | |
const slice = content.slice(position, position + length) | |
buffer.set(slice, offset) | |
callback(null, slice.length, buffer) | |
} else { | |
const slice = content.slice(handle.offset_, handle.offset_ + length) | |
handle.offset_ += slice.length | |
buffer.set(slice, offset) | |
callback(null, slice.length, buffer) | |
} | |
} | |
} | |
} | |
const FS = { | |
get writeSync() { return writeSync }, | |
set writeSync(value) { esbuildWriteSync = value }, | |
get read() { return read }, | |
set read(value) { esbuildRead = value }, | |
constants: { | |
O_WRONLY: -1, | |
O_RDWR: -1, | |
O_CREAT: -1, | |
O_TRUNC: -1, | |
O_APPEND: -1, | |
O_EXCL: -1, | |
}, | |
open( | |
path: string, flags: string | number, mode: string | number, | |
callback: (err: Error | null, fd: number | null) => void, | |
) { | |
try { | |
const entry = getEntryFromPath(path) | |
const fd = nextFD++ | |
handles.set(fd, { entry_: entry, offset_: 0 }) | |
callback(null, fd) | |
} catch (err) { | |
callback(err, null) | |
} | |
}, | |
close(fd: number, callback: (err: Error | null) => void) { | |
callback(handles.delete(fd) ? null : EBADF) | |
}, | |
write( | |
fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, | |
callback: (err: Error | null, count: number, buffer: Uint8Array) => void, | |
) { | |
if (fd <= 2) { | |
if (fd === 2) writeToStderr(buffer, offset, length) | |
else esbuildWriteSync(fd, buffer, offset, length, position) | |
callback(null, length, buffer) | |
} else { | |
callback(EINVAL, 0, buffer) | |
} | |
}, | |
readdir(path: string, callback: (err: Error | null, files: string[] | null) => void) { | |
try { | |
const entry = getEntryFromPath(path) | |
if (entry.kind_ !== Kind.Directory) throw ENOTDIR | |
callback(null, [...entry.children_.keys()]) | |
} catch (err) { | |
callback(err, null) | |
} | |
}, | |
stat(path: string, callback: (err: Error | null, stats: Stats | null) => void) { | |
try { | |
const entry = getEntryFromPath(path) | |
callback(null, new Stats(entry)) | |
} catch (err) { | |
callback(err, null) | |
} | |
}, | |
lstat(path: string, callback: (err: Error | null, stats: Stats | null) => void) { | |
try { | |
const entry = getEntryFromPath(path) | |
callback(null, new Stats(entry)) | |
} catch (err) { | |
callback(err, null) | |
} | |
}, | |
fstat(fd: number, callback: (err: Error | null, stats: Stats | null) => void) { | |
const handle = handles.get(fd) | |
if (handle) { | |
callback(null, new Stats(handle.entry_)) | |
} else { | |
callback(EBADF, null) | |
} | |
}, | |
} | |
globalThis.fs = FS | |
declare global { | |
var fs: typeof FS | |
} | |
function createFile(content: Uint8Array): File { | |
const now = new Date | |
return { | |
kind_: Kind.File, | |
inode_: nextInode++, | |
ctime_: now, | |
mtime_: now, | |
content_: content, | |
} | |
} | |
function createDirectory(): Directory { | |
const now = new Date | |
return { | |
kind_: Kind.Directory, | |
inode_: nextInode++, | |
ctime_: now, | |
mtime_: now, | |
children_: new Map, | |
} | |
} | |
function absoluteNormalizedPath(path: string): string { | |
if (path[0] !== '/') path = '/' + path | |
const parts = path.split('/') | |
parts.shift() | |
let end = 0 | |
for (let i = 0; i < parts.length; i++) { | |
const part = parts[i] | |
if (part === '..') { | |
if (end) end-- | |
} else if (part !== '.' && part !== '') { | |
parts[end++] = part | |
} | |
} | |
parts.length = end | |
return '/' + parts.join('/') | |
} | |
function splitPath(path: string): string[] { | |
path = absoluteNormalizedPath(path) | |
if (path === '/') return [] | |
const parts = path.split('/') | |
parts.shift() | |
return parts | |
} | |
export function getEntryFromPath(path: string): Entry { | |
const parts = splitPath(path) | |
let dir = root | |
for (let i = 0, n = parts.length; i < n; i++) { | |
const child = dir.children_.get(parts[i]) | |
if (!child) throw ENOENT | |
if (child.kind_ === Kind.File) { | |
if (i + 1 === n) return child | |
throw ENOTDIR | |
} | |
dir = child | |
} | |
return dir | |
} | |
function errorWithCode(code: string): Error { | |
const err = new Error(code) as any | |
err.code = code | |
return err | |
} | |
function writeToStderr(buffer: Uint8Array, offset: number, length: number): void { | |
stderrSinceReset += decode( | |
offset === 0 && length === buffer.length ? buffer : buffer.slice(offset, offset + length)) | |
} | |
function rejectConflict(part: string): never { | |
throw new Error(JSON.stringify(part) + ' cannot be both a file and a directory') | |
} | |
export function resetFileSystem(files: Record<string, string>): void { | |
root.children_.clear() | |
stderrSinceReset = '' | |
setFileSystem(files) | |
} | |
export function setFileSystem(files: Record<string, string>): void { | |
for (const path in files) { | |
const parts = splitPath(absoluteNormalizedPath(path)) | |
let dir = root | |
for (let i = 0; i + 1 < parts.length; i++) { | |
const part = parts[i] | |
let child = dir.children_.get(part) | |
if (!child) { | |
child = createDirectory() | |
dir.children_.set(part, child) | |
} else if (child.kind_ !== Kind.Directory) { | |
rejectConflict(part) | |
} | |
dir = child | |
} | |
const part = parts[parts.length - 1] | |
if (dir.children_.has(part)) rejectConflict(part) | |
dir.children_.set(part, createFile(encode(files[path]))) | |
} | |
} | |
export function toArr() { | |
const result: [string, File][] = []; | |
const queues = [ | |
Array.from(root.children_.entries(), ([_path, _item]) => { | |
const path = _path[0] !== '/' ? '/' + _path : _path; | |
return [path, _item] as const; | |
}) | |
]; | |
for (let i = 0; i < queues.length; i++) { | |
const queue = queues[i]; | |
const len = queue.length; | |
for (let j = 0; j < len; j++) { | |
const [path, item] = queue[j]; | |
if (item.kind_ !== Kind.Directory) { | |
result.push([path, item]) | |
} else if (item.children_.size > 0) { | |
queues.push( | |
Array.from(item.children_.entries(), ([_path, _item]) => { | |
const parentPath = path[0] !== '/' ? '/' + path : path; | |
return [parentPath + "/" + _path, _item] as const; | |
}) | |
) | |
} | |
} | |
} | |
return result | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { resetFileSystem, toArr } from "../util/filesystem"; | |
resetFileSystem({ | |
[`/input.${config.tsx ? "tsx" : "ts"}`]: `${input}`, | |
["/package.json"]: JSON.stringify(deepAssign({}, config["package.json"], { | |
// dependencies: Object.assign({}, ) | |
})) | |
}) | |
let inputFiles = []; | |
for (let [path, item] of toArr()) { | |
// This minifies & compresses input files for a accurate view of what is eating up the most size | |
// It uses the esbuild options to determine how it should minify input code | |
let text = decode(contents); | |
let code = text; | |
let text = decode(item.content_); | |
let buf = item.content_; | |
// For debugging reasons, if the user chooses verbose, print all the content to the devtools console | |
let ignoreFile = /\.(wasm|png|jpeg|webp)$/.test(path); | |
logger(`Analyze ${path}${esbuildOpts?.logLevel == "verbose" && !ignoreFile ? "\n" + text : ""}`); | |
inputFiles.push({ path, contents: buf, text }); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment