From 7de9ab1847568be7377e53131a77d1afedc9bacf Mon Sep 17 00:00:00 2001 From: Sepehr Laal <5657848+3p3r@users.noreply.github.com> Date: Sun, 5 May 2024 12:51:35 -0700 Subject: [PATCH] feat: fs.watch family of apis and fs mirror utility (#1) * feat: support for fs.watch family of apis * feat: mirror utility api to handle testing and inspection * improve: tests for the mirror utility * improve: mirror utility input and output * chore: version bump * chore: removed deprecated plugin in favor of browserify-webpack-plugin * chore: formatting * feat: split builds for better native support added a native build to pull Stream and Buffer environments from the environment to better support Node. --- .vscode/launch.json | 14 ++ README.md | 10 -- karma.conf.ts | 1 + package-lock.json | 22 ++- package.json | 7 +- src/fs.rs | 207 ++++++++++++--------- src/fs/lfs.rs | 10 +- src/index.ts | 302 +++++++++++++++++++++++++++++++ src/plugin.ts | 57 ------ test/filesystem/watch.test.ts | 46 +++++ test/path.test.ts | 29 +++ test/test.mirror.ts | 94 ++++++++++ webpack.config.ts | 326 +++++++++++++++++----------------- 13 files changed, 796 insertions(+), 329 deletions(-) create mode 100644 .vscode/launch.json delete mode 100644 src/plugin.ts create mode 100644 test/filesystem/watch.test.ts create mode 100644 test/path.test.ts create mode 100644 test/test.mirror.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..bd686f2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "tests", + "request": "launch", + "runtimeArgs": ["run-script", "test"], + "runtimeExecutable": "npm", + "skipFiles": ["/**"], + "outFiles": ["${workspaceFolder}/dist/*.(m|c|)js", "!**/node_modules/**"], + "type": "node" + } + ] +} diff --git a/README.md b/README.md index cac4b47..1f82341 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ WebAssembly and SharedArrayBuffer IO. Pronounced "wassabee-yo". - [Initialization](#initialization) - [From New Memory](#from-new-memory) - [From Existing Memory](#from-existing-memory) - - [Webpack Plugin](#webpack-plugin) ## Purpose @@ -114,12 +113,3 @@ addEventListener("message", async ({ data }) => { In this case, `reboot` signifies that the library is being initialized from cold storage and thread-local state should be reset. - -### Webpack Plugin - -A Webpack plugin is provided to allow for seamless integration of `wasabio` into -any project. The plugin currently supports generating a WASM memory that can be -used to boot the library on the main thread with. - -If you are not interested in using the Webpack plugin, you may opt out of its -dependencies being installed by using `npm install wasabio --omit=optional`. diff --git a/karma.conf.ts b/karma.conf.ts index f55f09f..e4ce305 100644 --- a/karma.conf.ts +++ b/karma.conf.ts @@ -37,6 +37,7 @@ module.exports = function (config: karma.Config) { timeout: mochaConfig.timeout, }, }, + concurrency: 1, autoWatch: false, singleRun: true, logLevel: config.LOG_WARN, diff --git a/package-lock.json b/package-lock.json index ffd8906..f71a839 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { "name": "wasabio", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wasabio", - "version": "1.2.0", + "version": "1.3.0", "hasInstallScript": true, "license": "MIT", "devDependencies": { "@types/atob-lite": "^2.0.2", "@types/chai": "^4.3.14", "@types/karma": "^6.3.8", + "@types/lodash": "^4.17.0", "@types/mocha": "^10.0.6", "@types/node": "^20.11.30", "@types/typedarray-to-buffer": "^4.0.4", @@ -32,6 +33,7 @@ "karma-jasmine": "^5.1.0", "karma-mocha": "^2.0.1", "karma-webpack": "^5.0.1", + "lodash": "^4.17.21", "memfs": "^4.8.1", "mocha": "^10.3.0", "patch-package": "^8.0.0", @@ -53,7 +55,6 @@ "web-worker": "^1.3.0", "webpack": "^5.91.0", "webpack-cli": "^5.1.4", - "webpack-node-externals": "^3.0.0", "webpack-shell-plugin-next": "^2.3.1", "worker-loader": "^3.0.8" }, @@ -1159,6 +1160,12 @@ "log4js": "^6.4.1" } }, + "node_modules/@types/lodash": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", + "dev": true + }, "node_modules/@types/mocha": { "version": "10.0.6", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", @@ -7517,15 +7524,6 @@ "node": ">=10.0.0" } }, - "node_modules/webpack-node-externals": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", - "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/webpack-shell-plugin-next": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/webpack-shell-plugin-next/-/webpack-shell-plugin-next-2.3.1.tgz", diff --git a/package.json b/package.json index aef746f..c11d895 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wasabio", - "version": "1.2.0", + "version": "1.3.0", "description": "WebAssembly and SharedArrayBuffer IO. Pronounced 'wassabee-yo'.", "repository": { "type": "git", @@ -9,7 +9,7 @@ "main": "index.js", "scripts": { "postinstall": "patch-package", - "test": "ts-mocha && karma start --single-run", + "test": "ts-mocha && karma start --single-run && NODE_OPTIONS='--import tsx' node test/test.mirror.ts", "lint": "prettier --check .", "cosmo": "prettier --write .", "build": "NODE_OPTIONS='--import tsx' webpack --mode=production", @@ -36,6 +36,7 @@ "@types/atob-lite": "^2.0.2", "@types/chai": "^4.3.14", "@types/karma": "^6.3.8", + "@types/lodash": "^4.17.0", "@types/mocha": "^10.0.6", "@types/node": "^20.11.30", "@types/typedarray-to-buffer": "^4.0.4", @@ -55,6 +56,7 @@ "karma-jasmine": "^5.1.0", "karma-mocha": "^2.0.1", "karma-webpack": "^5.0.1", + "lodash": "^4.17.21", "memfs": "^4.8.1", "mocha": "^10.3.0", "patch-package": "^8.0.0", @@ -76,7 +78,6 @@ "web-worker": "^1.3.0", "webpack": "^5.91.0", "webpack-cli": "^5.1.4", - "webpack-node-externals": "^3.0.0", "webpack-shell-plugin-next": "^2.3.1", "worker-loader": "^3.0.8" }, diff --git a/src/fs.rs b/src/fs.rs index daf7397..4a44415 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -31,10 +31,9 @@ pub unsafe fn sab_fs_locked() -> bool { struct ChangeType {} impl ChangeType { - const CREATE: &'static str = "create"; const CHANGE: &'static str = "change"; const RENAME: &'static str = "rename"; - const DELETE: &'static str = "delete"; + const WATCH_: &'static str = "watch_"; } fn parse_filesystem_mode(mode: String) -> i32 { @@ -82,9 +81,9 @@ macro_rules! debug_log { }; } -macro_rules! broadcast { +macro_rules! broadcast_defer { ($name:expr, $($arg:expr),*) => { - debug_log!($name, $($arg),*); + debug_log!($name, $($arg),*); let ev = json!([ $($arg),* ]); defr!(unsafe { EMITTER @@ -94,19 +93,36 @@ macro_rules! broadcast { }; } +macro_rules! broadcast_watch { + ($path:expr) => { + let prevStat = lfs::stat_sync($path.as_str()); + defr!(unsafe { + let currStat = lfs::stat_sync($path.as_str()); + let ev = json!([$path.to_string(), prevStat, currStat]); + EMITTER + .emit(ChangeType::WATCH_.to_string(), ev.to_string()) + .unwrap_or(()); + }); + }; +} + #[wasm_bindgen] pub fn linkSync(existing: String, path: String) -> Result { - broadcast!(name_of!(linkSync), existing, path); - broadcast!(ChangeType::CREATE, path); - broadcast!(ChangeType::CHANGE, existing); + broadcast_watch!(path); + broadcast_watch!(existing); + broadcast_defer!(ChangeType::RENAME, path); + broadcast_defer!(ChangeType::CHANGE, existing); + broadcast_defer!(name_of!(linkSync), existing, path); lfs::link_sync(existing.as_str(), path.as_str()) } #[wasm_bindgen] pub fn symlinkSync(target: String, path: String) -> Result { - broadcast!(name_of!(symlinkSync), target, path); - broadcast!(ChangeType::CREATE, path); - broadcast!(ChangeType::CHANGE, target); + broadcast_watch!(path); + broadcast_watch!(target); + broadcast_defer!(ChangeType::RENAME, path); + broadcast_defer!(ChangeType::CHANGE, target); + broadcast_defer!(name_of!(symlinkSync), target, path); lfs::symlink_sync(target.as_str(), path.as_str()) } @@ -168,7 +184,9 @@ pub unsafe fn openSync( } else { None }; - broadcast!(name_of!(openSync), path, flags, mode); + let pathClone = path.clone(); + broadcast_watch!(pathClone); + broadcast_defer!(name_of!(openSync), path, flags, mode); if let Some(fd) = lfs::open_sync(path.as_str(), flags.as_ref().map(|s| s.as_str()), mode) { return Ok(fd); } else { @@ -182,7 +200,7 @@ pub unsafe fn openSync( #[wasm_bindgen] pub unsafe fn opendirSync(path: String) -> Result { - broadcast!(name_of!(opendirSync), path); + broadcast_defer!(name_of!(opendirSync), path); openSync(path, None, None) } @@ -192,13 +210,13 @@ pub unsafe fn openfileSync( flags: Option, mode: Option, ) -> Result { - broadcast!(name_of!(openfileSync), path); + broadcast_defer!(name_of!(openfileSync), path); openSync(path, flags, mode) } #[wasm_bindgen] pub unsafe fn closeSync(fd: usize) -> Result<(), JsValue> { - broadcast!(name_of!(closeSync), fd); + broadcast_defer!(name_of!(closeSync), fd); if lfs::close_sync(fd).is_some() { Ok(()) } else { @@ -211,7 +229,7 @@ pub unsafe fn closeSync(fd: usize) -> Result<(), JsValue> { #[wasm_bindgen] pub unsafe fn lseekSync(fd: usize, offset: i32, whence: i32) -> i32 { - broadcast!(name_of!(lseekSync), fd, offset, whence); + broadcast_defer!(name_of!(lseekSync), fd, offset, whence); lfs::lseek_sync(fd, offset, whence).unwrap_or(-1) } @@ -223,7 +241,7 @@ pub unsafe fn readSync( length: Option, position: Option, ) -> usize { - broadcast!(name_of!(readSync), fd, offset, length, position); + broadcast_defer!(name_of!(readSync), fd, offset, length, position); lfs::read_sync(fd, buffer, offset, length, position).unwrap_or(0) } @@ -235,14 +253,15 @@ pub unsafe fn writeSync( length: Option, position: Option, ) -> usize { - broadcast!(name_of!(writeSync), fd, offset, length, position); - broadcast!(ChangeType::CHANGE, path_from_fd(fd)); + broadcast_watch!(path_from_fd(fd)); + broadcast_defer!(name_of!(writeSync), fd, offset, length, position); + broadcast_defer!(ChangeType::CHANGE, path_from_fd(fd)); lfs::write_sync(fd, buffer, offset, length, position).unwrap_or(0) } #[wasm_bindgen] pub unsafe fn fstatSync(fd: usize) -> NodeStats { - broadcast!(name_of!(fstatSync), fd); + broadcast_defer!(name_of!(fstatSync), fd); let stat = lfs::fstat(fd).unwrap(); NodeStats { dev: stat.dev, @@ -269,48 +288,55 @@ pub unsafe fn fchmodSync(fd: usize, mode: UnionStringNumber) { } else { mode.as_f64().unwrap() as i32 }; - broadcast!(name_of!(fchmodSync), fd, mode); - broadcast!(ChangeType::CHANGE, path_from_fd(fd)); + broadcast_watch!(path_from_fd(fd)); + broadcast_defer!(name_of!(fchmodSync), fd, mode); + broadcast_defer!(ChangeType::CHANGE, path_from_fd(fd)); lfs::fchmod(fd, mode).unwrap(); } #[wasm_bindgen] pub unsafe fn fchownSync(fd: usize, uid: usize, gid: usize) { - broadcast!(name_of!(fchownSync), fd, uid, gid); - broadcast!(ChangeType::CHANGE, path_from_fd(fd)); + broadcast_watch!(path_from_fd(fd)); + broadcast_defer!(name_of!(fchownSync), fd, uid, gid); + broadcast_defer!(ChangeType::CHANGE, path_from_fd(fd)); lfs::fchown(fd, uid as i32, gid as i32).unwrap(); } #[wasm_bindgen] pub unsafe fn ftruncateSync(fd: usize, len: Option) { let len = len.unwrap_or(0); - broadcast!(name_of!(ftruncateSync), fd, len); - broadcast!(ChangeType::CHANGE, path_from_fd(fd)); + broadcast_watch!(path_from_fd(fd)); + broadcast_defer!(name_of!(ftruncateSync), fd, len); + broadcast_defer!(ChangeType::CHANGE, path_from_fd(fd)); lfs::ftruncate(fd, len).unwrap(); } #[wasm_bindgen] pub unsafe fn futimesSync(fd: usize, atime: f64, mtime: f64) { - broadcast!(name_of!(futimesSync), fd, atime, mtime); - broadcast!(ChangeType::CHANGE, path_from_fd(fd)); + broadcast_watch!(path_from_fd(fd)); + broadcast_defer!(name_of!(futimesSync), fd, atime, mtime); + broadcast_defer!(ChangeType::CHANGE, path_from_fd(fd)); lfs::futimes(fd, atime, mtime).unwrap(); } #[wasm_bindgen] pub unsafe fn fsyncSync(fd: usize) { - broadcast!(name_of!(fsyncSync), fd); + broadcast_watch!(path_from_fd(fd)); + broadcast_defer!(name_of!(fsyncSync), fd); lfs::fsync(fd).unwrap(); } #[wasm_bindgen] pub unsafe fn fdatasyncSync(fd: usize) { - broadcast!(name_of!(fdatasyncSync), fd); + broadcast_watch!(path_from_fd(fd)); + broadcast_defer!(name_of!(fdatasyncSync), fd); lfs::fdatasync(fd).unwrap() } #[wasm_bindgen] pub unsafe fn existsSync(path: String) -> bool { - broadcast!(name_of!(existsSync), path); + broadcast_watch!(path); + broadcast_defer!(name_of!(existsSync), path); lfs::exists_sync(path.as_str()) } @@ -359,7 +385,8 @@ impl Dirent { #[wasm_bindgen] pub unsafe fn freaddirSync(fd: usize) -> Option { - broadcast!(name_of!(freaddirSync), fd); + broadcast_watch!(path_from_fd(fd)); + broadcast_defer!(name_of!(freaddirSync), fd); let ent = lfs::freaddir_sync(fd)?; Some( (Dirent { @@ -377,6 +404,8 @@ pub unsafe fn readdirSync( path: String, options: Option, ) -> Result { + let pathClone = path.clone(); + broadcast_watch!(pathClone); if !existsSync(path.clone()) { let err: JsValue = JsError::new("ENOENT: no such file or directory").into(); Reflect::set(&err, &"path".into(), &path.into()).unwrap(); @@ -398,7 +427,7 @@ pub unsafe fn readdirSync( .unwrap_or(false); let arr = js_sys::Array::new(); if with_file_types { - broadcast!(name_of!(readdirSync), path, with_file_types); + broadcast_defer!(name_of!(readdirSync), path, with_file_types); for dirent in lfs::readdir_sync(path.as_str()) { arr.push( &(Dirent { @@ -411,7 +440,7 @@ pub unsafe fn readdirSync( ); } } else { - broadcast!(name_of!(readdirSync), path); + broadcast_defer!(name_of!(readdirSync), path); for dirent in lfs::readdir_sync(path.as_str()) { arr.push(&dirent.name.into()); } @@ -433,19 +462,20 @@ pub unsafe fn mkdirSync( .unwrap_or_default() .as_f64() .unwrap_or(lfs::DEFAULT_PERM_DIR as f64) as i32; - broadcast!(name_of!(mkdirSync), path, recursive, mode); - broadcast!(ChangeType::CHANGE, path); // recursive? - broadcast!(ChangeType::CREATE, path); + broadcast_watch!(path); + broadcast_defer!(ChangeType::RENAME, path); + broadcast_defer!(name_of!(mkdirSync), path, recursive, mode); lfs::mkdir_sync(path.as_str(), recursive, mode) } #[wasm_bindgen] pub unsafe fn mkdtempSync(prefix: String) -> String { let ret = lfs::mkdtemp_sync(prefix.as_str()); + let retClone = ret.clone(); // note: this is the only function that broadcasts the result - broadcast!(name_of!(mkdtempSync), prefix, ret); - broadcast!(ChangeType::CHANGE, ret); - broadcast!(ChangeType::CREATE, ret); + broadcast_watch!(retClone); + broadcast_defer!(ChangeType::RENAME, ret); + broadcast_defer!(name_of!(mkdtempSync), prefix, ret); ret } @@ -470,8 +500,9 @@ pub unsafe fn writeFileSync( .as_string() .unwrap_or("w".to_string()) .to_lowercase(); - broadcast!(name_of!(writeFileSync), path); - broadcast!(ChangeType::CHANGE, path); + broadcast_watch!(path); + broadcast_defer!(ChangeType::CHANGE, path); + broadcast_defer!(name_of!(writeFileSync), path); if encoding != "utf8" && encoding != "utf-8" && encoding != "buffer" { return Err(JsError::new("unsupported encoding")); } @@ -514,7 +545,9 @@ pub unsafe fn readFileSync( Reflect::set(&err, &"syscall".into(), &"read".into()).unwrap(); return Err(err); } - broadcast!(name_of!(readFileSync), path); + let pathClone = path.clone(); + broadcast_watch!(pathClone); + broadcast_defer!(name_of!(readFileSync), path); let data = lfs::read_file_sync(path.as_str()).unwrap(); let out = match encoding.as_str() { "utf8" | "utf-8" => { @@ -551,8 +584,9 @@ pub unsafe fn appendFileSync( .as_string() .unwrap_or("a".to_string()) .to_lowercase(); - broadcast!(name_of!(appendFileSync), path); - broadcast!(ChangeType::CHANGE, path); + broadcast_watch!(path); + broadcast_defer!(ChangeType::CHANGE, path); + broadcast_defer!(name_of!(appendFileSync), path); if encoding != "utf8" && encoding != "utf-8" && encoding != "buffer" { return Err(JsError::new("unsupported encoding")); } @@ -599,7 +633,7 @@ impl StatFs { #[wasm_bindgen] pub unsafe fn statfsSync(path: String, dump: Option) -> StatFs { - broadcast!(name_of!(statfsSync), path, dump); + broadcast_defer!(name_of!(statfsSync), path, dump); let stat = lfs::statfs_sync(path.as_str(), dump); StatFs { bsize: stat.bsize, @@ -620,49 +654,52 @@ pub unsafe fn chmodSync(path: String, mode: UnionStringNumber) { } else { mode.as_f64().unwrap() as i32 }; - broadcast!(name_of!(chmodSync), path, mode); - broadcast!(ChangeType::CHANGE, path); + broadcast_watch!(path); + broadcast_defer!(ChangeType::CHANGE, path); + broadcast_defer!(name_of!(chmodSync), path, mode); lfs::chmod_sync(path.as_str(), mode).unwrap(); } #[wasm_bindgen] pub unsafe fn chownSync(path: String, uid: usize, gid: usize) { - broadcast!(name_of!(chownSync), path, uid, gid); - broadcast!(ChangeType::CHANGE, path); + broadcast_watch!(path); + broadcast_defer!(ChangeType::CHANGE, path); + broadcast_defer!(name_of!(chownSync), path, uid, gid); lfs::chown_sync(path.as_str(), uid as i32, gid as i32).unwrap(); } #[wasm_bindgen] pub unsafe fn truncateSync(path: String, len: Option) { let len = len.unwrap_or(0); - broadcast!(name_of!(truncateSync), path, len); - broadcast!(ChangeType::CHANGE, path); + broadcast_watch!(path); + broadcast_defer!(ChangeType::CHANGE, path); + broadcast_defer!(name_of!(truncateSync), path, len); lfs::truncate_sync(path.as_str(), len).unwrap(); } #[wasm_bindgen] pub unsafe fn utimesSync(path: String, atime: f64, mtime: f64) { - broadcast!(name_of!(utimesSync), path, atime, mtime); - broadcast!(ChangeType::CHANGE, path); + broadcast_watch!(path); + broadcast_defer!(ChangeType::CHANGE, path); + broadcast_defer!(name_of!(utimesSync), path, atime, mtime); lfs::utimes_sync(path.as_str(), atime, mtime).unwrap(); } #[wasm_bindgen] pub unsafe fn unlinkSync(path: String) -> Result { - broadcast!(name_of!(unlinkSync), path); - broadcast!(ChangeType::CHANGE, path); - broadcast!(ChangeType::DELETE, path); + broadcast_watch!(path); + broadcast_defer!(ChangeType::RENAME, path); + broadcast_defer!(name_of!(unlinkSync), path); lfs::unlink_sync(path.as_str(), Some(true)) } #[wasm_bindgen] pub unsafe fn renameSync(old_path: String, new_path: String) { - broadcast!(name_of!(renameSync), old_path, new_path); - broadcast!(ChangeType::RENAME, old_path, new_path); - broadcast!(ChangeType::CHANGE, old_path); - broadcast!(ChangeType::DELETE, old_path); - broadcast!(ChangeType::CHANGE, new_path); - broadcast!(ChangeType::CREATE, new_path); + broadcast_watch!(old_path); + broadcast_watch!(new_path); + broadcast_defer!(ChangeType::RENAME, old_path); + broadcast_defer!(ChangeType::RENAME, new_path); + broadcast_defer!(name_of!(renameSync), old_path, new_path); lfs::rename_sync(old_path.as_str(), new_path.as_str()).unwrap(); } @@ -679,17 +716,19 @@ pub unsafe fn copyFileSync( .as_f64() .unwrap_or(0.0) as i32; let excl = mode | COPYFILE_EXCL != 0; - broadcast!(name_of!(copyFileSync), src, dest); - broadcast!(ChangeType::CHANGE, dest); - broadcast!(ChangeType::CREATE, dest); + broadcast_watch!(src); + broadcast_watch!(dest); + broadcast_defer!(ChangeType::CHANGE, src); + broadcast_defer!(ChangeType::RENAME, dest); + broadcast_defer!(name_of!(copyFileSync), src, dest); lfs::copy_file_sync(src.as_str(), dest.as_str(), excl) } #[wasm_bindgen] pub unsafe fn rmdirSync(path: String) { - broadcast!(name_of!(rmdirSync), path); - broadcast!(ChangeType::CHANGE, path); // recursive? - broadcast!(ChangeType::DELETE, path); + broadcast_watch!(path); + broadcast_defer!(ChangeType::RENAME, path); + broadcast_defer!(name_of!(rmdirSync), path); lfs::rmdir_sync(path.as_str(), Some(false)).unwrap(); } @@ -704,27 +743,28 @@ pub unsafe fn rmSync(path: String, options: Option) { .unwrap_or(JsValue::UNDEFINED) .as_bool() .unwrap_or(false); - broadcast!(name_of!(rmSync), path, recursive, force); - broadcast!(ChangeType::CHANGE, path); // recursive? - broadcast!(ChangeType::DELETE, path); + broadcast_watch!(path); + broadcast_defer!(ChangeType::RENAME, path); + broadcast_defer!(name_of!(rmSync), path, recursive, force); lfs::rm_sync(path.as_str(), recursive, force).unwrap(); } #[wasm_bindgen] pub unsafe fn accessSync(path: String, mode: Option) -> Result { - broadcast!(name_of!(accessSync), path, mode); + // broadcast_watch!(path); // deadlocks? + broadcast_defer!(name_of!(accessSync), path, mode); lfs::access_sync(path.as_str(), mode) } #[wasm_bindgen] pub unsafe fn realpathSync(path: String) -> String { - broadcast!(name_of!(realpathSync), path); + broadcast_defer!(name_of!(realpathSync), path); lfs::realpath_sync(path.as_str(), None) } #[wasm_bindgen] pub unsafe fn readlinkSync(path: String) -> Result { - broadcast!(name_of!(readlinkSync), path); + broadcast_defer!(name_of!(readlinkSync), path); lfs::readlink_sync(path.as_str()) } @@ -734,7 +774,7 @@ pub unsafe fn statSync( options: Option, ) -> Result { let options = options.unwrap_or(UnionObjectUndefined::from(JsValue::undefined())); - broadcast!(name_of!(statSync), path); + broadcast_defer!(name_of!(statSync), path); let throw_if_no_entry = Reflect::get(&options, &"throwIfNoEntry".into()) .unwrap_or(JsValue::UNDEFINED) .as_bool() @@ -778,22 +818,25 @@ pub unsafe fn lchmodSync(path: String, mode: UnionStringNumber) { } else { mode.as_f64().unwrap() as i32 }; - broadcast!(name_of!(lchmodSync), path, mode); - broadcast!(ChangeType::CHANGE, path); + broadcast_watch!(path); + broadcast_defer!(ChangeType::CHANGE, path); + broadcast_defer!(name_of!(lchmodSync), path, mode); lfs::lchmod_sync(path.as_str(), mode).unwrap(); } #[wasm_bindgen] pub unsafe fn lchownSync(path: String, uid: usize, gid: usize) { - broadcast!(name_of!(lchownSync), path, uid, gid); - broadcast!(ChangeType::CHANGE, path); + broadcast_watch!(path); + broadcast_defer!(ChangeType::CHANGE, path); + broadcast_defer!(name_of!(lchownSync), path, uid, gid); lfs::lchown_sync(path.as_str(), uid as i32, gid as i32).unwrap(); } #[wasm_bindgen] pub unsafe fn lutimesSync(path: String, atime: f64, mtime: f64) { - broadcast!(name_of!(lutimesSync), path, atime, mtime); - broadcast!(ChangeType::CHANGE, path); + broadcast_watch!(path); + broadcast_defer!(ChangeType::CHANGE, path); + broadcast_defer!(name_of!(lutimesSync), path, atime, mtime); lfs::lutimes_sync(path.as_str(), atime, mtime).unwrap(); } @@ -803,7 +846,7 @@ pub unsafe fn lstatSync( options: Option, ) -> Result { let options = options.unwrap_or(UnionObjectUndefined::from(JsValue::undefined())); - broadcast!(name_of!(lstatSync), path); + broadcast_defer!(name_of!(lstatSync), path); let throw_if_no_entry = Reflect::get(&options, &"throwIfNoEntry".into()) .unwrap_or(JsValue::UNDEFINED) .as_bool() diff --git a/src/fs/lfs.rs b/src/fs/lfs.rs index 7eb438e..26ed66a 100644 --- a/src/fs/lfs.rs +++ b/src/fs/lfs.rs @@ -234,7 +234,7 @@ impl Drop for InfoHandle { } } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct NodeStats { pub dev: f64, pub ino: f64, @@ -1104,7 +1104,11 @@ pub fn mkdtemp_sync(prefix: &str) -> String { pub fn readdir_sync(path: &str) -> Vec { let mut res = vec![]; - let mut handle = DirHandle::open(path).unwrap(); + let handle = DirHandle::open(path); + if handle.is_none() { + return res; + } + let mut handle = handle.unwrap(); while let Some(dirent) = handle.read() { res.push(dirent); } @@ -1623,7 +1627,7 @@ fn path_normalize(path: &str) -> String { normalized_path } -fn path_split(path: &str) -> Vec { +pub fn path_split(path: &str) -> Vec { let mut paths = Vec::new(); let components: Vec<&str> = path.split('/').collect(); let mut current_path = String::new(); diff --git a/src/index.ts b/src/index.ts index 4593172..23441e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,8 @@ import { ok } from "assert"; import { Volume } from "memfs/lib/volume"; import { backOff } from "exponential-backoff"; import { callbackify } from "util"; +// @ts-ignore - some times "npm run build" wipes @types for this ?? +import { isEqual } from "lodash"; const toUInt8 = (buf: any): Uint8Array => buf instanceof ArrayBuffer @@ -867,3 +869,303 @@ export function createWriteStream(path: fs.PathLike, options?: string | object): const _opts = typeof options === "string" ? { encoding: options } : options || {}; return new emptyVolume.WriteStream.prototype.__proto__.constructor({ open, write, close }, path, _opts) as Writable; } + +class Singleton { + private _instance: T | undefined; + constructor(private readonly _factory: () => T) {} + release() { + this._instance = undefined; + } + get(): T { + if (!this._instance) { + this._instance = this._factory(); + } + return this._instance; + } +} + +const fileSystemSharedEmitter = new Singleton(() => new EventEmitter("fs")); +const exposedSurfacedEmitters = new Set(); + +class Watcher extends EventEmitter implements fs.FSWatcher, fs.StatWatcher { + private _refs = 0; + constructor( + readonly path: string, + readonly watcher: Function, + private readonly cleanup?: Function, + ) { + super(); + exposedSurfacedEmitters.add(this); + this.on("change", () => { + if (!exposedSurfacedEmitters.has(this)) { + this.close(); + } + }); + } + ref() { + this._refs++; + return this; + } + unref() { + this._refs--; + if (this._refs <= 0) this.close(); + return this; + } + close(): void { + exposedSurfacedEmitters.delete(this); + this._refs = 0; + this.cleanup?.(); + this.emit("close"); + this.removeAllListeners(); + } +} + +// @ts-ignore +export const watch: typeof fs.watch = (filename, ...args): Watcher => { + const opts = (args.find((arg) => typeof arg === "object") || {}) as fs.WatchOptions; + const listener = (args.find((arg) => typeof arg === "function") || (() => {})) as Function; + const _filename = normalizePathLikeToString(filename); + const watcher = new Watcher(_filename, listener); + watcher.on("change", listener as any); + const _listener = (event: string, file: string) => { + if (opts.recursive) { + if (file.startsWith(_filename)) { + watcher.emit("change", event, file); + } + } else { + if (file === _filename) { + watcher.emit("change", event, file); + } + } + }; + const _c = _listener.bind(null, "change"); + const _r = _listener.bind(null, "rename"); + fileSystemSharedEmitter.get().on("change", _c); + fileSystemSharedEmitter.get().on("rename", _r); + return watcher.once("close", () => { + fileSystemSharedEmitter.get().off("change", _c); + fileSystemSharedEmitter.get().off("rename", _r); + }); +}; + +// @ts-ignore +export const watchFile: typeof fs.watchFile = (filename, ...args): Watcher => { + // const opts = (args.find((arg) => typeof arg === "object") || {}) as fs.WatchFileOptions; + const listener = (args.find((arg) => typeof arg === "function") || (() => {})) as Function; + const watcher = new Watcher(normalizePathLikeToString(filename), listener); + watcher.on("change", listener as any); + const _listener = (curr?: Partial, prev?: Partial) => { + const now = new Date(); + const nowStat = { + atime: now, + mtime: now, + ctime: now, + birthtime: now, + atimeMs: now.getTime(), + mtimeMs: now.getTime(), + ctimeMs: now.getTime(), + birthtimeMs: now.getTime(), + blksize: 0, + blocks: 0, + dev: 0, + gid: 0, + ino: 0, + mode: 0, + nlink: 0, + rdev: 0, + size: 0, + uid: 0, + isBlockDevice: () => false, + isCharacterDevice: () => false, + isDirectory: () => false, + isFIFO: () => false, + isFile: () => false, + isSocket: () => false, + isSymbolicLink: () => false, + }; + const _curr = { ...nowStat, ...curr }; + const _prev = { ...nowStat, ...prev }; + watcher.emit("change", _curr, _prev); + }; + fileSystemSharedEmitter.get().on("watch_", _listener); + return watcher.once("close", () => { + fileSystemSharedEmitter.get().off("watch_", _listener); + }); +}; + +// @ts-ignore +export const unwatchFile: typeof fs.unwatchFile = (filename, ...args): void => { + if (filename === "*") { + fileSystemSharedEmitter.get().removeAllListeners(); + fileSystemSharedEmitter.get().dispose(); + fileSystemSharedEmitter.release(); + return exposedSurfacedEmitters.clear(); + } + const _filename = normalizePathLikeToString(filename); + const listener = (args.find((arg) => typeof arg === "function") || (() => {})) as Function; + for (const emitter of exposedSurfacedEmitters) { + if (listener ? emitter.watcher === listener : emitter.path === _filename) { + emitter.close(); + } + } +}; + +export const join = (path: string, ...paths: string[]) => resolve([path, ...paths].join("/")); +export const dirname = (path: string) => resolve(path).split("/").slice(0, -1).join("/") || "/"; +export const basename = (path: string) => resolve(path).split("/").pop() || ""; +export const resolve = (path: string) => path.replace(/\/$/, "").replace(/^/, "/").replace(/\/+/g, "/") || "/"; + +export interface MirrorOptions { + readonly externalFS: typeof fs; + readonly externalDir: string; + readonly internalDir?: string; + readonly fileFilter?: (path: string) => boolean; + readonly logger?: (format: string, ...args: any[]) => void; +} + +export interface MirrorOutput { + readonly internalWatcher: fs.FSWatcher; + readonly externalWatcher: fs.FSWatcher; + readonly close: () => void; +} + +export async function mirror(opts: MirrorOptions): Promise { + const { externalFS, externalDir, internalDir = externalDir, fileFilter = () => true, logger = console.log } = opts; + logger("mirroring %s (external) with %s (internal)", externalDir, internalDir); + await promises.rm(internalDir, { recursive: true, force: true }).catch(() => {}); + await promises.mkdir(internalDir, { recursive: true }); + logger("cleaned %s (internal)", internalDir); + + async function listAllFiles(path: string, readdirFn: typeof promises.readdir): Promise { + const stats = await readdirFn(path, { withFileTypes: true, recursive: true }); + const result: string[] = []; + for (const stat of stats) { + ok(typeof stat !== "string"); + const filePath = join(path, stat.name); + if (stat.isDirectory()) { + result.push(...(await listAllFiles(filePath, readdirFn))); + } else if (stat.isFile()) { + result.push(filePath); + } + } + return result.map((file) => file.replace(path, "")).filter(fileFilter); + } + + async function compareTwoFiles(rel: string) { + const internalPath = join(internalDir, rel); + const externalPath = join(externalDir, rel); + const [internalContent, externalContent] = await Promise.all([ + promises.readFile(internalPath).catch(() => ""), + externalFS.promises.readFile(externalPath).catch(() => ""), + ]); + if (!isEqual(internalContent, externalContent)) + return { + internalContent, + externalContent, + }; + } + + async function initialize() { + logger("initializing"); + await externalFS.promises.mkdir(externalDir, { recursive: true }); + const externalFiles = await listAllFiles(externalDir, externalFS.promises.readdir); + logger("found %d files in %s (external)", externalFiles.length, externalDir); + for (const file of externalFiles) { + logger("copying %s (external) to %s (internal)", externalDir + file, internalDir + file); + const externalPath = externalDir + file; + const internalPath = internalDir + file; + const externalContent = await externalFS.promises.readFile(externalPath); + await promises.mkdir(dirname(internalPath), { recursive: true }); + await promises.writeFile(internalPath, externalContent); + logger("copied %s", basename(internalPath)); + } + } + + await initialize(); + logger("initialized"); + + const internalCooldown: string[] = []; + const externalCooldown: string[] = []; + + const internalWatcher = watch(internalDir, { recursive: true }, async (change, affected) => { + logger(">> internal event: %s %s", change, affected); + const rel = affected.replace(internalDir, "").replace(/^\//, ""); + const internalPath = join(internalDir, rel); + const externalPath = join(externalDir, rel); + + if (!fileFilter(rel)) return; + + if (change === "change") { + setTimeout(async () => { + if (externalCooldown.includes(rel)) { + externalCooldown.splice(externalCooldown.indexOf(rel), 1); + return; + } + const diff = await compareTwoFiles(rel); + if (diff) { + internalCooldown.push(rel); + logger("syncing %s to %s", internalPath, externalPath); + await externalFS.promises.mkdir(dirname(externalPath), { recursive: true }); + await externalFS.promises.writeFile(externalPath, diff.internalContent); + } + }); + } + + if (change === "rename") { + setTimeout(async () => { + const existsInInternal = await promises.exists(internalPath); + const existsInExternal = externalFS.existsSync(externalPath); + if (!existsInInternal && existsInExternal) { + logger("deleting %s (external)", externalPath); + await externalFS.promises.rm(externalPath).catch(() => {}); + } + }, 100); + } + }); + + const externalWatcher = externalFS.watch(externalDir, { recursive: true }, async (change, affected) => { + logger(">> external event: %s %s", change, affected); + const rel = affected.replace(externalDir, "").replace(/^\//, ""); + const internalPath = join(internalDir, rel); + const externalPath = join(externalDir, rel); + + if (!fileFilter(rel)) return; + + if (change === "change") { + setTimeout(async () => { + if (internalCooldown.includes(rel)) { + internalCooldown.splice(internalCooldown.indexOf(rel), 1); + return; + } + const diff = await compareTwoFiles(rel); + console.log(diff); + if (diff) { + externalCooldown.push(rel); + logger("syncing %s to %s", externalPath, internalPath); + await promises.mkdir(dirname(internalPath), { recursive: true }); + await promises.writeFile(internalPath, diff.externalContent); + } + }); + } + + if (change === "rename") { + setTimeout(async () => { + const existsInInternal = await promises.exists(internalPath); + const existsInExternal = externalFS.existsSync(externalPath); + if (!existsInExternal && existsInInternal) { + logger("deleting %s (internal)", internalPath); + await promises.rm(internalPath, undefined).catch(() => {}); + } + }, 100); + } + }); + + return { + internalWatcher, + externalWatcher, + close: () => { + internalWatcher.close(); + externalWatcher.close(); + }, + }; +} diff --git a/src/plugin.ts b/src/plugin.ts deleted file mode 100644 index b713845..0000000 --- a/src/plugin.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Type, type Static } from "@sinclair/typebox"; -import { type Compiler, sources } from "webpack"; -import { Glob, type GlobOptions } from "glob"; -import { validate } from "schema-utils"; -import assert from "assert"; -import path from "path"; -import fs from "fs"; - -import * as wasabio from "."; - -const schema = Type.Object({ - name: Type.String(), - include: Type.Array(Type.String()), - baseDir: Type.Optional(Type.String()), - options: Type.Optional(Type.Object(Type.Any())), -}); - -export type WasabioPluginOptions = Static & { - options?: GlobOptions | undefined; -}; - -const PLUGIN_NAME = "WebpackWasabioPlugin"; - -export default class WebpackWasabioPlugin { - constructor(public readonly options: WasabioPluginOptions) { - validate(schema, options, { - name: PLUGIN_NAME, - baseDataPath: "options", - }); - } - apply(compiler: Compiler): void { - compiler.hooks.afterCompile.tapPromise(PLUGIN_NAME, async (compilation) => { - const { baseDir, include } = this.options; - const _baseDir = baseDir || process.cwd(); - const mem = await wasabio.initialize(); - for (const glob of include) { - const g = new Glob(glob, { - ...this.options.options, - withFileTypes: true, - cwd: _baseDir, - }); - for await (const p of g) { - assert(typeof p !== "string"); - if (p.isDirectory()) { - await wasabio.promises.mkdir(p.fullpath(), { recursive: true }); - } else { - await wasabio.promises.mkdir(path.dirname(p.fullpath()), { recursive: true }); - await wasabio.promises.writeFile(p.fullpath(), await fs.promises.readFile(p.fullpath())); - } - } - } - const buf = wasabio.serialize(mem); - const zip = await wasabio.compress(buf); - compilation.assets[this.options.name] = new sources.RawSource(Buffer.from(zip), false); - }); - } -} diff --git a/test/filesystem/watch.test.ts b/test/filesystem/watch.test.ts new file mode 100644 index 0000000..24e2418 --- /dev/null +++ b/test/filesystem/watch.test.ts @@ -0,0 +1,46 @@ +import * as wasabio from "../../dist"; +import { assert } from "chai"; +import path from "path"; + +declare global { + var WASABIO: typeof wasabio; +} + +const fs = globalThis.WASABIO !== undefined ? globalThis.WASABIO : wasabio; + +var testDir = "/tmp"; + +var filenameOne = "watch.txt"; +var filepathOne = path.join(testDir, filenameOne); + +try { + fs.unlinkSync(filepathOne); +} catch (e) {} + +describe("fs.watch tests", () => { + before(async () => { + if (!fs.available()) await fs.initialize(); + fs.mkdirSync(testDir, { recursive: true }); + }); + + it("should watch a file", function (done) { + fs.writeFileSync(filepathOne, "hello"); + + setTimeout(function () { + fs.writeFileSync(filepathOne, "world"); + }, 20); + + var watcher = fs.watch(filepathOne); + watcher.on("change", function (event, filename) { + assert.equal("change", event); + assert.isTrue(filename.toString().endsWith(filepathOne)); + watcher.close(); + done(); + }); + }); + + after(() => { + fs.unwatchFile("*"); + fs.rmSync(testDir, { recursive: true, force: true }); + }); +}); diff --git a/test/path.test.ts b/test/path.test.ts new file mode 100644 index 0000000..1c253de --- /dev/null +++ b/test/path.test.ts @@ -0,0 +1,29 @@ +import * as wasabio from "../dist"; +import { assert } from "chai"; + +declare global { + var WASABIO: typeof wasabio; +} + +const fs = globalThis.WASABIO !== undefined ? globalThis.WASABIO : wasabio; + +describe("path utility tests", () => { + before(async () => { + if (!fs.available()) await fs.initialize(); + }); + + it("should have sane join()", () => { + assert.strictEqual(fs.join("/a", "b", "c"), "/a/b/c"); + assert.strictEqual(fs.join("/a", "b", "c/"), "/a/b/c"); + }); + + it("should have sane dirname()", () => { + assert.strictEqual(fs.dirname("/a/b/c"), "/a/b"); + assert.strictEqual(fs.dirname("/a/b/c/"), "/a/b"); + }); + + it("should have sane basename()", () => { + assert.strictEqual(fs.basename("/a/b/c"), "c"); + assert.strictEqual(fs.basename("/a/b/c/"), "c"); + }); +}); diff --git a/test/test.mirror.ts b/test/test.mirror.ts new file mode 100644 index 0000000..62a300f --- /dev/null +++ b/test/test.mirror.ts @@ -0,0 +1,94 @@ +// this test is isolated because "memfs" does not clean after itself up properly + +import * as wasabio from "../dist"; +import externalFS from "memfs"; +import { assert } from "chai"; + +declare global { + var WASABIO: typeof wasabio; +} + +const internalFs = globalThis.WASABIO !== undefined ? globalThis.WASABIO : wasabio; +const { join } = internalFs; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +const internalDir = "/a/b/c"; +const externalDir = "/nested"; + +let cleanup: () => void; + +async function before() { + if (!internalFs.available()) await internalFs.initialize(); + externalFS.vol.reset(); + const mirrored = await internalFs.mirror({ + externalFS: externalFS.fs as any, + externalDir, + internalDir, + fileFilter: (f) => f.endsWith(".txt"), + logger: console.log, + }); + cleanup = mirrored.close; +} + +async function main() { + console.log("mirror utility tests"); + + await internalFs.promises.writeFile(join(internalDir, "/file.txt"), "hello world 1", "utf8"); + await sleep(500); + assert.strictEqual(externalFS.fs.readFileSync(join(externalDir, "/file.txt"), "utf8"), "hello world 1"); + console.log("file.txt written to externalFs"); + + await internalFs.promises.writeFile(join(internalDir, "/file2.txt"), "hello world 2", "utf8"); + await sleep(500); + assert.strictEqual(externalFS.fs.readFileSync(join(externalDir, "/file2.txt"), "utf8"), "hello world 2"); + console.log("file2.txt written to externalFs"); + + await internalFs.promises.writeFile(join(internalDir, "/file.txt"), "hello world 3", "utf8"); + await sleep(500); + assert.strictEqual(externalFS.fs.readFileSync(join(externalDir, "/file.txt"), "utf8"), "hello world 3"); + console.log("updates in file.txt reflected in externalFs"); + + await externalFS.fs.promises.writeFile(join(externalDir, "/file2.txt"), "hello world 4"); + await sleep(500); + assert.strictEqual(await internalFs.promises.readFile(join(internalDir, "/file2.txt"), "utf8"), "hello world 4"); + console.log("updates in file2.txt reflected in internalFs"); + + await internalFs.promises.rm(join(internalDir, "/file.txt"), {}); + console.log("file.txt removed from internalFs"); + await sleep(500); + const externalReadDir = externalFS.fs.readdirSync(externalDir); + assert.deepStrictEqual(externalReadDir, ["file2.txt"]); + console.log("file.txt removed from externalFs"); + + await externalFS.fs.promises.rm(join(externalDir, "/file2.txt")); + console.log("file2.txt removed from externalFs"); + await sleep(500); + const internalReadDir = await internalFs.promises.readdir(internalDir); + assert.deepStrictEqual(internalReadDir, []); + console.log("file2.txt removed from internalFs"); +} + +async function after() { + cleanup(); + internalFs.unwatchFile("*"); + externalFS.fs.unwatchFile(externalDir); + externalFS.vol.reset(); +} + +before() + .then(main) + .then(after) + .then(() => { + console.log("done."); + process.exit(0); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); + +setTimeout(() => { + console.error("timed out."); + process.exit(1); +}, +require("../.mocharc.json").timeout); diff --git a/webpack.config.ts b/webpack.config.ts index 7adf2fd..d469315 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -6,7 +6,6 @@ import childProcess from "child_process"; import CopyPlugin from "copy-webpack-plugin"; import TerserPlugin from "terser-webpack-plugin"; import ShellPlugin from "webpack-shell-plugin-next"; -import NodeExternals from "webpack-node-externals"; const OUT_DIR = path.resolve("dist"); fs.rmSync(OUT_DIR, { recursive: true, force: true }); @@ -124,179 +123,182 @@ function buildWithWasmPack(webpackMode: string) { fs.writeFileSync("pkg/package.json", JSON.stringify(pkjJson, null, 2)); } -export default [ - function wasabioLibrary(_env: unknown, { mode }: { mode: string }) { - const isProduction = mode === "production"; +function wasabioBrowserLibrary(_env: unknown, { mode }: { mode: string }) { + const isProduction = mode === "production"; - const config: webpack.Configuration = { - mode: isProduction ? "production" : "development", - entry: "./src/index.ts", - devtool: isProduction ? false : "inline-source-map", - output: { - path: OUT_DIR, - library: { - commonjs: "wasabio", - amd: "wasabio", - root: "WASABIO", - }, - libraryTarget: "umd", - umdNamedDefine: true, - globalObject: `(typeof self !== 'undefined' ? self : this)`, - filename: "index.js", - }, - node: { - global: false, - __filename: false, - __dirname: false, - }, - watchOptions: { - ignored: [OUT_DIR], + const config: webpack.Configuration = { + mode: isProduction ? "production" : "development", + target: "web", + entry: "./src/index.ts", + devtool: isProduction ? false : "inline-source-map", + output: { + path: OUT_DIR, + library: { + commonjs: "wasabio", + amd: "wasabio", + root: "WASABIO", }, - optimization: { - nodeEnv: false, - minimize: mode === "production", - minimizer: [ - new TerserPlugin({ - extractComments: false, - terserOptions: { - format: { - comments: false, - }, + libraryTarget: "umd", + umdNamedDefine: true, + globalObject: `(typeof self !== 'undefined' ? self : this)`, + filename: "index.browser.js", + }, + node: { + global: false, + __filename: false, + __dirname: false, + }, + watchOptions: { + ignored: [OUT_DIR], + }, + optimization: { + nodeEnv: false, + minimize: mode === "production", + minimizer: [ + new TerserPlugin({ + extractComments: false, + terserOptions: { + format: { + comments: false, }, - }), - ], - }, - performance: { - hints: false, - }, - plugins: [ - new ShellPlugin({ - safe: true, - onBuildStart: { - blocking: true, - scripts: [ - "mkdir -p deps", - installWasiSDK, - installEmccSDK, - installWasmBindgen, - buildWithWasmPack.bind(null, mode), - ], - }, - onAfterDone: { - blocking: false, - scripts: [ - [ - "npx dts-bundle-generator", - "--export-referenced-types=false", - "--umd-module-name=wasabio", - "-o dist/index.d.ts", - "src/index.ts", - ].join(" "), - ], }, }), - new webpack.ProvidePlugin({ - Buffer: ["buffer", "Buffer"], - process: "process", - URL: ["url", "URL"], - }), - new CopyPlugin({ - patterns: [ - { from: "LICENSE" }, - { from: "README.md" }, - { - from: "package.json", - transform: (content) => { - const pkgJson = JSON.parse(content.toString()); - delete pkgJson.devDependencies; - delete pkgJson.prettier; - delete pkgJson.scripts; - delete pkgJson.private; - delete pkgJson.type; - pkgJson.main = "./index.js"; - pkgJson.types = "./index.d.ts"; - return JSON.stringify(pkgJson, null, 2); - }, - }, - ], - }), ], - module: { - rules: [ - { - test: /\.[jt]sx?$/, - loader: "ts-loader", - options: { - transpileOnly: false, - }, - }, - { - test: /wasabio\.js$/, - loader: "string-replace-loader", - options: { - search: "input = new URL('wasabio_bg.wasm', import.meta.url);", - replace: "throw new Error('no default wasm binary bundled.');", - strict: true, - }, - }, + }, + performance: { + hints: false, + }, + plugins: [ + new ShellPlugin({ + safe: true, + onBuildStart: { + blocking: true, + scripts: [ + "mkdir -p deps", + installWasiSDK, + installEmccSDK, + installWasmBindgen, + buildWithWasmPack.bind(null, mode), + ], + }, + onAfterDone: { + blocking: false, + scripts: [ + [ + "npx dts-bundle-generator", + "--export-referenced-types=false", + "--umd-module-name=wasabio", + "-o dist/index.d.ts", + "src/index.ts", + ].join(" "), + ], + }, + }), + new webpack.ProvidePlugin({ + Buffer: ["buffer", "Buffer"], + process: "process", + URL: ["url", "URL"], + }), + new CopyPlugin({ + patterns: [ + { from: "LICENSE" }, + { from: "README.md" }, { - test: /\.wasm$/, - loader: "url-loader", - options: { - mimetype: "delete/me", - limit: 15 * 1024 * 1024, - // this removes the "data:;base64," from the bundle - generator: (content: Buffer) => content.toString("base64"), + from: "package.json", + transform: (content) => { + const pkgJson = JSON.parse(content.toString()); + delete pkgJson.devDependencies; + delete pkgJson.prettier; + delete pkgJson.scripts; + delete pkgJson.private; + delete pkgJson.type; + pkgJson.main = "./index.native.js"; + pkgJson.browser = "./index.browser.js"; + pkgJson.types = "./index.d.ts"; + return JSON.stringify(pkgJson, null, 2); }, }, ], - }, - resolve: { - extensions: [".tsx", ".ts", ".jsx", ".js", ".json"], - fallback: { - fs: false, - url: require.resolve("url/"), - path: require.resolve("path-browserify"), - stream: require.resolve("stream-browserify"), + }), + ], + module: { + rules: [ + { + test: /\.[jt]sx?$/, + loader: "ts-loader", + options: { + transpileOnly: false, + }, }, - alias: { - assert: "assert", - buffer: "buffer", - process: "process", + { + test: /wasabio\.js$/, + loader: "string-replace-loader", + options: { + search: "input = new URL('wasabio_bg.wasm', import.meta.url);", + replace: "throw new Error('no default wasm binary bundled.');", + strict: true, + }, }, - }, - }; - - return config; - }, - function wasabioPlugin(_env: unknown, { mode }: { mode: string }) { - const isProduction = mode === "production"; - return { - mode: isProduction ? "production" : "development", - entry: "./src/plugin.ts", - output: { - path: OUT_DIR, - filename: "plugin.js", - }, - target: "node", - devtool: isProduction ? false : "inline-source-map", - externalsPresets: { node: true }, - externals: [NodeExternals(), { "wasabio-external": "commonjs wasabio" }], - plugins: [new webpack.NormalModuleReplacementPlugin(/src\/index\.ts$/, "redirect.ts")], - module: { - rules: [ - { - test: /\.[jt]sx?$/, - loader: "ts-loader", - options: { - transpileOnly: false, - }, + { + test: /\.wasm$/, + loader: "url-loader", + options: { + mimetype: "delete/me", + limit: 15 * 1024 * 1024, + // this removes the "data:;base64," from the bundle + generator: (content: Buffer) => content.toString("base64"), }, - ], + }, + ], + }, + resolve: { + extensions: [".tsx", ".ts", ".jsx", ".js", ".json"], + fallback: { + fs: false, + url: require.resolve("url/"), + path: require.resolve("path-browserify"), + stream: require.resolve("stream-browserify"), }, - resolve: { - extensions: [".tsx", ".ts", ".jsx", ".js", ".json"], + alias: { + assert: "assert", + buffer: "buffer", + process: "process", }, - } as webpack.Configuration; - }, -]; + }, + }; + + return config; +} + +function wasabioNativeLibrary(_env: unknown, { mode }: { mode: string }) { + const browserConfig = wasabioBrowserLibrary(_env, { mode }); + browserConfig.target = "node"; + // remove buffer alias + // @ts-expect-error + delete browserConfig.resolve.alias.buffer; + // remove stream fallback + // @ts-expect-error + delete browserConfig.resolve.fallback.stream; + // change the output filename + // @ts-expect-error + browserConfig.output.filename = "index.native.js"; + // remove the Copy plugin + // @ts-expect-error + browserConfig.plugins.pop(); + // replace the Provide plugin with one that does not override Buffer + // @ts-expect-error + browserConfig.plugins.pop(); + // @ts-expect-error + browserConfig.plugins.push( + new webpack.ProvidePlugin({ + process: "process", + URL: ["url", "URL"], + }), + ); + // remove the shell plugin + // @ts-expect-error + browserConfig.plugins.shift(); + return browserConfig; +} + +export default [wasabioBrowserLibrary, wasabioNativeLibrary];