aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Ciro Spaciari <ciro.spaciari@gmail.com> 2023-06-24 03:24:34 -0300
committerGravatar GitHub <noreply@github.com> 2023-06-23 23:24:34 -0700
commit069b42a7cc1275969859dc60e7c303528ca2dccb (patch)
tree446e9728bb957571c2062d8bd988abc4342dc57c
parentceec1afec2bb187fecef0f5006dfe27ee3e1a9a6 (diff)
downloadbun-069b42a7cc1275969859dc60e7c303528ca2dccb.tar.gz
bun-069b42a7cc1275969859dc60e7c303528ca2dccb.tar.zst
bun-069b42a7cc1275969859dc60e7c303528ca2dccb.zip
[feat] fs.watch (#3249)
* initial support * add types * fix comment * fix types * bigfix up * more fixes * fix some encoding support for watch * fix rename event * fixup * fix latin1 * add fs_events, still failing some tests * fixuup * remove unecesary check * readd tests ops * this is necessary? just testing CI/CD weird errors * just use dupe here * cleanup and fix deinit * fix zig upgrade
-rw-r--r--packages/bun-types/fs.d.ts97
-rw-r--r--packages/bun-types/fs/promises.d.ts58
-rw-r--r--src/bun.js/bindings/JSSink.cpp2
-rw-r--r--src/bun.js/bindings/JSSink.h2
-rw-r--r--src/bun.js/bindings/JSSinkLookupTable.h2
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h1
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h1
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h6
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h7
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses.cpp322
-rw-r--r--src/bun.js/bindings/ZigGeneratedClasses.h56
-rw-r--r--src/bun.js/bindings/generated_classes.zig94
-rw-r--r--src/bun.js/bindings/generated_classes_list.zig1
-rw-r--r--src/bun.js/event_loop.zig7
-rw-r--r--src/bun.js/javascript.zig7
-rw-r--r--src/bun.js/node/fs_events.zig609
-rw-r--r--src/bun.js/node/node.classes.ts30
-rw-r--r--src/bun.js/node/node_fs.zig13
-rw-r--r--src/bun.js/node/node_fs_binding.zig2
-rw-r--r--src/bun.js/node/node_fs_watcher.zig913
-rw-r--r--src/bun.js/node/types.zig4
-rw-r--r--src/bun.js/webcore/encoding.zig15
-rw-r--r--src/fs.zig54
-rw-r--r--src/http.zig7
-rw-r--r--src/js/node/fs.js65
-rw-r--r--src/js/node/fs.promises.ts51
-rw-r--r--src/js/out/modules/node/fs.js50
-rw-r--r--src/js/out/modules/node/fs.promises.js2
-rw-r--r--src/js/private.d.ts86
-rw-r--r--src/jsc.zig1
-rw-r--r--src/watcher.zig68
-rw-r--r--test/js/node/watch/fixtures/close.js7
-rw-r--r--test/js/node/watch/fixtures/persistent.js5
-rw-r--r--test/js/node/watch/fixtures/relative.js23
-rw-r--r--test/js/node/watch/fixtures/unref.js7
-rw-r--r--test/js/node/watch/fs.watch.test.js424
36 files changed, 3072 insertions, 27 deletions
diff --git a/packages/bun-types/fs.d.ts b/packages/bun-types/fs.d.ts
index 14c5c1d1d..5dfb2c7f2 100644
--- a/packages/bun-types/fs.d.ts
+++ b/packages/bun-types/fs.d.ts
@@ -19,6 +19,7 @@
*/
declare module "fs" {
import * as stream from "stream";
+ import type EventEmitter from "events";
import type { SystemError, ArrayBufferView } from "bun";
interface ObjectEncodingOptions {
encoding?: BufferEncoding | null | undefined;
@@ -3929,6 +3930,102 @@ declare module "fs" {
*/
recursive?: boolean;
}
+
+ export interface FSWatcher extends EventEmitter {
+ /**
+ * Stop watching for changes on the given `fs.FSWatcher`. Once stopped, the `fs.FSWatcher` object is no longer usable.
+ * @since v0.6.8
+ */
+ close(): void;
+
+ /**
+ * When called, requests that the Node.js event loop not exit so long as the <fs.FSWatcher> is active. Calling watcher.ref() multiple times will have no effect.
+ */
+ ref(): void;
+
+ /**
+ * When called, the active <fs.FSWatcher> object will not require the Node.js event loop to remain active. If there is no other activity keeping the event loop running, the process may exit before the <fs.FSWatcher> object's callback is invoked. Calling watcher.unref() multiple times will have no effect.
+ */
+ unref(): void;
+
+ /**
+ * events.EventEmitter
+ * 1. change
+ * 2. error
+ */
+ addListener(event: string, listener: (...args: any[]) => void): this;
+ addListener(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this;
+ addListener(event: 'error', listener: (error: Error) => void): this;
+ addListener(event: 'close', listener: () => void): this;
+ on(event: string, listener: (...args: any[]) => void): this;
+ on(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this;
+ on(event: 'error', listener: (error: Error) => void): this;
+ on(event: 'close', listener: () => void): this;
+ once(event: string, listener: (...args: any[]) => void): this;
+ once(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this;
+ once(event: 'error', listener: (error: Error) => void): this;
+ once(event: 'close', listener: () => void): this;
+ prependListener(event: string, listener: (...args: any[]) => void): this;
+ prependListener(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this;
+ prependListener(event: 'error', listener: (error: Error) => void): this;
+ prependListener(event: 'close', listener: () => void): this;
+ prependOnceListener(event: string, listener: (...args: any[]) => void): this;
+ prependOnceListener(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this;
+ prependOnceListener(event: 'error', listener: (error: Error) => void): this;
+ prependOnceListener(event: 'close', listener: () => void): this;
+ }
+ /**
+ * Watch for changes on `filename`, where `filename` is either a file or a
+ * directory.
+ *
+ * The second argument is optional. If `options` is provided as a string, it
+ * specifies the `encoding`. Otherwise `options` should be passed as an object.
+ *
+ * The listener callback gets two arguments `(eventType, filename)`. `eventType`is either `'rename'` or `'change'`, and `filename` is the name of the file
+ * which triggered the event.
+ *
+ * On most platforms, `'rename'` is emitted whenever a filename appears or
+ * disappears in the directory.
+ *
+ * The listener callback is attached to the `'change'` event fired by `fs.FSWatcher`, but it is not the same thing as the `'change'` value of`eventType`.
+ *
+ * If a `signal` is passed, aborting the corresponding AbortController will close
+ * the returned `fs.FSWatcher`.
+ * @since v0.6.8
+ * @param listener
+ */
+ export function watch(
+ filename: PathLike,
+ options:
+ | (WatchOptions & {
+ encoding: 'buffer';
+ })
+ | 'buffer',
+ listener?: WatchListener<Buffer>
+ ): FSWatcher;
+ /**
+ * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`.
+ * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol.
+ * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options.
+ * If `encoding` is not supplied, the default of `'utf8'` is used.
+ * If `persistent` is not supplied, the default of `true` is used.
+ * If `recursive` is not supplied, the default of `false` is used.
+ */
+ export function watch(filename: PathLike, options?: WatchOptions | BufferEncoding | null, listener?: WatchListener<string>): FSWatcher;
+ /**
+ * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`.
+ * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol.
+ * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options.
+ * If `encoding` is not supplied, the default of `'utf8'` is used.
+ * If `persistent` is not supplied, the default of `true` is used.
+ * If `recursive` is not supplied, the default of `false` is used.
+ */
+ export function watch(filename: PathLike, options: WatchOptions | string, listener?: WatchListener<string | Buffer>): FSWatcher;
+ /**
+ * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`.
+ * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol.
+ */
+ export function watch(filename: PathLike, listener?: WatchListener<string>): FSWatcher;
}
declare module "node:fs" {
diff --git a/packages/bun-types/fs/promises.d.ts b/packages/bun-types/fs/promises.d.ts
index 0d71464b9..2b908fceb 100644
--- a/packages/bun-types/fs/promises.d.ts
+++ b/packages/bun-types/fs/promises.d.ts
@@ -26,6 +26,7 @@ declare module "fs/promises" {
Abortable,
RmOptions,
RmDirOptions,
+ WatchOptions,
} from "node:fs";
const constants: typeof import("node:fs")["constants"];
@@ -709,6 +710,63 @@ declare module "fs/promises" {
* To remove a directory recursively, use `fs.promises.rm()` instead, with the `recursive` option set to `true`.
*/
function rmdir(path: PathLike, options?: RmDirOptions): Promise<void>;
+
+ /**
+ * Returns an async iterator that watches for changes on `filename`, where `filename`is either a file or a directory.
+ *
+ * ```js
+ * const { watch } = require('node:fs/promises');
+ *
+ * const ac = new AbortController();
+ * const { signal } = ac;
+ * setTimeout(() => ac.abort(), 10000);
+ *
+ * (async () => {
+ * try {
+ * const watcher = watch(__filename, { signal });
+ * for await (const event of watcher)
+ * console.log(event);
+ * } catch (err) {
+ * if (err.name === 'AbortError')
+ * return;
+ * throw err;
+ * }
+ * })();
+ * ```
+ *
+ * On most platforms, `'rename'` is emitted whenever a filename appears or
+ * disappears in the directory.
+ *
+ * All the `caveats` for `fs.watch()` also apply to `fsPromises.watch()`.
+ * @since v0.6.8
+ * @return of objects with the properties:
+ */
+ function watch(
+ filename: PathLike,
+ options:
+ | (WatchOptions & {
+ encoding: 'buffer';
+ })
+ | 'buffer'
+ ): AsyncIterable<FileChangeInfo<Buffer>>;
+ /**
+ * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`.
+ * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol.
+ * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options.
+ * If `encoding` is not supplied, the default of `'utf8'` is used.
+ * If `persistent` is not supplied, the default of `true` is used.
+ * If `recursive` is not supplied, the default of `false` is used.
+ */
+ function watch(filename: PathLike, options?: WatchOptions | BufferEncoding): AsyncIterable<FileChangeInfo<string>>;
+ /**
+ * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`.
+ * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol.
+ * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options.
+ * If `encoding` is not supplied, the default of `'utf8'` is used.
+ * If `persistent` is not supplied, the default of `true` is used.
+ * If `recursive` is not supplied, the default of `false` is used.
+ */
+ function watch(filename: PathLike, options: WatchOptions | string): AsyncIterable<FileChangeInfo<string>> | AsyncIterable<FileChangeInfo<Buffer>>;
}
declare module "node:fs/promises" {
diff --git a/src/bun.js/bindings/JSSink.cpp b/src/bun.js/bindings/JSSink.cpp
index 36be334dd..4acf01ff7 100644
--- a/src/bun.js/bindings/JSSink.cpp
+++ b/src/bun.js/bindings/JSSink.cpp
@@ -1,6 +1,6 @@
// AUTO-GENERATED FILE. DO NOT EDIT.
-// Generated by 'make generate-sink' at 2023-05-18T01:04:00.447Z
+// Generated by 'make generate-sink' at 2023-06-14T21:38:04.394Z
// To regenerate this file, run:
//
// make generate-sink
diff --git a/src/bun.js/bindings/JSSink.h b/src/bun.js/bindings/JSSink.h
index 5bbfab777..37c458e9b 100644
--- a/src/bun.js/bindings/JSSink.h
+++ b/src/bun.js/bindings/JSSink.h
@@ -1,6 +1,6 @@
// AUTO-GENERATED FILE. DO NOT EDIT.
-// Generated by 'make generate-sink' at 2023-05-18T01:04:00.446Z
+// Generated by 'make generate-sink' at 2023-06-14T21:38:04.394Z
//
#pragma once
diff --git a/src/bun.js/bindings/JSSinkLookupTable.h b/src/bun.js/bindings/JSSinkLookupTable.h
index a4ace6dc3..e4ed81629 100644
--- a/src/bun.js/bindings/JSSinkLookupTable.h
+++ b/src/bun.js/bindings/JSSinkLookupTable.h
@@ -1,4 +1,4 @@
-// Automatically generated from src/bun.js/bindings/JSSink.cpp using /Users/jarred/Code/bun/src/bun.js/WebKit/Source/JavaScriptCore/create_hash_table. DO NOT EDIT!
+// Automatically generated from src/bun.js/bindings/JSSink.cpp using /home/cirospaciari/Repos/bun/src/bun.js/WebKit/Source/JavaScriptCore/create_hash_table. DO NOT EDIT!
diff --git a/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h b/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h
index b16febcdb..f0d491c0b 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h
+++ b/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h
@@ -8,6 +8,7 @@ std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForExpectConstructor;std:
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForExpectAnything;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForExpectStringContaining;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForExpectStringMatching;
+std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForFSWatcher;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForFileSystemRouter;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForFileSystemRouterConstructor;std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForListener;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForMD4;
diff --git a/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h b/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h
index 59263e62c..02a9adbca 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h
+++ b/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h
@@ -8,6 +8,7 @@ std::unique_ptr<IsoSubspace> m_subspaceForExpectConstructor;std::unique_ptr<IsoS
std::unique_ptr<IsoSubspace> m_subspaceForExpectAnything;
std::unique_ptr<IsoSubspace> m_subspaceForExpectStringContaining;
std::unique_ptr<IsoSubspace> m_subspaceForExpectStringMatching;
+std::unique_ptr<IsoSubspace> m_subspaceForFSWatcher;
std::unique_ptr<IsoSubspace> m_subspaceForFileSystemRouter;
std::unique_ptr<IsoSubspace> m_subspaceForFileSystemRouterConstructor;std::unique_ptr<IsoSubspace> m_subspaceForListener;
std::unique_ptr<IsoSubspace> m_subspaceForMD4;
diff --git a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h
index 4471fbab3..ac03032e6 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h
+++ b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h
@@ -58,6 +58,12 @@ JSC::Structure* JSExpectStringMatchingStructure() { return m_JSExpectStringMatch
JSC::LazyClassStructure m_JSExpectStringMatching;
bool hasJSExpectStringMatchingSetterValue { false };
mutable JSC::WriteBarrier<JSC::Unknown> m_JSExpectStringMatchingSetterValue;
+JSC::Structure* JSFSWatcherStructure() { return m_JSFSWatcher.getInitializedOnMainThread(this); }
+ JSC::JSObject* JSFSWatcherConstructor() { return m_JSFSWatcher.constructorInitializedOnMainThread(this); }
+ JSC::JSValue JSFSWatcherPrototype() { return m_JSFSWatcher.prototypeInitializedOnMainThread(this); }
+ JSC::LazyClassStructure m_JSFSWatcher;
+ bool hasJSFSWatcherSetterValue { false };
+ mutable JSC::WriteBarrier<JSC::Unknown> m_JSFSWatcherSetterValue;
JSC::Structure* JSFileSystemRouterStructure() { return m_JSFileSystemRouter.getInitializedOnMainThread(this); }
JSC::JSObject* JSFileSystemRouterConstructor() { return m_JSFileSystemRouter.constructorInitializedOnMainThread(this); }
JSC::JSValue JSFileSystemRouterPrototype() { return m_JSFileSystemRouter.prototypeInitializedOnMainThread(this); }
diff --git a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h
index 4e5a2c1fa..b3b5327a4 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h
+++ b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h
@@ -59,6 +59,12 @@ void GlobalObject::initGeneratedLazyClasses() {
init.setStructure(WebCore::JSExpectStringMatching::createStructure(init.vm, init.global, init.prototype));
});
+ m_JSFSWatcher.initLater(
+ [](LazyClassStructure::Initializer& init) {
+ init.setPrototype(WebCore::JSFSWatcher::createPrototype(init.vm, reinterpret_cast<Zig::GlobalObject*>(init.global)));
+ init.setStructure(WebCore::JSFSWatcher::createStructure(init.vm, init.global, init.prototype));
+
+ });
m_JSFileSystemRouter.initLater(
[](LazyClassStructure::Initializer& init) {
init.setPrototype(WebCore::JSFileSystemRouter::createPrototype(init.vm, reinterpret_cast<Zig::GlobalObject*>(init.global)));
@@ -211,6 +217,7 @@ void GlobalObject::visitGeneratedLazyClasses(GlobalObject *thisObject, Visitor&
thisObject->m_JSExpectAnything.visit(visitor); visitor.append(thisObject->m_JSExpectAnythingSetterValue);
thisObject->m_JSExpectStringContaining.visit(visitor); visitor.append(thisObject->m_JSExpectStringContainingSetterValue);
thisObject->m_JSExpectStringMatching.visit(visitor); visitor.append(thisObject->m_JSExpectStringMatchingSetterValue);
+ thisObject->m_JSFSWatcher.visit(visitor); visitor.append(thisObject->m_JSFSWatcherSetterValue);
thisObject->m_JSFileSystemRouter.visit(visitor); visitor.append(thisObject->m_JSFileSystemRouterSetterValue);
thisObject->m_JSListener.visit(visitor); visitor.append(thisObject->m_JSListenerSetterValue);
thisObject->m_JSMD4.visit(visitor); visitor.append(thisObject->m_JSMD4SetterValue);
diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp
index d51a1959a..e0a3f33d6 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses.cpp
+++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp
@@ -5381,6 +5381,297 @@ void JSExpectStringMatching::visitOutputConstraintsImpl(JSCell* cell, Visitor& v
}
DEFINE_VISIT_OUTPUT_CONSTRAINTS(JSExpectStringMatching);
+class JSFSWatcherPrototype final : public JSC::JSNonFinalObject {
+public:
+ using Base = JSC::JSNonFinalObject;
+
+ static JSFSWatcherPrototype* create(JSC::VM& vm, JSGlobalObject* globalObject, JSC::Structure* structure)
+ {
+ JSFSWatcherPrototype* ptr = new (NotNull, JSC::allocateCell<JSFSWatcherPrototype>(vm)) JSFSWatcherPrototype(vm, globalObject, structure);
+ ptr->finishCreation(vm, globalObject);
+ return ptr;
+ }
+
+ DECLARE_INFO;
+ template<typename CellType, JSC::SubspaceAccess>
+ static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
+ {
+ return &vm.plainObjectSpace();
+ }
+ static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
+ {
+ return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
+ }
+
+private:
+ JSFSWatcherPrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure)
+ : Base(vm, structure)
+ {
+ }
+
+ void finishCreation(JSC::VM&, JSC::JSGlobalObject*);
+};
+
+extern "C" void FSWatcherClass__finalize(void*);
+
+extern "C" EncodedJSValue FSWatcherPrototype__doClose(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
+JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__closeCallback);
+
+extern "C" EncodedJSValue FSWatcherPrototype__hasRef(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
+JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__hasRefCallback);
+
+extern "C" EncodedJSValue FSWatcherPrototype__doRef(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
+JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__refCallback);
+
+extern "C" EncodedJSValue FSWatcherPrototype__doUnref(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
+JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__unrefCallback);
+
+STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSFSWatcherPrototype, JSFSWatcherPrototype::Base);
+
+static const HashTableValue JSFSWatcherPrototypeTableValues[] = {
+ { "close"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__closeCallback, 0 } },
+ { "hasRef"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__hasRefCallback, 0 } },
+ { "ref"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__refCallback, 0 } },
+ { "unref"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__unrefCallback, 0 } }
+};
+
+const ClassInfo JSFSWatcherPrototype::s_info = { "FSWatcher"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSFSWatcherPrototype) };
+
+JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__closeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
+{
+ auto& vm = lexicalGlobalObject->vm();
+
+ JSFSWatcher* thisObject = jsDynamicCast<JSFSWatcher*>(callFrame->thisValue());
+
+ if (UNLIKELY(!thisObject)) {
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
+ return throwVMTypeError(lexicalGlobalObject, throwScope);
+ }
+
+ JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject);
+
+#ifdef BUN_DEBUG
+ /** View the file name of the JS file that called this function
+ * from a debugger */
+ SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm);
+ const char* fileName = sourceOrigin.string().utf8().data();
+ static const char* lastFileName = nullptr;
+ if (lastFileName != fileName) {
+ lastFileName = fileName;
+ }
+#endif
+
+ return FSWatcherPrototype__doClose(thisObject->wrapped(), lexicalGlobalObject, callFrame);
+}
+
+JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__hasRefCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
+{
+ auto& vm = lexicalGlobalObject->vm();
+
+ JSFSWatcher* thisObject = jsDynamicCast<JSFSWatcher*>(callFrame->thisValue());
+
+ if (UNLIKELY(!thisObject)) {
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
+ return throwVMTypeError(lexicalGlobalObject, throwScope);
+ }
+
+ JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject);
+
+#ifdef BUN_DEBUG
+ /** View the file name of the JS file that called this function
+ * from a debugger */
+ SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm);
+ const char* fileName = sourceOrigin.string().utf8().data();
+ static const char* lastFileName = nullptr;
+ if (lastFileName != fileName) {
+ lastFileName = fileName;
+ }
+#endif
+
+ return FSWatcherPrototype__hasRef(thisObject->wrapped(), lexicalGlobalObject, callFrame);
+}
+
+JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__refCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
+{
+ auto& vm = lexicalGlobalObject->vm();
+
+ JSFSWatcher* thisObject = jsDynamicCast<JSFSWatcher*>(callFrame->thisValue());
+
+ if (UNLIKELY(!thisObject)) {
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
+ return throwVMTypeError(lexicalGlobalObject, throwScope);
+ }
+
+ JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject);
+
+#ifdef BUN_DEBUG
+ /** View the file name of the JS file that called this function
+ * from a debugger */
+ SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm);
+ const char* fileName = sourceOrigin.string().utf8().data();
+ static const char* lastFileName = nullptr;
+ if (lastFileName != fileName) {
+ lastFileName = fileName;
+ }
+#endif
+
+ return FSWatcherPrototype__doRef(thisObject->wrapped(), lexicalGlobalObject, callFrame);
+}
+
+JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__unrefCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
+{
+ auto& vm = lexicalGlobalObject->vm();
+
+ JSFSWatcher* thisObject = jsDynamicCast<JSFSWatcher*>(callFrame->thisValue());
+
+ if (UNLIKELY(!thisObject)) {
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
+ return throwVMTypeError(lexicalGlobalObject, throwScope);
+ }
+
+ JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject);
+
+#ifdef BUN_DEBUG
+ /** View the file name of the JS file that called this function
+ * from a debugger */
+ SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm);
+ const char* fileName = sourceOrigin.string().utf8().data();
+ static const char* lastFileName = nullptr;
+ if (lastFileName != fileName) {
+ lastFileName = fileName;
+ }
+#endif
+
+ return FSWatcherPrototype__doUnref(thisObject->wrapped(), lexicalGlobalObject, callFrame);
+}
+
+extern "C" void FSWatcherPrototype__listenerSetCachedValue(JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value)
+{
+ auto& vm = globalObject->vm();
+ auto* thisObject = jsCast<JSFSWatcher*>(JSValue::decode(thisValue));
+ thisObject->m_listener.set(vm, thisObject, JSValue::decode(value));
+}
+
+extern "C" EncodedJSValue FSWatcherPrototype__listenerGetCachedValue(JSC::EncodedJSValue thisValue)
+{
+ auto* thisObject = jsCast<JSFSWatcher*>(JSValue::decode(thisValue));
+ return JSValue::encode(thisObject->m_listener.get());
+}
+
+void JSFSWatcherPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
+{
+ Base::finishCreation(vm);
+ reifyStaticProperties(vm, JSFSWatcher::info(), JSFSWatcherPrototypeTableValues, *this);
+ JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
+}
+
+JSFSWatcher::~JSFSWatcher()
+{
+ if (m_ctx) {
+ FSWatcherClass__finalize(m_ctx);
+ }
+}
+void JSFSWatcher::destroy(JSCell* cell)
+{
+ static_cast<JSFSWatcher*>(cell)->JSFSWatcher::~JSFSWatcher();
+}
+
+const ClassInfo JSFSWatcher::s_info = { "FSWatcher"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSFSWatcher) };
+
+void JSFSWatcher::finishCreation(VM& vm)
+{
+ Base::finishCreation(vm);
+ ASSERT(inherits(info()));
+}
+
+JSFSWatcher* JSFSWatcher::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx)
+{
+ JSFSWatcher* ptr = new (NotNull, JSC::allocateCell<JSFSWatcher>(vm)) JSFSWatcher(vm, structure, ctx);
+ ptr->finishCreation(vm);
+ return ptr;
+}
+
+extern "C" void* FSWatcher__fromJS(JSC::EncodedJSValue value)
+{
+ JSC::JSValue decodedValue = JSC::JSValue::decode(value);
+ if (decodedValue.isEmpty() || !decodedValue.isCell())
+ return nullptr;
+
+ JSC::JSCell* cell = decodedValue.asCell();
+ JSFSWatcher* object = JSC::jsDynamicCast<JSFSWatcher*>(cell);
+
+ if (!object)
+ return nullptr;
+
+ return object->wrapped();
+}
+
+extern "C" bool FSWatcher__dangerouslySetPtr(JSC::EncodedJSValue value, void* ptr)
+{
+ JSFSWatcher* object = JSC::jsDynamicCast<JSFSWatcher*>(JSValue::decode(value));
+ if (!object)
+ return false;
+
+ object->m_ctx = ptr;
+ return true;
+}
+
+extern "C" const size_t FSWatcher__ptrOffset = JSFSWatcher::offsetOfWrapped();
+
+void JSFSWatcher::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer)
+{
+ auto* thisObject = jsCast<JSFSWatcher*>(cell);
+ if (void* wrapped = thisObject->wrapped()) {
+ // if (thisObject->scriptExecutionContext())
+ // analyzer.setLabelForCell(cell, "url " + thisObject->scriptExecutionContext()->url().string());
+ }
+ Base::analyzeHeap(cell, analyzer);
+}
+
+JSObject* JSFSWatcher::createPrototype(VM& vm, JSDOMGlobalObject* globalObject)
+{
+ return JSFSWatcherPrototype::create(vm, globalObject, JSFSWatcherPrototype::createStructure(vm, globalObject, globalObject->objectPrototype()));
+}
+
+extern "C" EncodedJSValue FSWatcher__create(Zig::GlobalObject* globalObject, void* ptr)
+{
+ auto& vm = globalObject->vm();
+ JSC::Structure* structure = globalObject->JSFSWatcherStructure();
+ JSFSWatcher* instance = JSFSWatcher::create(vm, globalObject, structure, ptr);
+
+ return JSValue::encode(instance);
+}
+
+template<typename Visitor>
+void JSFSWatcher::visitChildrenImpl(JSCell* cell, Visitor& visitor)
+{
+ JSFSWatcher* thisObject = jsCast<JSFSWatcher*>(cell);
+ ASSERT_GC_OBJECT_INHERITS(thisObject, info());
+ Base::visitChildren(thisObject, visitor);
+ visitor.append(thisObject->m_listener);
+}
+
+DEFINE_VISIT_CHILDREN(JSFSWatcher);
+
+template<typename Visitor>
+void JSFSWatcher::visitAdditionalChildren(Visitor& visitor)
+{
+ JSFSWatcher* thisObject = this;
+ ASSERT_GC_OBJECT_INHERITS(thisObject, info());
+ visitor.append(thisObject->m_listener);
+}
+
+DEFINE_VISIT_ADDITIONAL_CHILDREN(JSFSWatcher);
+
+template<typename Visitor>
+void JSFSWatcher::visitOutputConstraintsImpl(JSCell* cell, Visitor& visitor)
+{
+ JSFSWatcher* thisObject = jsCast<JSFSWatcher*>(cell);
+ ASSERT_GC_OBJECT_INHERITS(thisObject, info());
+ thisObject->visitAdditionalChildren<Visitor>(visitor);
+}
+
+DEFINE_VISIT_OUTPUT_CONSTRAINTS(JSFSWatcher);
class JSFileSystemRouterPrototype final : public JSC::JSNonFinalObject {
public:
using Base = JSC::JSNonFinalObject;
@@ -7654,6 +7945,9 @@ JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__utimesCallback);
extern "C" EncodedJSValue NodeJSFSPrototype__utimesSync(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__utimesSyncCallback);
+extern "C" EncodedJSValue NodeJSFSPrototype__watch(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
+JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__watchCallback);
+
extern "C" EncodedJSValue NodeJSFSPrototype__write(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame);
JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__writeCallback);
@@ -7751,6 +8045,7 @@ static const HashTableValue JSNodeJSFSPrototypeTableValues[] = {
{ "unlinkSync"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__unlinkSyncCallback, 1 } },
{ "utimes"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__utimesCallback, 4 } },
{ "utimesSync"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__utimesSyncCallback, 3 } },
+ { "watch"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__watchCallback, 3 } },
{ "write"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__writeCallback, 6 } },
{ "writeFile"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__writeFileCallback, 4 } },
{ "writeFileSync"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__writeFileSyncCallback, 3 } },
@@ -9795,6 +10090,33 @@ JSC_DEFINE_HOST_FUNCTION(NodeJSFSPrototype__utimesSyncCallback, (JSGlobalObject
return NodeJSFSPrototype__utimesSync(thisObject->wrapped(), lexicalGlobalObject, callFrame);
}
+JSC_DEFINE_HOST_FUNCTION(NodeJSFSPrototype__watchCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
+{
+ auto& vm = lexicalGlobalObject->vm();
+
+ JSNodeJSFS* thisObject = jsDynamicCast<JSNodeJSFS*>(callFrame->thisValue());
+
+ if (UNLIKELY(!thisObject)) {
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
+ return throwVMTypeError(lexicalGlobalObject, throwScope);
+ }
+
+ JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject);
+
+#ifdef BUN_DEBUG
+ /** View the file name of the JS file that called this function
+ * from a debugger */
+ SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm);
+ const char* fileName = sourceOrigin.string().utf8().data();
+ static const char* lastFileName = nullptr;
+ if (lastFileName != fileName) {
+ lastFileName = fileName;
+ }
+#endif
+
+ return NodeJSFSPrototype__watch(thisObject->wrapped(), lexicalGlobalObject, callFrame);
+}
+
JSC_DEFINE_HOST_FUNCTION(NodeJSFSPrototype__writeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
{
auto& vm = lexicalGlobalObject->vm();
diff --git a/src/bun.js/bindings/ZigGeneratedClasses.h b/src/bun.js/bindings/ZigGeneratedClasses.h
index 668cd3f6b..3fa0e26d2 100644
--- a/src/bun.js/bindings/ZigGeneratedClasses.h
+++ b/src/bun.js/bindings/ZigGeneratedClasses.h
@@ -578,6 +578,62 @@ public:
mutable JSC::WriteBarrier<JSC::Unknown> m_testValue;
};
+class JSFSWatcher final : public JSC::JSDestructibleObject {
+public:
+ using Base = JSC::JSDestructibleObject;
+ static JSFSWatcher* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx);
+
+ DECLARE_EXPORT_INFO;
+ template<typename, JSC::SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
+ {
+ if constexpr (mode == JSC::SubspaceAccess::Concurrently)
+ return nullptr;
+ return WebCore::subspaceForImpl<JSFSWatcher, WebCore::UseCustomHeapCellType::No>(
+ vm,
+ [](auto& spaces) { return spaces.m_clientSubspaceForFSWatcher.get(); },
+ [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForFSWatcher = std::forward<decltype(space)>(space); },
+ [](auto& spaces) { return spaces.m_subspaceForFSWatcher.get(); },
+ [](auto& spaces, auto&& space) { spaces.m_subspaceForFSWatcher = std::forward<decltype(space)>(space); });
+ }
+
+ static void destroy(JSC::JSCell*);
+ static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
+ {
+ return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(static_cast<JSC::JSType>(0b11101110), StructureFlags), info());
+ }
+
+ static JSObject* createPrototype(VM& vm, JSDOMGlobalObject* globalObject);
+ ;
+
+ ~JSFSWatcher();
+
+ void* wrapped() const { return m_ctx; }
+
+ void detach()
+ {
+ m_ctx = nullptr;
+ }
+
+ static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&);
+ static ptrdiff_t offsetOfWrapped() { return OBJECT_OFFSETOF(JSFSWatcher, m_ctx); }
+
+ void* m_ctx { nullptr };
+
+ JSFSWatcher(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr)
+ : Base(vm, structure)
+ {
+ m_ctx = sinkPtr;
+ }
+
+ void finishCreation(JSC::VM&);
+
+ DECLARE_VISIT_CHILDREN;
+ template<typename Visitor> void visitAdditionalChildren(Visitor&);
+ DECLARE_VISIT_OUTPUT_CONSTRAINTS;
+
+ mutable JSC::WriteBarrier<JSC::Unknown> m_listener;
+};
+
class JSFileSystemRouter final : public JSC::JSDestructibleObject {
public:
using Base = JSC::JSDestructibleObject;
diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig
index 0ec65a469..74e30cd83 100644
--- a/src/bun.js/bindings/generated_classes.zig
+++ b/src/bun.js/bindings/generated_classes.zig
@@ -1406,6 +1406,96 @@ pub const JSExpectStringMatching = struct {
}
}
};
+pub const JSFSWatcher = struct {
+ const FSWatcher = Classes.FSWatcher;
+ const GetterType = fn (*FSWatcher, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue;
+ const GetterTypeWithThisValue = fn (*FSWatcher, JSC.JSValue, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue;
+ const SetterType = fn (*FSWatcher, *JSC.JSGlobalObject, JSC.JSValue) callconv(.C) bool;
+ const SetterTypeWithThisValue = fn (*FSWatcher, JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) callconv(.C) bool;
+ const CallbackType = fn (*FSWatcher, *JSC.JSGlobalObject, *JSC.CallFrame) callconv(.C) JSC.JSValue;
+
+ /// Return the pointer to the wrapped object.
+ /// If the object does not match the type, return null.
+ pub fn fromJS(value: JSC.JSValue) ?*FSWatcher {
+ JSC.markBinding(@src());
+ return FSWatcher__fromJS(value);
+ }
+
+ extern fn FSWatcherPrototype__listenerSetCachedValue(JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) void;
+
+ extern fn FSWatcherPrototype__listenerGetCachedValue(JSC.JSValue) JSC.JSValue;
+
+ /// `FSWatcher.listener` setter
+ /// This value will be visited by the garbage collector.
+ pub fn listenerSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void {
+ JSC.markBinding(@src());
+ FSWatcherPrototype__listenerSetCachedValue(thisValue, globalObject, value);
+ }
+
+ /// `FSWatcher.listener` getter
+ /// This value will be visited by the garbage collector.
+ pub fn listenerGetCached(thisValue: JSC.JSValue) ?JSC.JSValue {
+ JSC.markBinding(@src());
+ const result = FSWatcherPrototype__listenerGetCachedValue(thisValue);
+ if (result == .zero)
+ return null;
+
+ return result;
+ }
+
+ /// Create a new instance of FSWatcher
+ pub fn toJS(this: *FSWatcher, globalObject: *JSC.JSGlobalObject) JSC.JSValue {
+ JSC.markBinding(@src());
+ if (comptime Environment.allow_assert) {
+ const value__ = FSWatcher__create(globalObject, this);
+ std.debug.assert(value__.as(FSWatcher).? == this); // If this fails, likely a C ABI issue.
+ return value__;
+ } else {
+ return FSWatcher__create(globalObject, this);
+ }
+ }
+
+ /// Modify the internal ptr to point to a new instance of FSWatcher.
+ pub fn dangerouslySetPtr(value: JSC.JSValue, ptr: ?*FSWatcher) bool {
+ JSC.markBinding(@src());
+ return FSWatcher__dangerouslySetPtr(value, ptr);
+ }
+
+ /// Detach the ptr from the thisValue
+ pub fn detachPtr(_: *FSWatcher, value: JSC.JSValue) void {
+ JSC.markBinding(@src());
+ std.debug.assert(FSWatcher__dangerouslySetPtr(value, null));
+ }
+
+ extern fn FSWatcher__fromJS(JSC.JSValue) ?*FSWatcher;
+ extern fn FSWatcher__getConstructor(*JSC.JSGlobalObject) JSC.JSValue;
+
+ extern fn FSWatcher__create(globalObject: *JSC.JSGlobalObject, ptr: ?*FSWatcher) JSC.JSValue;
+
+ extern fn FSWatcher__dangerouslySetPtr(JSC.JSValue, ?*FSWatcher) bool;
+
+ comptime {
+ if (@TypeOf(FSWatcher.finalize) != (fn (*FSWatcher) callconv(.C) void)) {
+ @compileLog("FSWatcher.finalize is not a finalizer");
+ }
+
+ if (@TypeOf(FSWatcher.doClose) != CallbackType)
+ @compileLog("Expected FSWatcher.doClose to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.doClose)));
+ if (@TypeOf(FSWatcher.hasRef) != CallbackType)
+ @compileLog("Expected FSWatcher.hasRef to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.hasRef)));
+ if (@TypeOf(FSWatcher.doRef) != CallbackType)
+ @compileLog("Expected FSWatcher.doRef to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.doRef)));
+ if (@TypeOf(FSWatcher.doUnref) != CallbackType)
+ @compileLog("Expected FSWatcher.doUnref to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.doUnref)));
+ if (!JSC.is_bindgen) {
+ @export(FSWatcher.doClose, .{ .name = "FSWatcherPrototype__doClose" });
+ @export(FSWatcher.doRef, .{ .name = "FSWatcherPrototype__doRef" });
+ @export(FSWatcher.doUnref, .{ .name = "FSWatcherPrototype__doUnref" });
+ @export(FSWatcher.finalize, .{ .name = "FSWatcherClass__finalize" });
+ @export(FSWatcher.hasRef, .{ .name = "FSWatcherPrototype__hasRef" });
+ }
+ }
+};
pub const JSFileSystemRouter = struct {
const FileSystemRouter = Classes.FileSystemRouter;
const GetterType = fn (*FileSystemRouter, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue;
@@ -2312,6 +2402,8 @@ pub const JSNodeJSFS = struct {
@compileLog("Expected NodeJSFS.utimes to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.utimes)));
if (@TypeOf(NodeJSFS.utimesSync) != CallbackType)
@compileLog("Expected NodeJSFS.utimesSync to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.utimesSync)));
+ if (@TypeOf(NodeJSFS.watch) != CallbackType)
+ @compileLog("Expected NodeJSFS.watch to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.watch)));
if (@TypeOf(NodeJSFS.write) != CallbackType)
@compileLog("Expected NodeJSFS.write to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.write)));
if (@TypeOf(NodeJSFS.writeFile) != CallbackType)
@@ -2402,6 +2494,7 @@ pub const JSNodeJSFS = struct {
@export(NodeJSFS.unlinkSync, .{ .name = "NodeJSFSPrototype__unlinkSync" });
@export(NodeJSFS.utimes, .{ .name = "NodeJSFSPrototype__utimes" });
@export(NodeJSFS.utimesSync, .{ .name = "NodeJSFSPrototype__utimesSync" });
+ @export(NodeJSFS.watch, .{ .name = "NodeJSFSPrototype__watch" });
@export(NodeJSFS.write, .{ .name = "NodeJSFSPrototype__write" });
@export(NodeJSFS.writeFile, .{ .name = "NodeJSFSPrototype__writeFile" });
@export(NodeJSFS.writeFileSync, .{ .name = "NodeJSFSPrototype__writeFileSync" });
@@ -4855,6 +4948,7 @@ comptime {
_ = JSExpectAnything;
_ = JSExpectStringContaining;
_ = JSExpectStringMatching;
+ _ = JSFSWatcher;
_ = JSFileSystemRouter;
_ = JSListener;
_ = JSMD4;
diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig
index c54965093..d90267337 100644
--- a/src/bun.js/bindings/generated_classes_list.zig
+++ b/src/bun.js/bindings/generated_classes_list.zig
@@ -37,4 +37,5 @@ pub const Classes = struct {
pub const BuildArtifact = JSC.API.BuildArtifact;
pub const BuildMessage = JSC.BuildMessage;
pub const ResolveMessage = JSC.ResolveMessage;
+ pub const FSWatcher = JSC.Node.FSWatcher.JSObject;
};
diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig
index 0a3459d64..a3ccd16ad 100644
--- a/src/bun.js/event_loop.zig
+++ b/src/bun.js/event_loop.zig
@@ -224,6 +224,7 @@ pub const CppTask = opaque {
const ThreadSafeFunction = JSC.napi.ThreadSafeFunction;
const MicrotaskForDefaultGlobalObject = JSC.MicrotaskForDefaultGlobalObject;
const HotReloadTask = JSC.HotReloader.HotReloadTask;
+const FSWatchTask = JSC.Node.FSWatcher.FSWatchTask;
const PollPendingModulesTask = JSC.ModuleLoader.AsyncModule.Queue;
// const PromiseTask = JSInternalPromise.Completion.PromiseTask;
const GetAddrInfoRequestTask = JSC.DNS.GetAddrInfoRequest.Task;
@@ -242,6 +243,7 @@ pub const Task = TaggedPointerUnion(.{
HotReloadTask,
PollPendingModulesTask,
GetAddrInfoRequestTask,
+ FSWatchTask,
// PromiseTask,
// TimeoutTasklet,
});
@@ -467,6 +469,11 @@ pub const EventLoop = struct {
// special case: we return
return 0;
},
+ .FSWatchTask => {
+ var transform_task: *FSWatchTask = task.get(FSWatchTask).?;
+ transform_task.*.run();
+ transform_task.deinit();
+ },
@field(Task.Tag, typeBaseName(@typeName(AnyTask))) => {
var any: *AnyTask = task.get(AnyTask).?;
any.run();
diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig
index bebfbeb18..3baa25e22 100644
--- a/src/bun.js/javascript.zig
+++ b/src/bun.js/javascript.zig
@@ -2609,6 +2609,13 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime
return this.tombstones.get(key);
}
+ pub fn onError(
+ _: *@This(),
+ err: anyerror,
+ ) void {
+ Output.prettyErrorln("<r>Watcher crashed: <red><b>{s}<r>", .{@errorName(err)});
+ }
+
pub fn onFileUpdate(
this: *@This(),
events: []watcher.WatchEvent,
diff --git a/src/bun.js/node/fs_events.zig b/src/bun.js/node/fs_events.zig
new file mode 100644
index 000000000..a3fba5441
--- /dev/null
+++ b/src/bun.js/node/fs_events.zig
@@ -0,0 +1,609 @@
+const std = @import("std");
+const bun = @import("root").bun;
+const Environment = bun.Environment;
+const Mutex = @import("../../lock.zig").Lock;
+const sync = @import("../../sync.zig");
+const Semaphore = sync.Semaphore;
+const UnboundedQueue = @import("../unbounded_queue.zig").UnboundedQueue;
+const TaggedPointerUnion = @import("../../tagged_pointer.zig").TaggedPointerUnion;
+const string = bun.string;
+
+pub const CFAbsoluteTime = f64;
+pub const CFTimeInterval = f64;
+pub const CFArrayCallBacks = anyopaque;
+
+pub const FSEventStreamEventFlags = c_int;
+pub const OSStatus = c_int;
+pub const CFIndex = c_long;
+
+pub const FSEventStreamCreateFlags = u32;
+pub const FSEventStreamEventId = u64;
+
+pub const CFStringEncoding = c_uint;
+
+pub const CFArrayRef = ?*anyopaque;
+pub const CFAllocatorRef = ?*anyopaque;
+pub const CFBundleRef = ?*anyopaque;
+pub const CFDictionaryRef = ?*anyopaque;
+pub const CFRunLoopRef = ?*anyopaque;
+pub const CFRunLoopSourceRef = ?*anyopaque;
+pub const CFStringRef = ?*anyopaque;
+pub const CFTypeRef = ?*anyopaque;
+pub const FSEventStreamRef = ?*anyopaque;
+pub const FSEventStreamCallback = *const fn (FSEventStreamRef, ?*anyopaque, usize, ?*anyopaque, *FSEventStreamEventFlags, *FSEventStreamEventId) callconv(.C) void;
+
+// we only care about info and perform
+pub const CFRunLoopSourceContext = extern struct {
+ version: CFIndex = 0,
+ info: *anyopaque,
+ retain: ?*anyopaque = null,
+ release: ?*anyopaque = null,
+ copyDescription: ?*anyopaque = null,
+ equal: ?*anyopaque = null,
+ hash: ?*anyopaque = null,
+ schedule: ?*anyopaque = null,
+ cancel: ?*anyopaque = null,
+ perform: *const fn (?*anyopaque) callconv(.C) void,
+};
+
+pub const FSEventStreamContext = extern struct {
+ version: CFIndex = 0,
+ info: ?*anyopaque = null,
+ pad: [3]?*anyopaque = .{ null, null, null },
+};
+
+pub const kCFStringEncodingUTF8: CFStringEncoding = 0x8000100;
+pub const noErr: OSStatus = 0;
+
+pub const kFSEventStreamCreateFlagNoDefer: c_int = 2;
+pub const kFSEventStreamCreateFlagFileEvents: c_int = 16;
+
+pub const kFSEventStreamEventFlagEventIdsWrapped: c_int = 8;
+pub const kFSEventStreamEventFlagHistoryDone: c_int = 16;
+pub const kFSEventStreamEventFlagItemChangeOwner: c_int = 0x4000;
+pub const kFSEventStreamEventFlagItemCreated: c_int = 0x100;
+pub const kFSEventStreamEventFlagItemFinderInfoMod: c_int = 0x2000;
+pub const kFSEventStreamEventFlagItemInodeMetaMod: c_int = 0x400;
+pub const kFSEventStreamEventFlagItemIsDir: c_int = 0x20000;
+pub const kFSEventStreamEventFlagItemModified: c_int = 0x1000;
+pub const kFSEventStreamEventFlagItemRemoved: c_int = 0x200;
+pub const kFSEventStreamEventFlagItemRenamed: c_int = 0x800;
+pub const kFSEventStreamEventFlagItemXattrMod: c_int = 0x8000;
+pub const kFSEventStreamEventFlagKernelDropped: c_int = 4;
+pub const kFSEventStreamEventFlagMount: c_int = 64;
+pub const kFSEventStreamEventFlagRootChanged: c_int = 32;
+pub const kFSEventStreamEventFlagUnmount: c_int = 128;
+pub const kFSEventStreamEventFlagUserDropped: c_int = 2;
+
+// Lazy function call binding.
+const RTLD_LAZY = 0x1;
+// Symbols exported from this image (dynamic library or bundle)
+// are generally hidden and only availble to dlsym() when
+// directly using the handle returned by this call to dlopen().
+const RTLD_LOCAL = 0x4;
+
+pub const kFSEventsModified: c_int =
+ kFSEventStreamEventFlagItemChangeOwner |
+ kFSEventStreamEventFlagItemFinderInfoMod |
+ kFSEventStreamEventFlagItemInodeMetaMod |
+ kFSEventStreamEventFlagItemModified |
+ kFSEventStreamEventFlagItemXattrMod;
+
+pub const kFSEventsRenamed: c_int =
+ kFSEventStreamEventFlagItemCreated |
+ kFSEventStreamEventFlagItemRemoved |
+ kFSEventStreamEventFlagItemRenamed;
+
+pub const kFSEventsSystem: c_int =
+ kFSEventStreamEventFlagUserDropped |
+ kFSEventStreamEventFlagKernelDropped |
+ kFSEventStreamEventFlagEventIdsWrapped |
+ kFSEventStreamEventFlagHistoryDone |
+ kFSEventStreamEventFlagMount |
+ kFSEventStreamEventFlagUnmount |
+ kFSEventStreamEventFlagRootChanged;
+
+var fsevents_mutex: Mutex = Mutex.init();
+var fsevents_default_loop_mutex: Mutex = Mutex.init();
+var fsevents_default_loop: ?*FSEventsLoop = null;
+
+fn dlsym(handle: ?*anyopaque, comptime Type: type, comptime symbol: [:0]const u8) ?Type {
+ if (std.c.dlsym(handle, symbol)) |ptr| {
+ return bun.cast(Type, ptr);
+ }
+ return null;
+}
+
+pub const CoreFoundation = struct {
+ handle: ?*anyopaque,
+ ArrayCreate: *fn (CFAllocatorRef, [*]?*anyopaque, CFIndex, ?*CFArrayCallBacks) callconv(.C) CFArrayRef,
+ Release: *fn (CFTypeRef) callconv(.C) void,
+
+ RunLoopAddSource: *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void,
+ RunLoopGetCurrent: *fn () callconv(.C) CFRunLoopRef,
+ RunLoopRemoveSource: *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void,
+ RunLoopRun: *fn () callconv(.C) void,
+ RunLoopSourceCreate: *fn (CFAllocatorRef, CFIndex, *CFRunLoopSourceContext) callconv(.C) CFRunLoopSourceRef,
+ RunLoopSourceSignal: *fn (CFRunLoopSourceRef) callconv(.C) void,
+ RunLoopStop: *fn (CFRunLoopRef) callconv(.C) void,
+ RunLoopWakeUp: *fn (CFRunLoopRef) callconv(.C) void,
+ StringCreateWithFileSystemRepresentation: *fn (CFAllocatorRef, [*]const u8) callconv(.C) CFStringRef,
+ RunLoopDefaultMode: *CFStringRef,
+
+ pub fn get() CoreFoundation {
+ if (fsevents_cf) |cf| return cf;
+ fsevents_mutex.lock();
+ defer fsevents_mutex.unlock();
+ if (fsevents_cf) |cf| return cf;
+
+ InitLibrary();
+
+ return fsevents_cf.?;
+ }
+
+ // We Actually never deinit it
+ // pub fn deinit(this: *CoreFoundation) void {
+ // if(this.handle) | ptr| {
+ // this.handle = null;
+ // _ = std.c.dlclose(this.handle);
+ // }
+ // }
+
+};
+
+pub const CoreServices = struct {
+ handle: ?*anyopaque,
+ FSEventStreamCreate: *fn (CFAllocatorRef, FSEventStreamCallback, *FSEventStreamContext, CFArrayRef, FSEventStreamEventId, CFTimeInterval, FSEventStreamCreateFlags) callconv(.C) FSEventStreamRef,
+ FSEventStreamInvalidate: *fn (FSEventStreamRef) callconv(.C) void,
+ FSEventStreamRelease: *fn (FSEventStreamRef) callconv(.C) void,
+ FSEventStreamScheduleWithRunLoop: *fn (FSEventStreamRef, CFRunLoopRef, CFStringRef) callconv(.C) void,
+ FSEventStreamStart: *fn (FSEventStreamRef) callconv(.C) c_int,
+ FSEventStreamStop: *fn (FSEventStreamRef) callconv(.C) void,
+ // libuv set it to -1 so the actual value is this
+ kFSEventStreamEventIdSinceNow: FSEventStreamEventId = 18446744073709551615,
+
+ pub fn get() CoreServices {
+ if (fsevents_cs) |cs| return cs;
+ fsevents_mutex.lock();
+ defer fsevents_mutex.unlock();
+ if (fsevents_cs) |cs| return cs;
+
+ InitLibrary();
+
+ return fsevents_cs.?;
+ }
+
+ // We Actually never deinit it
+ // pub fn deinit(this: *CoreServices) void {
+ // if(this.handle) | ptr| {
+ // this.handle = null;
+ // _ = std.c.dlclose(this.handle);
+ // }
+ // }
+
+};
+
+var fsevents_cf: ?CoreFoundation = null;
+var fsevents_cs: ?CoreServices = null;
+
+fn InitLibrary() void {
+ const fsevents_cf_handle = std.c.dlopen("/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation", RTLD_LAZY | RTLD_LOCAL);
+ if (fsevents_cf_handle == null) @panic("Cannot Load CoreFoundation");
+
+ fsevents_cf = CoreFoundation{
+ .handle = fsevents_cf_handle,
+ .ArrayCreate = dlsym(fsevents_cf_handle, *fn (CFAllocatorRef, [*]?*anyopaque, CFIndex, ?*CFArrayCallBacks) callconv(.C) CFArrayRef, "CFArrayCreate") orelse @panic("Cannot Load CoreFoundation"),
+ .Release = dlsym(fsevents_cf_handle, *fn (CFTypeRef) callconv(.C) void, "CFRelease") orelse @panic("Cannot Load CoreFoundation"),
+ .RunLoopAddSource = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void, "CFRunLoopAddSource") orelse @panic("Cannot Load CoreFoundation"),
+ .RunLoopGetCurrent = dlsym(fsevents_cf_handle, *fn () callconv(.C) CFRunLoopRef, "CFRunLoopGetCurrent") orelse @panic("Cannot Load CoreFoundation"),
+ .RunLoopRemoveSource = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void, "CFRunLoopRemoveSource") orelse @panic("Cannot Load CoreFoundation"),
+ .RunLoopRun = dlsym(fsevents_cf_handle, *fn () callconv(.C) void, "CFRunLoopRun") orelse @panic("Cannot Load CoreFoundation"),
+ .RunLoopSourceCreate = dlsym(fsevents_cf_handle, *fn (CFAllocatorRef, CFIndex, *CFRunLoopSourceContext) callconv(.C) CFRunLoopSourceRef, "CFRunLoopSourceCreate") orelse @panic("Cannot Load CoreFoundation"),
+ .RunLoopSourceSignal = dlsym(fsevents_cf_handle, *fn (CFRunLoopSourceRef) callconv(.C) void, "CFRunLoopSourceSignal") orelse @panic("Cannot Load CoreFoundation"),
+ .RunLoopStop = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef) callconv(.C) void, "CFRunLoopStop") orelse @panic("Cannot Load CoreFoundation"),
+ .RunLoopWakeUp = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef) callconv(.C) void, "CFRunLoopWakeUp") orelse @panic("Cannot Load CoreFoundation"),
+ .StringCreateWithFileSystemRepresentation = dlsym(fsevents_cf_handle, *fn (CFAllocatorRef, [*]const u8) callconv(.C) CFStringRef, "CFStringCreateWithFileSystemRepresentation") orelse @panic("Cannot Load CoreFoundation"),
+ .RunLoopDefaultMode = dlsym(fsevents_cf_handle, *CFStringRef, "kCFRunLoopDefaultMode") orelse @panic("Cannot Load CoreFoundation"),
+ };
+
+ const fsevents_cs_handle = std.c.dlopen("/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices", RTLD_LAZY | RTLD_LOCAL);
+ if (fsevents_cs_handle == null) @panic("Cannot Load CoreServices");
+
+ fsevents_cs = CoreServices{
+ .handle = fsevents_cs_handle,
+ .FSEventStreamCreate = dlsym(fsevents_cs_handle, *fn (CFAllocatorRef, FSEventStreamCallback, *FSEventStreamContext, CFArrayRef, FSEventStreamEventId, CFTimeInterval, FSEventStreamCreateFlags) callconv(.C) FSEventStreamRef, "FSEventStreamCreate") orelse @panic("Cannot Load CoreServices"),
+ .FSEventStreamInvalidate = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) void, "FSEventStreamInvalidate") orelse @panic("Cannot Load CoreServices"),
+ .FSEventStreamRelease = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) void, "FSEventStreamRelease") orelse @panic("Cannot Load CoreServices"),
+ .FSEventStreamScheduleWithRunLoop = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef, CFRunLoopRef, CFStringRef) callconv(.C) void, "FSEventStreamScheduleWithRunLoop") orelse @panic("Cannot Load CoreServices"),
+ .FSEventStreamStart = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) c_int, "FSEventStreamStart") orelse @panic("Cannot Load CoreServices"),
+ .FSEventStreamStop = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) void, "FSEventStreamStop") orelse @panic("Cannot Load CoreServices"),
+ };
+}
+
+pub const FSEventsLoop = struct {
+ signal_source: CFRunLoopSourceRef,
+ mutex: Mutex,
+ loop: CFRunLoopRef = null,
+ sem: Semaphore,
+ thread: std.Thread = undefined,
+ tasks: ConcurrentTask.Queue = ConcurrentTask.Queue{},
+ watchers: bun.BabyList(?*FSEventsWatcher) = .{},
+ watcher_count: u32 = 0,
+ fsevent_stream: FSEventStreamRef = null,
+ paths: ?[]?*anyopaque = null,
+ cf_paths: CFArrayRef = null,
+ has_scheduled_watchers: bool = false,
+
+ pub const Task = struct {
+ ctx: ?*anyopaque,
+ callback: *const (fn (*anyopaque) void),
+
+ pub fn run(this: *Task) void {
+ var callback = this.callback;
+ var ctx = this.ctx;
+ callback(ctx.?);
+ }
+
+ pub fn New(comptime Type: type, comptime Callback: anytype) type {
+ return struct {
+ pub fn init(ctx: *Type) Task {
+ return Task{
+ .callback = wrap,
+ .ctx = ctx,
+ };
+ }
+
+ pub fn wrap(this: ?*anyopaque) void {
+ @call(.always_inline, Callback, .{@ptrCast(*Type, @alignCast(@alignOf(Type), this.?))});
+ }
+ };
+ }
+ };
+
+ pub const ConcurrentTask = struct {
+ task: Task = undefined,
+ next: ?*ConcurrentTask = null,
+ auto_delete: bool = false,
+
+ pub const Queue = UnboundedQueue(ConcurrentTask, .next);
+
+ pub fn from(this: *ConcurrentTask, task: Task) *ConcurrentTask {
+ this.* = .{
+ .task = task,
+ .next = null,
+ };
+ return this;
+ }
+ };
+
+ pub fn CFThreadLoop(this: *FSEventsLoop) void {
+ bun.Output.Source.configureNamedThread("CFThreadLoop");
+
+ const CF = CoreFoundation.get();
+
+ this.loop = CF.RunLoopGetCurrent();
+
+ CF.RunLoopAddSource(this.loop, this.signal_source, CF.RunLoopDefaultMode.*);
+
+ this.sem.post();
+
+ CF.RunLoopRun();
+ CF.RunLoopRemoveSource(this.loop, this.signal_source, CF.RunLoopDefaultMode.*);
+
+ this.loop = null;
+ }
+
+ // Runs in CF thread, executed after `enqueueTaskConcurrent()`
+ fn CFLoopCallback(arg: ?*anyopaque) callconv(.C) void {
+ if (arg) |self| {
+ const this = bun.cast(*FSEventsLoop, self);
+
+ var concurrent = this.tasks.popBatch();
+ const count = concurrent.count;
+ if (count == 0)
+ return;
+
+ var iter = concurrent.iterator();
+ while (iter.next()) |task| {
+ task.task.run();
+ if (task.auto_delete) bun.default_allocator.destroy(task);
+ }
+ }
+ }
+
+ pub fn init() !*FSEventsLoop {
+ const this = bun.default_allocator.create(FSEventsLoop) catch unreachable;
+
+ const CF = CoreFoundation.get();
+
+ var ctx = CFRunLoopSourceContext{
+ .info = this,
+ .perform = CFLoopCallback,
+ };
+
+ const signal_source = CF.RunLoopSourceCreate(null, 0, &ctx);
+ if (signal_source == null) {
+ return error.FailedToCreateCoreFoudationSourceLoop;
+ }
+
+ var fs_loop = FSEventsLoop{ .sem = Semaphore.init(0), .mutex = Mutex.init(), .signal_source = signal_source };
+
+ this.* = fs_loop;
+ this.thread = try std.Thread.spawn(.{}, FSEventsLoop.CFThreadLoop, .{this});
+
+ // sync threads
+ this.sem.wait();
+ return this;
+ }
+
+ fn enqueueTaskConcurrent(this: *FSEventsLoop, task: Task) void {
+ const CF = CoreFoundation.get();
+ var concurrent = bun.default_allocator.create(ConcurrentTask) catch unreachable;
+ concurrent.auto_delete = true;
+ this.tasks.push(concurrent.from(task));
+ CF.RunLoopSourceSignal(this.signal_source);
+ CF.RunLoopWakeUp(this.loop);
+ }
+
+ // Runs in CF thread, when there're events in FSEventStream
+ fn _events_cb(_: FSEventStreamRef, info: ?*anyopaque, numEvents: usize, eventPaths: ?*anyopaque, eventFlags: *FSEventStreamEventFlags, _: *FSEventStreamEventId) callconv(.C) void {
+ const paths_ptr = bun.cast([*][*:0]const u8, eventPaths);
+ const paths = paths_ptr[0..numEvents];
+ var loop = bun.cast(*FSEventsLoop, info);
+ const event_flags = bun.cast([*]FSEventStreamEventFlags, eventFlags);
+
+ for (loop.watchers.slice()) |watcher| {
+ if (watcher) |handle| {
+ for (paths, 0..) |path_ptr, i| {
+ var flags = event_flags[i];
+ var path = path_ptr[0..bun.len(path_ptr)];
+ // Filter out paths that are outside handle's request
+ if (path.len < handle.path.len or !bun.strings.startsWith(path, handle.path)) {
+ continue;
+ }
+ const is_file = (flags & kFSEventStreamEventFlagItemIsDir) == 0;
+
+ // Remove common prefix, unless the watched folder is "/"
+ if (!(handle.path.len == 1 and handle.path[0] == '/')) {
+ path = path[handle.path.len..];
+
+ // Ignore events with path equal to directory itself
+ if (path.len <= 1 and is_file) {
+ continue;
+ }
+ if (path.len == 0) {
+ // Since we're using fsevents to watch the file itself, path == handle.path, and we now need to get the basename of the file back
+ while (path.len > 0) {
+ if (bun.strings.startsWithChar(path, '/')) {
+ path = path[1..];
+ break;
+ } else {
+ path = path[1..];
+ }
+ }
+
+ // Created and Removed seem to be always set, but don't make sense
+ flags &= ~kFSEventsRenamed;
+ } else {
+ // Skip forward slash
+ path = path[1..];
+ }
+ }
+
+ // Do not emit events from subdirectories (without option set)
+ if (path.len == 0 or (bun.strings.containsChar(path, '/') and !handle.recursive)) {
+ continue;
+ }
+
+ var is_rename = true;
+
+ if ((flags & kFSEventsRenamed) == 0) {
+ if ((flags & kFSEventsModified) != 0 or is_file) {
+ is_rename = false;
+ }
+ }
+
+ handle.callback(handle.ctx, path, is_file, is_rename);
+ }
+ }
+ }
+ }
+
+ // Runs on CF Thread
+ pub fn _schedule(this: *FSEventsLoop) void {
+ this.mutex.lock();
+ defer this.mutex.unlock();
+ this.has_scheduled_watchers = false;
+
+ var watchers = this.watchers.slice();
+
+ const CF = CoreFoundation.get();
+ const CS = CoreServices.get();
+
+ if (this.fsevent_stream) |stream| {
+ // Stop emitting events
+ CS.FSEventStreamStop(stream);
+
+ // Release stream
+ CS.FSEventStreamInvalidate(stream);
+ CS.FSEventStreamRelease(stream);
+ this.fsevent_stream = null;
+ }
+ // clean old paths
+ if (this.paths) |p| {
+ this.paths = null;
+ bun.default_allocator.destroy(p);
+ }
+ if (this.cf_paths) |cf| {
+ this.cf_paths = null;
+ CF.Release(cf);
+ }
+
+ const paths = bun.default_allocator.alloc(?*anyopaque, this.watcher_count) catch unreachable;
+ var count: u32 = 0;
+ for (watchers) |w| {
+ if (w) |watcher| {
+ const path = CF.StringCreateWithFileSystemRepresentation(null, watcher.path.ptr);
+ paths[count] = path;
+ count += 1;
+ }
+ }
+
+ const cf_paths = CF.ArrayCreate(null, paths.ptr, count, null);
+ var ctx: FSEventStreamContext = .{
+ .info = this,
+ };
+
+ const latency: CFAbsoluteTime = 0.05;
+ // Explanation of selected flags:
+ // 1. NoDefer - without this flag, events that are happening continuously
+ // (i.e. each event is happening after time interval less than `latency`,
+ // counted from previous event), will be deferred and passed to callback
+ // once they'll either fill whole OS buffer, or when this continuous stream
+ // will stop (i.e. there'll be delay between events, bigger than
+ // `latency`).
+ // Specifying this flag will invoke callback after `latency` time passed
+ // since event.
+ // 2. FileEvents - fire callback for file changes too (by default it is firing
+ // it only for directory changes).
+ //
+ const flags: FSEventStreamCreateFlags = kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents;
+
+ //
+ // NOTE: It might sound like a good idea to remember last seen StreamEventId,
+ // but in reality one dir might have last StreamEventId less than, the other,
+ // that is being watched now. Which will cause FSEventStream API to report
+ // changes to files from the past.
+ //
+ const ref = CS.FSEventStreamCreate(null, _events_cb, &ctx, cf_paths, CS.kFSEventStreamEventIdSinceNow, latency, flags);
+
+ CS.FSEventStreamScheduleWithRunLoop(ref, this.loop, CF.RunLoopDefaultMode.*);
+ if (CS.FSEventStreamStart(ref) == 0) {
+ //clean in case of failure
+ bun.default_allocator.destroy(paths);
+ CF.Release(cf_paths);
+ CS.FSEventStreamInvalidate(ref);
+ CS.FSEventStreamRelease(ref);
+ return;
+ }
+ this.fsevent_stream = ref;
+ this.paths = paths;
+ this.cf_paths = cf_paths;
+ }
+
+ fn registerWatcher(this: *FSEventsLoop, watcher: *FSEventsWatcher) void {
+ this.mutex.lock();
+ defer this.mutex.unlock();
+ if (this.watcher_count == this.watchers.len) {
+ this.watcher_count += 1;
+ this.watchers.push(bun.default_allocator, watcher) catch unreachable;
+ } else {
+ var watchers = this.watchers.slice();
+ for (watchers, 0..) |w, i| {
+ if (w == null) {
+ watchers[i] = watcher;
+ this.watcher_count += 1;
+ break;
+ }
+ }
+ }
+
+ if (this.has_scheduled_watchers == false) {
+ this.has_scheduled_watchers = true;
+ this.enqueueTaskConcurrent(Task.New(FSEventsLoop, _schedule).init(this));
+ }
+ }
+
+ fn unregisterWatcher(this: *FSEventsLoop, watcher: *FSEventsWatcher) void {
+ this.mutex.lock();
+ defer this.mutex.unlock();
+ var watchers = this.watchers.slice();
+ for (watchers, 0..) |w, i| {
+ if (w) |item| {
+ if (item == watcher) {
+ watchers[i] = null;
+ // if is the last one just pop
+ if (i == watchers.len - 1) {
+ this.watchers.len -= 1;
+ }
+ this.watcher_count -= 1;
+ break;
+ }
+ }
+ }
+ }
+
+ // Runs on CF loop to close the loop
+ fn _stop(this: *FSEventsLoop) void {
+ const CF = CoreFoundation.get();
+ CF.RunLoopStop(this.loop);
+ }
+ fn deinit(this: *FSEventsLoop) void {
+ // signal close and wait
+ this.enqueueTaskConcurrent(Task.New(FSEventsLoop, FSEventsLoop._stop).init(this));
+ this.thread.join();
+ const CF = CoreFoundation.get();
+
+ CF.Release(this.signal_source);
+ this.signal_source = null;
+
+ this.sem.deinit();
+ this.mutex.deinit();
+ if (this.watcher_count > 0) {
+ while (this.watchers.popOrNull()) |watcher| {
+ if (watcher) |w| {
+ // unlink watcher
+ w.loop = null;
+ }
+ }
+ }
+
+ this.watchers.deinitWithAllocator(bun.default_allocator);
+
+ bun.default_allocator.destroy(this);
+ }
+};
+
+pub const FSEventsWatcher = struct {
+ path: string,
+ callback: Callback,
+ loop: ?*FSEventsLoop,
+ recursive: bool,
+ ctx: ?*anyopaque,
+
+ const Callback = *const fn (ctx: ?*anyopaque, path: string, is_file: bool, is_rename: bool) void;
+
+ pub fn init(loop: *FSEventsLoop, path: string, recursive: bool, callback: Callback, ctx: ?*anyopaque) *FSEventsWatcher {
+ var this = bun.default_allocator.create(FSEventsWatcher) catch unreachable;
+ this.* = FSEventsWatcher{
+ .path = path,
+ .callback = callback,
+ .loop = loop,
+ .recursive = recursive,
+ .ctx = ctx,
+ };
+
+ loop.registerWatcher(this);
+ return this;
+ }
+
+ pub fn deinit(this: *FSEventsWatcher) void {
+ if (this.loop) |loop| {
+ loop.unregisterWatcher(this);
+ }
+ bun.default_allocator.destroy(this);
+ }
+};
+
+pub fn watch(path: string, recursive: bool, callback: FSEventsWatcher.Callback, ctx: ?*anyopaque) !*FSEventsWatcher {
+ if (fsevents_default_loop) |loop| {
+ return FSEventsWatcher.init(loop, path, recursive, callback, ctx);
+ } else {
+ fsevents_default_loop_mutex.lock();
+ defer fsevents_default_loop_mutex.unlock();
+ if (fsevents_default_loop == null) {
+ fsevents_default_loop = try FSEventsLoop.init();
+ }
+ return FSEventsWatcher.init(fsevents_default_loop.?, path, recursive, callback, ctx);
+ }
+}
diff --git a/src/bun.js/node/node.classes.ts b/src/bun.js/node/node.classes.ts
index f984077e4..ce35c940a 100644
--- a/src/bun.js/node/node.classes.ts
+++ b/src/bun.js/node/node.classes.ts
@@ -2,6 +2,34 @@ import { define } from "../scripts/class-definitions";
export default [
define({
+ name: "FSWatcher",
+ construct: false,
+ noConstructor: true,
+ finalize: true,
+ configurable: false,
+ klass: {},
+ JSType: "0b11101110",
+ proto: {
+ ref: {
+ fn: "doRef",
+ length: 0,
+ },
+ unref: {
+ fn: "doUnref",
+ length: 0,
+ },
+ hasRef: {
+ fn: "hasRef",
+ length: 0,
+ },
+ close: {
+ fn: "doClose",
+ length: 0,
+ },
+ },
+ values: ["listener"],
+ }),
+ define({
name: "Timeout",
construct: false,
noConstructor: true,
@@ -300,7 +328,7 @@ export default [
utimes: { fn: "utimes", length: 4 },
utimesSync: { fn: "utimesSync", length: 3 },
// TODO:
- // watch: { fn: "watch", length: 3 },
+ watch: { fn: "watch", length: 3 },
// watchFile: { fn: "watchFile", length: 3 },
writeFile: { fn: "writeFile", length: 4 },
writeFileSync: { fn: "writeFileSync", length: 3 },
diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig
index 3ea0822e6..21a65251a 100644
--- a/src/bun.js/node/node_fs.zig
+++ b/src/bun.js/node/node_fs.zig
@@ -34,7 +34,6 @@ const Mode = JSC.Node.Mode;
const uid_t = std.os.uid_t;
const gid_t = std.os.gid_t;
-
/// u63 to allow one null bit
const ReadPosition = u63;
@@ -2313,7 +2312,7 @@ pub const Arguments = struct {
};
pub const UnwatchFile = void;
- pub const Watch = void;
+ pub const Watch = JSC.Node.FSWatcher.Arguments;
pub const WatchFile = void;
pub const Fsync = struct {
fd: FileDescriptor,
@@ -2475,7 +2474,7 @@ const Return = struct {
pub const Truncate = void;
pub const Unlink = void;
pub const UnwatchFile = void;
- pub const Watch = void;
+ pub const Watch = JSC.JSValue;
pub const WatchFile = void;
pub const Utimes = void;
@@ -4181,8 +4180,12 @@ pub const NodeFS = struct {
return Maybe(Return.Lutimes).todo;
}
- pub fn watch(_: *NodeFS, _: Arguments.Watch, comptime _: Flavor) Maybe(Return.Watch) {
- return Maybe(Return.Watch).todo;
+ pub fn watch(_: *NodeFS, args: Arguments.Watch, comptime _: Flavor) Maybe(Return.Watch) {
+ const watcher = args.createFSWatcher() catch |err| {
+ args.global_this.throwError(err, "Failed to watch filename");
+ return Maybe(Return.Watch){ .result = JSC.JSValue.jsUndefined() };
+ };
+ return Maybe(Return.Watch){ .result = watcher };
}
pub fn createReadStream(_: *NodeFS, _: Arguments.CreateReadStream, comptime _: Flavor) Maybe(Return.CreateReadStream) {
return Maybe(Return.CreateReadStream).todo;
diff --git a/src/bun.js/node/node_fs_binding.zig b/src/bun.js/node/node_fs_binding.zig
index 74b769bf6..f178f0355 100644
--- a/src/bun.js/node/node_fs_binding.zig
+++ b/src/bun.js/node/node_fs_binding.zig
@@ -241,6 +241,8 @@ pub const NodeJSFS = struct {
return JSC.Node.Stats.getConstructor(globalThis);
}
+ pub const watch = callSync(.watch);
+
// Not implemented yet:
const notimpl = fdatasync;
pub const opendir = notimpl;
diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig
new file mode 100644
index 000000000..397d51916
--- /dev/null
+++ b/src/bun.js/node/node_fs_watcher.zig
@@ -0,0 +1,913 @@
+const std = @import("std");
+const JSC = @import("root").bun.JSC;
+const bun = @import("root").bun;
+const Fs = @import("../../fs.zig");
+const Path = @import("../../resolver/resolve_path.zig");
+const Encoder = JSC.WebCore.Encoder;
+
+const FSEvents = @import("./fs_events.zig");
+
+const VirtualMachine = JSC.VirtualMachine;
+const EventLoop = JSC.EventLoop;
+const PathLike = JSC.Node.PathLike;
+const ArgumentsSlice = JSC.Node.ArgumentsSlice;
+const Output = bun.Output;
+const string = bun.string;
+const StoredFileDescriptorType = bun.StoredFileDescriptorType;
+const Environment = bun.Environment;
+
+pub const FSWatcher = struct {
+ const watcher = @import("../../watcher.zig");
+ const options = @import("../../options.zig");
+ pub const Watcher = watcher.NewWatcher(*FSWatcher);
+ const log = Output.scoped(.FSWatcher, false);
+
+ pub const ChangeEvent = struct {
+ hash: Watcher.HashType = 0,
+ event_type: FSWatchTask.EventType = .change,
+ time_stamp: i64 = 0,
+ };
+
+ onAccept: std.ArrayHashMapUnmanaged(FSWatcher.Watcher.HashType, bun.BabyList(OnAcceptCallback), bun.ArrayIdentityContext, false) = .{},
+ ctx: *VirtualMachine,
+ js_watcher: ?*JSObject = null,
+ watcher_instance: ?*FSWatcher.Watcher = null,
+ verbose: bool = false,
+ file_paths: bun.BabyList(string) = .{},
+ entry_path: ?string = null,
+ entry_dir: string = "",
+ last_change_event: ChangeEvent = .{},
+
+ pub fn toJS(this: *FSWatcher) JSC.JSValue {
+ return if (this.js_watcher) |js| js.js_this else JSC.JSValue.jsUndefined();
+ }
+
+ pub fn eventLoop(this: FSWatcher) *EventLoop {
+ return this.ctx.eventLoop();
+ }
+
+ pub fn enqueueTaskConcurrent(this: FSWatcher, task: *JSC.ConcurrentTask) void {
+ this.eventLoop().enqueueTaskConcurrent(task);
+ }
+
+ pub fn deinit(this: *FSWatcher) void {
+ while (this.file_paths.popOrNull()) |file_path| {
+ bun.default_allocator.destroy(file_path);
+ }
+ this.file_paths.deinitWithAllocator(bun.default_allocator);
+ if (this.entry_path) |path| {
+ this.entry_path = null;
+ bun.default_allocator.destroy(path);
+ }
+ bun.default_allocator.destroy(this);
+ }
+
+ pub const FSWatchTask = struct {
+ ctx: *FSWatcher,
+ count: u8 = 0,
+
+ entries: [8]Entry = undefined,
+ concurrent_task: JSC.ConcurrentTask = undefined,
+
+ pub const EventType = enum {
+ rename,
+ change,
+ @"error",
+ abort,
+ };
+
+ pub const EventFreeType = enum {
+ destroy,
+ free,
+ none,
+ };
+
+ pub const Entry = struct {
+ file_path: string,
+ event_type: EventType,
+ free_type: EventFreeType,
+ };
+
+ pub fn append(this: *FSWatchTask, file_path: string, event_type: EventType, free_type: EventFreeType) void {
+ if (this.count == 8) {
+ this.enqueue();
+ var ctx = this.ctx;
+ this.* = .{
+ .ctx = ctx,
+ .count = 0,
+ };
+ }
+
+ this.entries[this.count] = .{
+ .file_path = file_path,
+ .event_type = event_type,
+ .free_type = free_type,
+ };
+ this.count += 1;
+ }
+
+ pub fn run(this: *FSWatchTask) void {
+ // this runs on JS Context
+ if (this.ctx.js_watcher) |js_watcher| {
+ for (this.entries[0..this.count]) |entry| {
+ switch (entry.event_type) {
+ .rename => {
+ js_watcher.emit(entry.file_path, "rename");
+ },
+ .change => {
+ js_watcher.emit(entry.file_path, "change");
+ },
+ .@"error" => {
+ // file_path is the error message in this case
+ js_watcher.emitError(entry.file_path);
+ },
+ .abort => {
+ js_watcher.emitIfAborted();
+ },
+ }
+ }
+ }
+ }
+
+ pub fn enqueue(this: *FSWatchTask) void {
+ if (this.count == 0)
+ return;
+
+ var that = bun.default_allocator.create(FSWatchTask) catch unreachable;
+
+ that.* = this.*;
+ this.count = 0;
+ that.concurrent_task.task = JSC.Task.init(that);
+ this.ctx.enqueueTaskConcurrent(&that.concurrent_task);
+ }
+
+ pub fn deinit(this: *FSWatchTask) void {
+ while (this.count > 0) {
+ this.count -= 1;
+ switch (this.entries[this.count].free_type) {
+ .destroy => bun.default_allocator.destroy(this.entries[this.count].file_path),
+ .free => bun.default_allocator.free(this.entries[this.count].file_path),
+ else => {},
+ }
+ }
+ bun.default_allocator.destroy(this);
+ }
+ };
+
+ fn NewCallback(comptime FunctionSignature: type) type {
+ return union(enum) {
+ javascript_callback: JSC.Strong,
+ zig_callback: struct {
+ ptr: *anyopaque,
+ function: *const FunctionSignature,
+ },
+ };
+ }
+
+ pub const OnAcceptCallback = NewCallback(fn (
+ vm: *JSC.VirtualMachine,
+ specifier: []const u8,
+ ) void);
+
+ fn addDirectory(ctx: *FSWatcher, fs_watcher: *FSWatcher.Watcher, fd: StoredFileDescriptorType, file_path: string, recursive: bool, buf: *[bun.MAX_PATH_BYTES + 1]u8, is_entry_path: bool) !void {
+ var dir_path_clone = bun.default_allocator.dupeZ(u8, file_path) catch unreachable;
+
+ if (is_entry_path) {
+ ctx.entry_path = dir_path_clone;
+ ctx.entry_dir = dir_path_clone;
+ } else {
+ ctx.file_paths.push(bun.default_allocator, dir_path_clone) catch unreachable;
+ }
+ fs_watcher.addDirectory(fd, dir_path_clone, FSWatcher.Watcher.getHash(file_path), false) catch |err| {
+ ctx.deinit();
+ fs_watcher.deinit(true);
+ return err;
+ };
+
+ var iter = (std.fs.IterableDir{ .dir = std.fs.Dir{
+ .fd = fd,
+ } }).iterate();
+
+ while (iter.next() catch |err| {
+ ctx.deinit();
+ fs_watcher.deinit(true);
+ return err;
+ }) |entry| {
+ var parts = [2]string{ dir_path_clone, entry.name };
+ var entry_path = Path.joinAbsStringBuf(
+ Fs.FileSystem.instance.topLevelDirWithoutTrailingSlash(),
+ buf,
+ &parts,
+ .auto,
+ );
+
+ buf[entry_path.len] = 0;
+ var entry_path_z = buf[0..entry_path.len :0];
+
+ var fs_info = fdFromAbsolutePathZ(entry_path_z) catch |err| {
+ ctx.deinit();
+ fs_watcher.deinit(true);
+ return err;
+ };
+
+ if (fs_info.is_file) {
+ const file_path_clone = bun.default_allocator.dupeZ(u8, entry_path) catch unreachable;
+
+ ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable;
+
+ fs_watcher.addFile(fs_info.fd, file_path_clone, FSWatcher.Watcher.getHash(entry_path), options.Loader.file, 0, null, false) catch |err| {
+ ctx.deinit();
+ fs_watcher.deinit(true);
+ return err;
+ };
+ } else {
+ if (recursive) {
+ addDirectory(ctx, fs_watcher, fs_info.fd, entry_path, recursive, buf, false) catch |err| {
+ ctx.deinit();
+ fs_watcher.deinit(true);
+ return err;
+ };
+ }
+ }
+ }
+ }
+
+ pub fn onError(
+ this: *FSWatcher,
+ err: anyerror,
+ ) void {
+ var current_task: FSWatchTask = .{
+ .ctx = this,
+ };
+ current_task.append(@errorName(err), .@"error", .none);
+ current_task.enqueue();
+ }
+
+ pub fn onFSEventUpdate(
+ ctx: ?*anyopaque,
+ path: string,
+ _: bool,
+ is_rename: bool,
+ ) void {
+ const this = bun.cast(*FSWatcher, ctx.?);
+
+ var current_task: FSWatchTask = .{
+ .ctx = this,
+ };
+ defer current_task.enqueue();
+
+ const relative_path = bun.default_allocator.dupe(u8, path) catch unreachable;
+ const event_type: FSWatchTask.EventType = if (is_rename) .rename else .change;
+
+ current_task.append(relative_path, event_type, .destroy);
+ }
+
+ pub fn onFileUpdate(
+ this: *FSWatcher,
+ events: []watcher.WatchEvent,
+ changed_files: []?[:0]u8,
+ watchlist: watcher.Watchlist,
+ ) void {
+ var slice = watchlist.slice();
+ const file_paths = slice.items(.file_path);
+
+ var counts = slice.items(.count);
+ const kinds = slice.items(.kind);
+ var _on_file_update_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+
+ var ctx = this.watcher_instance.?;
+ defer ctx.flushEvictions();
+ defer Output.flush();
+
+ var bundler = if (@TypeOf(this.ctx.bundler) == *bun.Bundler)
+ this.ctx.bundler
+ else
+ &this.ctx.bundler;
+
+ var fs: *Fs.FileSystem = bundler.fs;
+
+ var current_task: FSWatchTask = .{
+ .ctx = this,
+ };
+ defer current_task.enqueue();
+
+ const time_stamp = std.time.milliTimestamp();
+ const time_diff = time_stamp - this.last_change_event.time_stamp;
+
+ for (events) |event| {
+ const file_path = file_paths[event.index];
+ const update_count = counts[event.index] + 1;
+ counts[event.index] = update_count;
+ const kind = kinds[event.index];
+
+ if (comptime Environment.isDebug) {
+ if (this.verbose) {
+ Output.prettyErrorln("[watch] {s} ({s}, {})", .{ file_path, @tagName(kind), event.op });
+ }
+ }
+
+ switch (kind) {
+ .file => {
+ if (event.op.delete) {
+ ctx.removeAtIndex(
+ event.index,
+ 0,
+ &.{},
+ .file,
+ );
+ }
+
+ var file_hash: FSWatcher.Watcher.HashType = FSWatcher.Watcher.getHash(file_path);
+
+ if (event.op.write or event.op.delete or event.op.rename) {
+ const event_type: FSWatchTask.EventType = if (event.op.delete or event.op.rename or event.op.move_to) .rename else .change;
+ // skip consecutive duplicates
+ if ((this.last_change_event.time_stamp == 0 or time_diff > 1) or this.last_change_event.event_type != event_type and this.last_change_event.hash != file_hash) {
+ this.last_change_event.time_stamp = time_stamp;
+ this.last_change_event.event_type = event_type;
+ this.last_change_event.hash = file_hash;
+
+ const relative_slice = fs.relative(this.entry_dir, file_path);
+
+ if (this.verbose)
+ Output.prettyErrorln("<r><d>File changed: {s}<r>", .{relative_slice});
+
+ const relative_path = bun.default_allocator.dupe(u8, relative_slice) catch unreachable;
+
+ current_task.append(relative_path, event_type, .destroy);
+ }
+ }
+ },
+ .directory => {
+ // macOS should use FSEvents for directories
+ if (comptime Environment.isMac) {
+ @panic("Unexpected directory watch");
+ }
+
+ const affected = event.names(changed_files);
+
+ for (affected) |changed_name_| {
+ const changed_name: []const u8 = bun.asByteSlice(changed_name_.?);
+ if (changed_name.len == 0 or changed_name[0] == '~' or changed_name[0] == '.') continue;
+
+ var file_hash: FSWatcher.Watcher.HashType = 0;
+ const relative_slice: string = brk: {
+ var file_path_without_trailing_slash = std.mem.trimRight(u8, file_path, std.fs.path.sep_str);
+
+ @memcpy(_on_file_update_path_buf[0..file_path_without_trailing_slash.len], file_path_without_trailing_slash);
+
+ _on_file_update_path_buf[file_path_without_trailing_slash.len] = std.fs.path.sep;
+
+ @memcpy(_on_file_update_path_buf[file_path_without_trailing_slash.len + 1 ..][0..changed_name.len], changed_name);
+ const path_slice = _on_file_update_path_buf[0 .. file_path_without_trailing_slash.len + changed_name.len + 1];
+ file_hash = FSWatcher.Watcher.getHash(path_slice);
+
+ const relative = fs.relative(this.entry_dir, path_slice);
+
+ break :brk relative;
+ };
+
+ // skip consecutive duplicates
+ const event_type: FSWatchTask.EventType = .rename; // renaming folders, creating folder or files will be always be rename
+ if ((this.last_change_event.time_stamp == 0 or time_diff > 1) or this.last_change_event.event_type != event_type and this.last_change_event.hash != file_hash) {
+ const relative_path = bun.default_allocator.dupe(u8, relative_slice) catch unreachable;
+
+ this.last_change_event.time_stamp = time_stamp;
+ this.last_change_event.event_type = event_type;
+ this.last_change_event.hash = file_hash;
+
+ current_task.append(relative_path, event_type, .destroy);
+
+ if (this.verbose)
+ Output.prettyErrorln("<r> <d>Dir change: {s}<r>", .{relative_path});
+ }
+ }
+
+ if (this.verbose and affected.len == 0) {
+ Output.prettyErrorln("<r> <d>Dir change: {s}<r>", .{fs.relative(this.entry_dir, file_path)});
+ }
+ },
+ }
+ }
+ }
+
+ pub const Arguments = struct {
+ path: PathLike,
+ listener: JSC.JSValue,
+ global_this: JSC.C.JSContextRef,
+ signal: ?*JSC.AbortSignal,
+ persistent: bool,
+ recursive: bool,
+ encoding: JSC.Node.Encoding,
+ verbose: bool,
+ pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Arguments {
+ const vm = ctx.vm();
+ const path = PathLike.fromJS(ctx, arguments, exception) orelse {
+ if (exception.* == null) {
+ JSC.throwInvalidArguments(
+ "filename must be a string or TypedArray",
+ .{},
+ ctx,
+ exception,
+ );
+ }
+ return null;
+ };
+
+ if (exception.* != null) return null;
+ var listener: JSC.JSValue = .zero;
+ var signal: ?*JSC.AbortSignal = null;
+ var persistent: bool = true;
+ var recursive: bool = false;
+ var encoding: JSC.Node.Encoding = .utf8;
+ var verbose = false;
+ if (arguments.nextEat()) |options_or_callable| {
+
+ // options
+ if (options_or_callable.isObject()) {
+ if (options_or_callable.get(ctx, "persistent")) |persistent_| {
+ if (!persistent_.isBoolean()) {
+ JSC.throwInvalidArguments(
+ "persistent must be a boolean.",
+ .{},
+ ctx,
+ exception,
+ );
+ return null;
+ }
+ persistent = persistent_.toBoolean();
+ }
+
+ if (options_or_callable.get(ctx, "verbose")) |verbose_| {
+ if (!verbose_.isBoolean()) {
+ JSC.throwInvalidArguments(
+ "verbose must be a boolean.",
+ .{},
+ ctx,
+ exception,
+ );
+ return null;
+ }
+ verbose = verbose_.toBoolean();
+ }
+
+ if (options_or_callable.get(ctx, "encoding")) |encoding_| {
+ if (!encoding_.isString()) {
+ JSC.throwInvalidArguments(
+ "encoding must be a string.",
+ .{},
+ ctx,
+ exception,
+ );
+ return null;
+ }
+ if (JSC.Node.Encoding.fromJS(encoding_, ctx.ptr())) |node_encoding| {
+ encoding = node_encoding;
+ } else {
+ JSC.throwInvalidArguments(
+ "invalid encoding.",
+ .{},
+ ctx,
+ exception,
+ );
+ return null;
+ }
+ }
+
+ if (options_or_callable.get(ctx, "recursive")) |recursive_| {
+ if (!recursive_.isBoolean()) {
+ JSC.throwInvalidArguments(
+ "recursive must be a boolean.",
+ .{},
+ ctx,
+ exception,
+ );
+ return null;
+ }
+ recursive = recursive_.toBoolean();
+ }
+
+ // abort signal
+ if (options_or_callable.get(ctx, "signal")) |signal_| {
+ if (JSC.AbortSignal.fromJS(signal_)) |signal_obj| {
+ //Keep it alive
+ signal_.ensureStillAlive();
+ signal = signal_obj;
+ } else {
+ JSC.throwInvalidArguments(
+ "signal is not of type AbortSignal.",
+ .{},
+ ctx,
+ exception,
+ );
+
+ return null;
+ }
+ }
+
+ // listener
+ if (arguments.nextEat()) |callable| {
+ if (!callable.isCell() or !callable.isCallable(vm)) {
+ exception.* = JSC.toInvalidArguments("Expected \"listener\" callback to be a function", .{}, ctx).asObjectRef();
+ return null;
+ }
+ listener = callable;
+ }
+ } else {
+ if (!options_or_callable.isCell() or !options_or_callable.isCallable(vm)) {
+ exception.* = JSC.toInvalidArguments("Expected \"listener\" callback to be a function", .{}, ctx).asObjectRef();
+ return null;
+ }
+ listener = options_or_callable;
+ }
+ }
+ if (listener == .zero) {
+ exception.* = JSC.toInvalidArguments("Expected \"listener\" callback", .{}, ctx).asObjectRef();
+ return null;
+ }
+
+ return Arguments{
+ .path = path,
+ .listener = listener,
+ .global_this = ctx,
+ .signal = signal,
+ .persistent = persistent,
+ .recursive = recursive,
+ .encoding = encoding,
+ .verbose = verbose,
+ };
+ }
+
+ pub fn createFSWatcher(this: Arguments) !JSC.JSValue {
+ const obj = try FSWatcher.init(this);
+ return obj.toJS();
+ }
+ };
+
+ pub const JSObject = struct {
+ signal: ?*JSC.AbortSignal,
+ persistent: bool,
+ manager: ?*FSWatcher.Watcher,
+ fsevents_watcher: ?*FSEvents.FSEventsWatcher,
+ poll_ref: JSC.PollRef = .{},
+ globalThis: ?*JSC.JSGlobalObject,
+ js_this: JSC.JSValue,
+ encoding: JSC.Node.Encoding,
+ closed: bool,
+
+ pub usingnamespace JSC.Codegen.JSFSWatcher;
+
+ pub fn getFSWatcher(this: *JSObject) *FSWatcher {
+ if (this.manager) |manager| return manager.ctx;
+ if (this.fsevents_watcher) |manager| return bun.cast(*FSWatcher, manager.ctx.?);
+
+ @panic("No context attached to JSFSWatcher");
+ }
+
+ pub fn init(globalThis: *JSC.JSGlobalObject, manager: ?*FSWatcher.Watcher, fsevents_watcher: ?*FSEvents.FSEventsWatcher, signal: ?*JSC.AbortSignal, listener: JSC.JSValue, persistent: bool, encoding: JSC.Node.Encoding) !*JSObject {
+ var obj = try globalThis.allocator().create(JSObject);
+ obj.* = .{
+ .signal = null,
+ .persistent = persistent,
+ .manager = manager,
+ .fsevents_watcher = fsevents_watcher,
+ .globalThis = globalThis,
+ .js_this = .zero,
+ .encoding = encoding,
+ .closed = false,
+ };
+ const instance = obj.getFSWatcher();
+
+ if (persistent) {
+ obj.poll_ref.ref(instance.ctx);
+ }
+
+ var js_this = JSObject.toJS(obj, globalThis);
+ JSObject.listenerSetCached(js_this, globalThis, listener);
+ obj.js_this = js_this;
+ obj.js_this.protect();
+
+ if (signal) |s| {
+
+ // already aborted?
+ if (s.aborted()) {
+ obj.signal = s.ref();
+ // abort next tick
+ var current_task: FSWatchTask = .{
+ .ctx = instance,
+ };
+ current_task.append("", .abort, .none);
+ current_task.enqueue();
+ } else {
+ // watch for abortion
+ obj.signal = s.ref().listen(JSObject, obj, JSObject.emitAbort);
+ }
+ }
+ return obj;
+ }
+
+ pub fn emitIfAborted(this: *JSObject) void {
+ if (this.signal) |s| {
+ if (s.aborted()) {
+ const err = s.abortReason();
+ this.emitAbort(err);
+ }
+ }
+ }
+
+ pub fn emitAbort(this: *JSObject, err: JSC.JSValue) void {
+ if (this.closed) return;
+ defer this.close(true);
+
+ err.ensureStillAlive();
+
+ if (this.globalThis) |globalThis| {
+ if (this.js_this != .zero) {
+ if (JSObject.listenerGetCached(this.js_this)) |listener| {
+ var args = [_]JSC.JSValue{
+ JSC.ZigString.static("error").toValue(globalThis),
+ if (err.isEmptyOrUndefinedOrNull()) JSC.WebCore.AbortSignal.createAbortError(JSC.ZigString.static("The user aborted a request"), &JSC.ZigString.Empty, globalThis) else err,
+ };
+ _ = listener.callWithGlobalThis(
+ globalThis,
+ &args,
+ );
+ }
+ }
+ }
+ }
+ pub fn emitError(this: *JSObject, err: string) void {
+ if (this.closed) return;
+ defer this.close(true);
+
+ if (this.globalThis) |globalThis| {
+ if (this.js_this != .zero) {
+ if (JSObject.listenerGetCached(this.js_this)) |listener| {
+ var args = [_]JSC.JSValue{
+ JSC.ZigString.static("error").toValue(globalThis),
+ JSC.ZigString.fromUTF8(err).toErrorInstance(globalThis),
+ };
+ _ = listener.callWithGlobalThis(
+ globalThis,
+ &args,
+ );
+ }
+ }
+ }
+ }
+
+ pub fn emit(this: *JSObject, file_name: string, comptime eventType: string) void {
+ if (this.globalThis) |globalThis| {
+ if (this.js_this != .zero) {
+ if (JSObject.listenerGetCached(this.js_this)) |listener| {
+ var filename: JSC.JSValue = JSC.JSValue.jsUndefined();
+ if (file_name.len > 0) {
+ if (this.encoding == .buffer)
+ filename = JSC.ArrayBuffer.createBuffer(globalThis, file_name)
+ else if (this.encoding == .utf8) {
+ filename = JSC.ZigString.fromUTF8(file_name).toValueGC(globalThis);
+ } else {
+ // convert to desired encoding
+ filename = Encoder.toStringAtRuntime(file_name.ptr, file_name.len, globalThis, this.encoding);
+ }
+ }
+ var args = [_]JSC.JSValue{
+ JSC.ZigString.static(eventType).toValue(globalThis),
+ filename,
+ };
+ _ = listener.callWithGlobalThis(
+ globalThis,
+ &args,
+ );
+ }
+ }
+ }
+ }
+
+ pub fn ref(this: *JSObject) void {
+ if (this.closed) return;
+
+ if (!this.persistent) {
+ this.persistent = true;
+ this.poll_ref.ref(this.getFSWatcher().ctx);
+ }
+ }
+
+ pub fn doRef(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
+ this.ref();
+ return JSC.JSValue.jsUndefined();
+ }
+
+ pub fn unref(this: *JSObject) void {
+ if (this.persistent) {
+ this.persistent = false;
+ this.poll_ref.unref(this.getFSWatcher().ctx);
+ }
+ }
+
+ pub fn doUnref(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
+ this.unref();
+ return JSC.JSValue.jsUndefined();
+ }
+
+ pub fn hasRef(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
+ return JSC.JSValue.jsBoolean(this.persistent);
+ }
+
+ pub fn close(
+ this: *JSObject,
+ emitEvent: bool,
+ ) void {
+ if (!this.closed) {
+ if (this.signal) |signal| {
+ this.signal = null;
+ signal.detach(this);
+ }
+ this.closed = true;
+ if (emitEvent) {
+ this.emit("", "close");
+ }
+
+ this.detach();
+ }
+ }
+
+ pub fn detach(this: *JSObject) void {
+ this.unref();
+
+ if (this.js_this != .zero) {
+ this.js_this.unprotect();
+ this.js_this = .zero;
+ }
+
+ this.globalThis = null;
+
+ if (this.signal) |signal| {
+ this.signal = null;
+ signal.detach(this);
+ }
+ if (this.manager) |manager| {
+ var ctx = manager.ctx;
+ this.manager = null;
+ ctx.js_watcher = null;
+ ctx.deinit();
+ manager.deinit(true);
+ }
+
+ if (this.fsevents_watcher) |manager| {
+ var ctx = bun.cast(*FSWatcher, manager.ctx.?);
+ ctx.js_watcher = null;
+ ctx.deinit();
+ manager.deinit();
+ }
+ }
+
+ pub fn doClose(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue {
+ this.close(true);
+ return JSC.JSValue.jsUndefined();
+ }
+
+ pub fn finalize(this: *JSObject) callconv(.C) void {
+ if (!this.closed) {
+ this.detach();
+ }
+
+ bun.default_allocator.destroy(this);
+ }
+ };
+
+ const PathResult = struct {
+ fd: StoredFileDescriptorType = 0,
+ is_file: bool = true,
+ };
+
+ fn fdFromAbsolutePathZ(
+ absolute_path_z: [:0]const u8,
+ ) !PathResult {
+ var stat = try bun.C.lstat_absolute(absolute_path_z);
+ var result = PathResult{};
+
+ switch (stat.kind) {
+ .sym_link => {
+ var file = try std.fs.openFileAbsoluteZ(absolute_path_z, .{ .mode = .read_only });
+ result.fd = file.handle;
+ const _stat = try file.stat();
+
+ result.is_file = _stat.kind == .directory;
+ },
+ .directory => {
+ const dir = (try std.fs.openIterableDirAbsoluteZ(absolute_path_z, .{
+ .access_sub_paths = true,
+ })).dir;
+ result.fd = dir.fd;
+ result.is_file = false;
+ },
+ else => {
+ const file = try std.fs.openFileAbsoluteZ(absolute_path_z, .{ .mode = .read_only });
+ result.fd = file.handle;
+ result.is_file = true;
+ },
+ }
+ return result;
+ }
+
+ pub fn init(args: Arguments) !*FSWatcher {
+ var buf: [bun.MAX_PATH_BYTES + 1]u8 = undefined;
+ var slice = args.path.slice();
+ if (bun.strings.startsWith(slice, "file://")) {
+ slice = slice[6..];
+ }
+ var parts = [_]string{
+ slice,
+ };
+
+ var file_path = Path.joinAbsStringBuf(
+ Fs.FileSystem.instance.top_level_dir,
+ &buf,
+ &parts,
+ .auto,
+ );
+
+ buf[file_path.len] = 0;
+ var file_path_z = buf[0..file_path.len :0];
+
+ var fs_type = try fdFromAbsolutePathZ(file_path_z);
+
+ var ctx = try bun.default_allocator.create(FSWatcher);
+ const vm = args.global_this.bunVM();
+ ctx.* = .{
+ .ctx = vm,
+ .verbose = args.verbose,
+ .file_paths = bun.BabyList(string).initCapacity(bun.default_allocator, 1) catch |err| {
+ ctx.deinit();
+ return err;
+ },
+ };
+
+ if (comptime Environment.isMac) {
+ if (!fs_type.is_file) {
+ var dir_path_clone = bun.default_allocator.dupeZ(u8, file_path) catch unreachable;
+ ctx.entry_path = dir_path_clone;
+ ctx.entry_dir = dir_path_clone;
+
+ var fsevents_watcher = FSEvents.watch(dir_path_clone, args.recursive, onFSEventUpdate, bun.cast(*anyopaque, ctx)) catch |err| {
+ ctx.deinit();
+ return err;
+ };
+
+ ctx.js_watcher = JSObject.init(args.global_this, null, fsevents_watcher, args.signal, args.listener, args.persistent, args.encoding) catch |err| {
+ ctx.deinit();
+ fsevents_watcher.deinit();
+ return err;
+ };
+
+ return ctx;
+ }
+ }
+
+ var fs_watcher = FSWatcher.Watcher.init(
+ ctx,
+ vm.bundler.fs,
+ bun.default_allocator,
+ ) catch |err| {
+ ctx.deinit();
+ return err;
+ };
+
+ ctx.watcher_instance = fs_watcher;
+
+ if (fs_type.is_file) {
+ var file_path_clone = bun.default_allocator.dupeZ(u8, file_path) catch unreachable;
+
+ ctx.entry_path = file_path_clone;
+ ctx.entry_dir = std.fs.path.dirname(file_path_clone) orelse file_path_clone;
+
+ fs_watcher.addFile(fs_type.fd, file_path_clone, FSWatcher.Watcher.getHash(file_path), options.Loader.file, 0, null, false) catch |err| {
+ ctx.deinit();
+ fs_watcher.deinit(true);
+ return err;
+ };
+ } else {
+ addDirectory(ctx, fs_watcher, fs_type.fd, file_path, args.recursive, &buf, true) catch |err| {
+ ctx.deinit();
+ fs_watcher.deinit(true);
+ return err;
+ };
+ }
+
+ fs_watcher.start() catch |err| {
+ ctx.deinit();
+
+ fs_watcher.deinit(true);
+ return err;
+ };
+
+ ctx.js_watcher = JSObject.init(args.global_this, fs_watcher, null, args.signal, args.listener, args.persistent, args.encoding) catch |err| {
+ ctx.deinit();
+ fs_watcher.deinit(true);
+ return err;
+ };
+
+ return ctx;
+ }
+};
diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig
index e2de35706..659ac31bb 100644
--- a/src/bun.js/node/types.zig
+++ b/src/bun.js/node/types.zig
@@ -93,6 +93,10 @@ pub fn Maybe(comptime ResultType: type) type {
return JSC.JSValue.jsUndefined();
}
+ if (comptime ReturnType == JSC.JSValue) {
+ return r;
+ }
+
if (comptime ReturnType == JSC.ArrayBuffer) {
return r.toJS(globalThis, null);
}
diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig
index 5c8221128..061a25eed 100644
--- a/src/bun.js/webcore/encoding.zig
+++ b/src/bun.js/webcore/encoding.zig
@@ -802,7 +802,20 @@ pub const Encoder = struct {
// pub fn writeUTF16AsUTF8(utf16: [*]const u16, len: usize, to: [*]u8, to_len: usize) callconv(.C) i32 {
// return @intCast(i32, strings.copyUTF16IntoUTF8(to[0..to_len], []const u16, utf16[0..len], true).written);
// }
-
+ pub fn toStringAtRuntime(input: [*]const u8, len: usize, globalObject: *JSGlobalObject, encoding: JSC.Node.Encoding) JSValue {
+ return switch (encoding) {
+ .ucs2 => toString(input, len, globalObject, .utf16le),
+ .utf16le => toString(input, len, globalObject, .utf16le),
+ .utf8 => toString(input, len, globalObject, .utf8),
+ .ascii => toString(input, len, globalObject, .ascii),
+ .hex => toString(input, len, globalObject, .hex),
+ .base64 => toString(input, len, globalObject, .base64),
+ .base64url => toString(input, len, globalObject, .base64url),
+ .latin1 => toString(input, len, globalObject, .latin1),
+ // treat everything else as utf8
+ else => toString(input, len, globalObject, .utf8),
+ };
+ }
pub fn toString(input_ptr: [*]const u8, len: usize, global: *JSGlobalObject, comptime encoding: JSC.Node.Encoding) JSValue {
if (len == 0)
return ZigString.Empty.toValue(global);
diff --git a/src/fs.zig b/src/fs.zig
index e87d931df..98174fac3 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -1109,6 +1109,60 @@ pub const FileSystem = struct {
return File{ .path = Path.init(path), .contents = file_contents };
}
+ pub fn kindFromAbsolute(
+ fs: *RealFS,
+ absolute_path: [:0]const u8,
+ existing_fd: StoredFileDescriptorType,
+ store_fd: bool,
+ ) !Entry.Cache {
+ var outpath: [bun.MAX_PATH_BYTES]u8 = undefined;
+
+ var stat = try C.lstat_absolute(absolute_path);
+ const is_symlink = stat.kind == std.fs.File.Kind.SymLink;
+ var _kind = stat.kind;
+ var cache = Entry.Cache{
+ .kind = Entry.Kind.file,
+ .symlink = PathString.empty,
+ };
+ var symlink: []const u8 = "";
+
+ if (is_symlink) {
+ var file = try if (existing_fd != 0)
+ std.fs.File{ .handle = existing_fd }
+ else if (store_fd)
+ std.fs.openFileAbsoluteZ(absolute_path, .{ .mode = .read_only })
+ else
+ bun.openFileForPath(absolute_path);
+ setMaxFd(file.handle);
+
+ defer {
+ if ((!store_fd or fs.needToCloseFiles()) and existing_fd == 0) {
+ file.close();
+ } else if (comptime FeatureFlags.store_file_descriptors) {
+ cache.fd = file.handle;
+ }
+ }
+ const _stat = try file.stat();
+
+ symlink = try bun.getFdPath(file.handle, &outpath);
+
+ _kind = _stat.kind;
+ }
+
+ std.debug.assert(_kind != .SymLink);
+
+ if (_kind == .Directory) {
+ cache.kind = .dir;
+ } else {
+ cache.kind = .file;
+ }
+ if (symlink.len > 0) {
+ cache.symlink = PathString.init(try FilenameStore.instance.append([]const u8, symlink));
+ }
+
+ return cache;
+ }
+
pub fn kind(
fs: *RealFS,
_dir: string,
diff --git a/src/http.zig b/src/http.zig
index 827bfa6de..80718db2f 100644
--- a/src/http.zig
+++ b/src/http.zig
@@ -3238,7 +3238,12 @@ pub const Server = struct {
threadlocal var filechange_buf: [32]u8 = undefined;
threadlocal var filechange_buf_hinted: [32]u8 = undefined;
-
+ pub fn onError(
+ _: *@This(),
+ err: anyerror,
+ ) void {
+ Output.prettyErrorln("<r>Watcher crashed: <red><b>{s}<r>", .{@errorName(err)});
+ }
pub fn onFileUpdate(
ctx: *Server,
events: []watcher.WatchEvent,
diff --git a/src/js/node/fs.js b/src/js/node/fs.js
index f117020dd..6b0e3954e 100644
--- a/src/js/node/fs.js
+++ b/src/js/node/fs.js
@@ -1,3 +1,5 @@
+import { EventEmitter } from "stream";
+
// Hardcoded module "node:fs"
var { direct, isPromise, isCallable } = import.meta.primordials;
var promises = import.meta.require("node:fs/promises");
@@ -7,6 +9,63 @@ var NativeReadable = _getNativeReadableStreamPrototype(2, Readable); // 2 means
var fs = Bun.fs();
var debug = process.env.DEBUG ? console.log : () => {};
+
+class FSWatcher extends EventEmitter {
+ #watcher;
+ #listener;
+ constructor(path, options, listener) {
+ super();
+
+ if (typeof options === "function") {
+ listener = options;
+ options = {};
+ } else if (typeof options === "string") {
+ options = { encoding: options };
+ }
+
+ if (typeof listener !== "function") {
+ listener = () => {};
+ }
+
+ this.#listener = listener;
+ try {
+ this.#watcher = fs.watch(path, options || {}, this.#onEvent.bind(this));
+ } catch (e) {
+ if (!e.message?.startsWith("FileNotFound")) {
+ throw e;
+ }
+ const notFound = new Error(`ENOENT: no such file or directory, watch '${path}'`);
+ notFound.code = "ENOENT";
+ notFound.errno = -2;
+ notFound.path = path;
+ notFound.syscall = "watch";
+ notFound.filename = path;
+ throw notFound;
+ }
+ }
+
+ #onEvent(eventType, filenameOrError) {
+ if (eventType === "error" || eventType === "close") {
+ this.emit(eventType, filenameOrError);
+ } else {
+ this.emit("change", eventType, filenameOrError);
+ this.#listener(eventType, filenameOrError);
+ }
+ }
+
+ close() {
+ this.#watcher?.close();
+ this.#watcher = null;
+ }
+
+ ref() {
+ this.#watcher?.ref();
+ }
+
+ unref() {
+ this.#watcher?.unref();
+ }
+}
export var access = function access(...args) {
callbackify(fs.accessSync, args);
},
@@ -153,6 +212,9 @@ export var access = function access(...args) {
rmdirSync = fs.rmdirSync.bind(fs),
Dirent = fs.Dirent,
Stats = fs.Stats,
+ watch = function watch(path, options, listener) {
+ return new FSWatcher(path, options, listener);
+ },
promises = import.meta.require("node:fs/promises");
function callbackify(fsFunction, args) {
@@ -1002,7 +1064,8 @@ export default {
writeSync,
WriteStream,
ReadStream,
-
+ watch,
+ FSWatcher,
[Symbol.for("::bunternal::")]: {
ReadStreamClass,
WriteStreamClass,
diff --git a/src/js/node/fs.promises.ts b/src/js/node/fs.promises.ts
index de802928b..7df446ccb 100644
--- a/src/js/node/fs.promises.ts
+++ b/src/js/node/fs.promises.ts
@@ -1,4 +1,5 @@
// Hardcoded module "node:fs/promises"
+
// Note: `constants` is injected into the top of this file
declare var constants: typeof import("node:fs/promises").constants;
@@ -38,6 +39,55 @@ var promisify = {
},
}[notrace];
+export function watch(
+ filename: string | Buffer | URL,
+ options: { encoding?: BufferEncoding; persistent?: boolean; recursive?: boolean; signal?: AbortSignal } = {},
+) {
+ type Event = {
+ eventType: string;
+ filename: string | Buffer | undefined;
+ };
+ const events: Array<Event> = [];
+ if (filename instanceof URL) {
+ throw new TypeError("Watch URLs are not supported yet");
+ } else if (Buffer.isBuffer(filename)) {
+ filename = filename.toString();
+ } else if (typeof filename !== "string") {
+ throw new TypeError("Expected path to be a string or Buffer");
+ }
+ let nextEventResolve: Function | null = null;
+ if (typeof options === "string") {
+ options = { encoding: options };
+ }
+ fs.watch(filename, options || {}, (eventType: string, filename: string | Buffer | undefined) => {
+ events.push({ eventType, filename });
+ if (nextEventResolve) {
+ const resolve = nextEventResolve;
+ nextEventResolve = null;
+ resolve();
+ }
+ });
+ return {
+ async *[Symbol.asyncIterator]() {
+ let closed = false;
+ while (!closed) {
+ while (events.length) {
+ let event = events.shift() as Event;
+ if (event.eventType === "close") {
+ closed = true;
+ break;
+ }
+ if (event.eventType === "error") {
+ closed = true;
+ throw event.filename;
+ }
+ yield event;
+ }
+ await new Promise((resolve: Function) => (nextEventResolve = resolve));
+ }
+ },
+ };
+}
export var access = promisify(fs.accessSync),
appendFile = promisify(fs.appendFileSync),
close = promisify(fs.closeSync),
@@ -112,6 +162,7 @@ export default {
lutimes,
rm,
rmdir,
+ watch,
constants,
[Symbol.for("CommonJS")]: 0,
};
diff --git a/src/js/out/modules/node/fs.js b/src/js/out/modules/node/fs.js
index cc1e14d2b..cc3763cfc 100644
--- a/src/js/out/modules/node/fs.js
+++ b/src/js/out/modules/node/fs.js
@@ -1,3 +1,4 @@
+var {EventEmitter } = import.meta.require("node:stream");
var callbackify = function(fsFunction, args) {
try {
const result = fsFunction.apply(fs, args.slice(0, args.length - 1)), callback = args[args.length - 1];
@@ -16,7 +17,47 @@ function createWriteStream(path, options) {
return new WriteStream(path, options);
}
var { direct, isPromise, isCallable } = import.meta.primordials, promises = import.meta.require("node:fs/promises"), { Readable, NativeWritable, _getNativeReadableStreamPrototype, eos: eos_ } = import.meta.require("node:stream"), NativeReadable = _getNativeReadableStreamPrototype(2, Readable), fs = Bun.fs(), debug = process.env.DEBUG ? console.log : () => {
-}, access = function access2(...args) {
+};
+
+class FSWatcher extends EventEmitter {
+ #watcher;
+ #listener;
+ constructor(path, options, listener) {
+ super();
+ if (typeof options === "function")
+ listener = options, options = {};
+ else if (typeof options === "string")
+ options = { encoding: options };
+ if (typeof listener !== "function")
+ listener = () => {
+ };
+ this.#listener = listener;
+ try {
+ this.#watcher = fs.watch(path, options || {}, this.#onEvent.bind(this));
+ } catch (e) {
+ if (!e.message?.startsWith("FileNotFound"))
+ throw e;
+ const notFound = new Error(`ENOENT: no such file or directory, watch '${path}'`);
+ throw notFound.code = "ENOENT", notFound.errno = -2, notFound.path = path, notFound.syscall = "watch", notFound.filename = path, notFound;
+ }
+ }
+ #onEvent(eventType, filenameOrError) {
+ if (eventType === "error" || eventType === "close")
+ this.emit(eventType, filenameOrError);
+ else
+ this.emit("change", eventType, filenameOrError), this.#listener(eventType, filenameOrError);
+ }
+ close() {
+ this.#watcher?.close(), this.#watcher = null;
+ }
+ ref() {
+ this.#watcher?.ref();
+ }
+ unref() {
+ this.#watcher?.unref();
+ }
+}
+var access = function access2(...args) {
callbackify(fs.accessSync, args);
}, appendFile = function appendFile2(...args) {
callbackify(fs.appendFileSync, args);
@@ -88,7 +129,9 @@ var { direct, isPromise, isCallable } = import.meta.primordials, promises = impo
callbackify(fs.utimesSync, args);
}, lutimes = function lutimes2(...args) {
callbackify(fs.lutimesSync, args);
-}, accessSync = fs.accessSync.bind(fs), appendFileSync = fs.appendFileSync.bind(fs), closeSync = fs.closeSync.bind(fs), copyFileSync = fs.copyFileSync.bind(fs), existsSync = fs.existsSync.bind(fs), chownSync = fs.chownSync.bind(fs), chmodSync = fs.chmodSync.bind(fs), fchmodSync = fs.fchmodSync.bind(fs), fchownSync = fs.fchownSync.bind(fs), fstatSync = fs.fstatSync.bind(fs), fsyncSync = fs.fsyncSync.bind(fs), ftruncateSync = fs.ftruncateSync.bind(fs), futimesSync = fs.futimesSync.bind(fs), lchmodSync = fs.lchmodSync.bind(fs), lchownSync = fs.lchownSync.bind(fs), linkSync = fs.linkSync.bind(fs), lstatSync = fs.lstatSync.bind(fs), mkdirSync = fs.mkdirSync.bind(fs), mkdtempSync = fs.mkdtempSync.bind(fs), openSync = fs.openSync.bind(fs), readSync = fs.readSync.bind(fs), writeSync = fs.writeSync.bind(fs), readdirSync = fs.readdirSync.bind(fs), readFileSync = fs.readFileSync.bind(fs), writeFileSync = fs.writeFileSync.bind(fs), readlinkSync = fs.readlinkSync.bind(fs), realpathSync = fs.realpathSync.bind(fs), renameSync = fs.renameSync.bind(fs), statSync = fs.statSync.bind(fs), symlinkSync = fs.symlinkSync.bind(fs), truncateSync = fs.truncateSync.bind(fs), unlinkSync = fs.unlinkSync.bind(fs), utimesSync = fs.utimesSync.bind(fs), lutimesSync = fs.lutimesSync.bind(fs), rmSync = fs.rmSync.bind(fs), rmdirSync = fs.rmdirSync.bind(fs), Dirent = fs.Dirent, Stats = fs.Stats, promises = import.meta.require("node:fs/promises"), readStreamPathFastPathSymbol = Symbol.for("Bun.Node.readStreamPathFastPath"), readStreamSymbol = Symbol.for("Bun.NodeReadStream"), readStreamPathOrFdSymbol = Symbol.for("Bun.NodeReadStreamPathOrFd"), writeStreamSymbol = Symbol.for("Bun.NodeWriteStream"), writeStreamPathFastPathSymbol = Symbol.for("Bun.NodeWriteStreamFastPath"), writeStreamPathFastPathCallSymbol = Symbol.for("Bun.NodeWriteStreamFastPathCall"), kIoDone = Symbol.for("kIoDone"), defaultReadStreamOptions = {
+}, accessSync = fs.accessSync.bind(fs), appendFileSync = fs.appendFileSync.bind(fs), closeSync = fs.closeSync.bind(fs), copyFileSync = fs.copyFileSync.bind(fs), existsSync = fs.existsSync.bind(fs), chownSync = fs.chownSync.bind(fs), chmodSync = fs.chmodSync.bind(fs), fchmodSync = fs.fchmodSync.bind(fs), fchownSync = fs.fchownSync.bind(fs), fstatSync = fs.fstatSync.bind(fs), fsyncSync = fs.fsyncSync.bind(fs), ftruncateSync = fs.ftruncateSync.bind(fs), futimesSync = fs.futimesSync.bind(fs), lchmodSync = fs.lchmodSync.bind(fs), lchownSync = fs.lchownSync.bind(fs), linkSync = fs.linkSync.bind(fs), lstatSync = fs.lstatSync.bind(fs), mkdirSync = fs.mkdirSync.bind(fs), mkdtempSync = fs.mkdtempSync.bind(fs), openSync = fs.openSync.bind(fs), readSync = fs.readSync.bind(fs), writeSync = fs.writeSync.bind(fs), readdirSync = fs.readdirSync.bind(fs), readFileSync = fs.readFileSync.bind(fs), writeFileSync = fs.writeFileSync.bind(fs), readlinkSync = fs.readlinkSync.bind(fs), realpathSync = fs.realpathSync.bind(fs), renameSync = fs.renameSync.bind(fs), statSync = fs.statSync.bind(fs), symlinkSync = fs.symlinkSync.bind(fs), truncateSync = fs.truncateSync.bind(fs), unlinkSync = fs.unlinkSync.bind(fs), utimesSync = fs.utimesSync.bind(fs), lutimesSync = fs.lutimesSync.bind(fs), rmSync = fs.rmSync.bind(fs), rmdirSync = fs.rmdirSync.bind(fs), Dirent = fs.Dirent, Stats = fs.Stats, watch = function watch2(path, options, listener) {
+ return new FSWatcher(path, options, listener);
+}, promises = import.meta.require("node:fs/promises"), readStreamPathFastPathSymbol = Symbol.for("Bun.Node.readStreamPathFastPath"), readStreamSymbol = Symbol.for("Bun.NodeReadStream"), readStreamPathOrFdSymbol = Symbol.for("Bun.NodeReadStreamPathOrFd"), writeStreamSymbol = Symbol.for("Bun.NodeWriteStream"), writeStreamPathFastPathSymbol = Symbol.for("Bun.NodeWriteStreamFastPath"), writeStreamPathFastPathCallSymbol = Symbol.for("Bun.NodeWriteStreamFastPathCall"), kIoDone = Symbol.for("kIoDone"), defaultReadStreamOptions = {
file: void 0,
fd: void 0,
flags: "r",
@@ -590,6 +633,8 @@ var fs_default = {
writeSync,
WriteStream,
ReadStream,
+ watch,
+ FSWatcher,
[Symbol.for("::bunternal::")]: {
ReadStreamClass,
WriteStreamClass
@@ -600,6 +645,7 @@ export {
writeFileSync,
writeFile,
write,
+ watch,
utimesSync,
utimes,
unlinkSync,
diff --git a/src/js/out/modules/node/fs.promises.js b/src/js/out/modules/node/fs.promises.js
index 2780ff166..ef3330771 100644
--- a/src/js/out/modules/node/fs.promises.js
+++ b/src/js/out/modules/node/fs.promises.js
@@ -1 +1 @@
-var D=Bun.fs(),B="::bunternal::",E={[B]:(S)=>{var b={[B]:function(C,J,q){var z;try{z=S.apply(D,q),q=void 0}catch(A){q=void 0,J(A);return}C(z)}}[B];return async function(...C){return await new Promise((J,q)=>{process.nextTick(b,J,q,C)})}}}[B],G=E(D.accessSync),H=E(D.appendFileSync),I=E(D.closeSync),K=E(D.copyFileSync),L=E(D.existsSync),M=E(D.chownSync),N=E(D.chmodSync),O=E(D.fchmodSync),P=E(D.fchownSync),Q=E(D.fstatSync),R=E(D.fsyncSync),T=E(D.ftruncateSync),U=E(D.futimesSync),V=E(D.lchmodSync),W=E(D.lchownSync),X=E(D.linkSync),Y=E(D.lstatSync),Z=E(D.mkdirSync),_=E(D.mkdtempSync),$=E(D.openSync),x=E(D.readSync),j=E(D.writeSync),v=E(D.readdirSync),w=E(D.readFileSync),k=E(D.writeFileSync),F=E(D.readlinkSync),h=E(D.realpathSync),g=E(D.renameSync),u=E(D.statSync),d=E(D.symlinkSync),n=E(D.truncateSync),l=E(D.unlinkSync),a=E(D.utimesSync),c=E(D.lutimesSync),t=E(D.rmSync),y=E(D.rmdirSync),p={access:G,appendFile:H,close:I,copyFile:K,exists:L,chown:M,chmod:N,fchmod:O,fchown:P,fstat:Q,fsync:R,ftruncate:T,futimes:U,lchmod:V,lchown:W,link:X,lstat:Y,mkdir:Z,mkdtemp:_,open:$,read:x,write:j,readdir:v,readFile:w,writeFile:k,readlink:F,realpath:h,rename:g,stat:u,symlink:d,truncate:n,unlink:l,utimes:a,lutimes:c,rm:t,rmdir:y,constants,[Symbol.for("CommonJS")]:0};export{k as writeFile,j as write,a as utimes,l as unlink,n as truncate,d as symlink,u as stat,y as rmdir,t as rm,g as rename,h as realpath,F as readlink,v as readdir,w as readFile,x as read,$ as open,_ as mkdtemp,Z as mkdir,c as lutimes,Y as lstat,X as link,W as lchown,V as lchmod,U as futimes,T as ftruncate,R as fsync,Q as fstat,P as fchown,O as fchmod,L as exists,p as default,K as copyFile,I as close,M as chown,N as chmod,H as appendFile,G as access};
+function H(S,C={}){const J=[];if(S instanceof URL)throw new TypeError("Watch URLs are not supported yet");else if(Buffer.isBuffer(S))S=S.toString();else if(typeof S!=="string")throw new TypeError("Expected path to be a string or Buffer");let b=null;if(typeof C==="string")C={encoding:C};return D.watch(S,C||{},(q,z)=>{if(J.push({eventType:q,filename:z}),b){const A=b;b=null,A()}}),{async*[Symbol.asyncIterator](){let q=!1;while(!q){while(J.length){let z=J.shift();if(z.eventType==="close"){q=!0;break}if(z.eventType==="error")throw q=!0,z.filename;yield z}await new Promise((z)=>b=z)}}}}var D=Bun.fs(),B="::bunternal::",G={[B]:(S)=>{var C={[B]:function(J,b,q){var z;try{z=S.apply(D,q),q=void 0}catch(A){q=void 0,b(A);return}J(z)}}[B];return async function(...J){return await new Promise((b,q)=>{process.nextTick(C,b,q,J)})}}}[B],I=G(D.accessSync),K=G(D.appendFileSync),L=G(D.closeSync),M=G(D.copyFileSync),N=G(D.existsSync),O=G(D.chownSync),P=G(D.chmodSync),Q=G(D.fchmodSync),U=G(D.fchownSync),V=G(D.fstatSync),W=G(D.fsyncSync),X=G(D.ftruncateSync),Y=G(D.futimesSync),Z=G(D.lchmodSync),_=G(D.lchownSync),$=G(D.linkSync),T=G(D.lstatSync),E=G(D.mkdirSync),j=G(D.mkdtempSync),R=G(D.openSync),k=G(D.readSync),x=G(D.writeSync),F=G(D.readdirSync),u=G(D.readFileSync),w=G(D.writeFileSync),g=G(D.readlinkSync),h=G(D.realpathSync),d=G(D.renameSync),c=G(D.statSync),v=G(D.symlinkSync),a=G(D.truncateSync),y=G(D.unlinkSync),l=G(D.utimesSync),t=G(D.lutimesSync),p=G(D.rmSync),n=G(D.rmdirSync),m={access:I,appendFile:K,close:L,copyFile:M,exists:N,chown:O,chmod:P,fchmod:Q,fchown:U,fstat:V,fsync:W,ftruncate:X,futimes:Y,lchmod:Z,lchown:_,link:$,lstat:T,mkdir:E,mkdtemp:j,open:R,read:k,write:x,readdir:F,readFile:u,writeFile:w,readlink:g,realpath:h,rename:d,stat:c,symlink:v,truncate:a,unlink:y,utimes:l,lutimes:t,rm:p,rmdir:n,watch:H,constants,[Symbol.for("CommonJS")]:0};export{w as writeFile,x as write,H as watch,l as utimes,y as unlink,a as truncate,v as symlink,c as stat,n as rmdir,p as rm,d as rename,h as realpath,g as readlink,F as readdir,u as readFile,k as read,R as open,j as mkdtemp,E as mkdir,t as lutimes,T as lstat,$ as link,_ as lchown,Z as lchmod,Y as futimes,X as ftruncate,W as fsync,V as fstat,U as fchown,Q as fchmod,N as exists,m as default,M as copyFile,L as close,O as chown,P as chmod,K as appendFile,I as access};
diff --git a/src/js/private.d.ts b/src/js/private.d.ts
index b6ed64801..b689c208e 100644
--- a/src/js/private.d.ts
+++ b/src/js/private.d.ts
@@ -6,11 +6,95 @@
*/
declare function $bundleError(error: string);
+type BunFSWatchOptions = { encoding?: BufferEncoding; persistent?: boolean; recursive?: boolean; signal?: AbortSignal };
+
+type BunWatchEventType = "rename" | "change" | "error" | "close";
+type BunWatchListener<T> = (event: WatchEventType, filename: T | Error | undefined) => void;
+
+interface BunFSWatcher {
+ /**
+ * Stop watching for changes on the given `BunFSWatcher`. Once stopped, the `BunFSWatcher` object is no longer usable.
+ * @since v0.6.8
+ */
+ close(): void;
+
+ /**
+ * When called, requests that the Node.js event loop not exit so long as the <BunFSWatcher> is active. Calling watcher.ref() multiple times will have no effect.
+ */
+ ref(): void;
+
+ /**
+ * When called, the active <BunFSWatcher> object will not require the Node.js event loop to remain active. If there is no other activity keeping the event loop running, the process may exit before the <BunFSWatcher> object's callback is invoked. Calling watcher.unref() multiple times will have no effect.
+ */
+ unref(): void;
+}
+type BunFS = Omit<typeof import("node:fs"), "watch"> & {
+ /**
+ * Watch for changes on `filename`, where `filename` is either a file or a
+ * directory.
+ *
+ * The second argument is optional. If `options` is provided as a string, it
+ * specifies the `encoding`. Otherwise `options` should be passed as an object.
+ *
+ * The listener callback gets two arguments `(eventType, filename)`. `eventType`is either `'rename'`, `'change', 'error' or 'close'`, and `filename` is the name of the file
+ * which triggered the event, the error when `eventType` is 'error' or undefined when eventType is 'close'.
+ *
+ * On most platforms, `'rename'` is emitted whenever a filename appears or
+ * disappears in the directory.
+ *
+ *
+ * If a `signal` is passed, aborting the corresponding AbortController will close
+ * the returned `BunFSWatcher`.
+ * @since v0.6.8
+ * @param listener
+ */
+ watch(
+ filename: string,
+ options:
+ | (WatchOptions & {
+ encoding: "buffer";
+ })
+ | "buffer",
+ listener?: BunWatchListener<Buffer>,
+ ): BunFSWatcher;
+ /**
+ * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `BunFSWatcher`.
+ * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol.
+ * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options.
+ * If `encoding` is not supplied, the default of `'utf8'` is used.
+ * If `persistent` is not supplied, the default of `true` is used.
+ * If `recursive` is not supplied, the default of `false` is used.
+ */
+ watch(
+ filename: string,
+ options?: WatchOptions | BufferEncoding | null,
+ listener?: BunWatchListener<string>,
+ ): BunFSWatcher;
+ /**
+ * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `BunFSWatcher`.
+ * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol.
+ * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options.
+ * If `encoding` is not supplied, the default of `'utf8'` is used.
+ * If `persistent` is not supplied, the default of `true` is used.
+ * If `recursive` is not supplied, the default of `false` is used.
+ */
+ watch(
+ filename: string,
+ options: BunWatchListener | string,
+ listener?: BunWatchListener<string | Buffer>,
+ ): BunFSWatcher;
+ /**
+ * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `BunFSWatcher`.
+ * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol.
+ */
+ watch(filename: string, listener?: BunWatchListener<string>): BunFSWatcher;
+};
+
declare module "bun" {
var TOML: {
parse(contents: string): any;
};
- function fs(): typeof import("node:fs");
+ function fs(): BunFS;
function _Os(): typeof import("node:os");
function jest(): typeof import("bun:test");
var main: string;
diff --git a/src/jsc.zig b/src/jsc.zig
index 67cf3f05c..ca31d5f1a 100644
--- a/src/jsc.zig
+++ b/src/jsc.zig
@@ -50,6 +50,7 @@ pub const FFI = @import("./bun.js/api/ffi.zig").FFI;
pub const Node = struct {
pub usingnamespace @import("./bun.js/node/types.zig");
pub usingnamespace @import("./bun.js/node/node_fs.zig");
+ pub usingnamespace @import("./bun.js/node/node_fs_watcher.zig");
pub usingnamespace @import("./bun.js/node/node_fs_binding.zig");
pub usingnamespace @import("./bun.js/node/node_os.zig");
pub const Syscall = @import("./bun.js/node/syscall.zig");
diff --git a/src/watcher.zig b/src/watcher.zig
index 155c0b473..044770dc4 100644
--- a/src/watcher.zig
+++ b/src/watcher.zig
@@ -108,6 +108,10 @@ pub const INotify = struct {
std.os.inotify_rm_watch(inotify_fd, wd);
}
+ pub fn isRunning() bool {
+ return loaded_inotify;
+ }
+
var coalesce_interval: isize = 100_000;
pub fn init() !void {
std.debug.assert(!loaded_inotify);
@@ -229,6 +233,10 @@ const DarwinWatcher = struct {
if (fd == 0) return error.KQueueError;
}
+ pub fn isRunning() bool {
+ return fd != 0;
+ }
+
pub fn stop() void {
if (fd != 0) {
std.os.close(fd);
@@ -361,6 +369,8 @@ pub fn NewWatcher(comptime ContextType: type) type {
watchloop_handle: ?std.Thread.Id = null,
cwd: string,
thread: std.Thread = undefined,
+ running: bool = true,
+ close_descriptors: bool = false,
pub const HashType = u32;
@@ -372,7 +382,9 @@ pub fn NewWatcher(comptime ContextType: type) type {
pub fn init(ctx: ContextType, fs: *Fs.FileSystem, allocator: std.mem.Allocator) !*Watcher {
var watcher = try allocator.create(Watcher);
- try PlatformWatcher.init();
+ if (!PlatformWatcher.isRunning()) {
+ try PlatformWatcher.init();
+ }
watcher.* = Watcher{
.fs = fs,
@@ -393,6 +405,26 @@ pub fn NewWatcher(comptime ContextType: type) type {
this.thread = try std.Thread.spawn(.{}, Watcher.watchLoop, .{this});
}
+ pub fn deinit(this: *Watcher, close_descriptors: bool) void {
+ this.mutex.lock();
+ defer this.mutex.unlock();
+
+ this.close_descriptors = close_descriptors;
+ if (this.watchloop_handle != null) {
+ this.running = false;
+ } else {
+ if (this.close_descriptors and this.running) {
+ const fds = this.watchlist.items(.fd);
+ for (fds) |fd| {
+ std.os.close(fd);
+ }
+ }
+ this.watchlist.deinit(this.allocator);
+ const allocator = this.allocator;
+ allocator.destroy(this);
+ }
+ }
+
// This must only be called from the watcher thread
pub fn watchLoop(this: *Watcher) !void {
this.watchloop_handle = std.Thread.getCurrentId();
@@ -402,12 +434,24 @@ pub fn NewWatcher(comptime ContextType: type) type {
if (FeatureFlags.verbose_watcher) Output.prettyln("Watcher started", .{});
this._watchLoop() catch |err| {
- Output.prettyErrorln("<r>Watcher crashed: <red><b>{s}<r>", .{@errorName(err)});
-
this.watchloop_handle = null;
PlatformWatcher.stop();
- return;
+ if (this.running) {
+ this.ctx.onError(err);
+ }
};
+
+ // deinit and close descriptors if needed
+ if (this.close_descriptors) {
+ const fds = this.watchlist.items(.fd);
+ for (fds) |fd| {
+ std.os.close(fd);
+ }
+ }
+ this.watchlist.deinit(this.allocator);
+
+ const allocator = this.allocator;
+ allocator.destroy(this);
}
var evict_list_i: WatchItemIndex = 0;
@@ -475,7 +519,7 @@ pub fn NewWatcher(comptime ContextType: type) type {
var changelist_array: [128]KEvent = std.mem.zeroes([128]KEvent);
var changelist = &changelist_array;
- while (true) {
+ while (this.running) {
defer Output.flush();
var count_ = std.os.system.kevent(
@@ -530,11 +574,12 @@ pub fn NewWatcher(comptime ContextType: type) type {
this.mutex.lock();
defer this.mutex.unlock();
-
- this.ctx.onFileUpdate(watchevents, this.changed_filepaths[0..watchevents.len], this.watchlist);
+ if (this.running) {
+ this.ctx.onFileUpdate(watchevents, this.changed_filepaths[0..watchevents.len], this.watchlist);
+ }
}
} else if (Environment.isLinux) {
- restart: while (true) {
+ restart: while (this.running) {
defer Output.flush();
var events = try INotify.read();
@@ -600,9 +645,10 @@ pub fn NewWatcher(comptime ContextType: type) type {
this.mutex.lock();
defer this.mutex.unlock();
-
- this.ctx.onFileUpdate(all_events[0 .. last_event_index + 1], this.changed_filepaths[0 .. name_off + 1], this.watchlist);
- remaining_events -= slice.len;
+ if (this.running) {
+ this.ctx.onFileUpdate(all_events[0 .. last_event_index + 1], this.changed_filepaths[0 .. name_off + 1], this.watchlist);
+ remaining_events -= slice.len;
+ }
}
}
}
diff --git a/test/js/node/watch/fixtures/close.js b/test/js/node/watch/fixtures/close.js
new file mode 100644
index 000000000..8eeeb79a3
--- /dev/null
+++ b/test/js/node/watch/fixtures/close.js
@@ -0,0 +1,7 @@
+import fs from "fs";
+fs.watch(import.meta.path, { signal: AbortSignal.timeout(4000) })
+ .on("error", err => {
+ console.error(err.message);
+ process.exit(1);
+ })
+ .close();
diff --git a/test/js/node/watch/fixtures/persistent.js b/test/js/node/watch/fixtures/persistent.js
new file mode 100644
index 000000000..72a2b6564
--- /dev/null
+++ b/test/js/node/watch/fixtures/persistent.js
@@ -0,0 +1,5 @@
+import fs from "fs";
+fs.watch(import.meta.path, { persistent: false, signal: AbortSignal.timeout(4000) }).on("error", err => {
+ console.error(err.message);
+ process.exit(1);
+});
diff --git a/test/js/node/watch/fixtures/relative.js b/test/js/node/watch/fixtures/relative.js
new file mode 100644
index 000000000..26e09da1a
--- /dev/null
+++ b/test/js/node/watch/fixtures/relative.js
@@ -0,0 +1,23 @@
+import fs from "fs";
+const watcher = fs.watch("relative.txt", { signal: AbortSignal.timeout(2000) });
+
+watcher.on("change", function (event, filename) {
+ if (filename !== "relative.txt" && event !== "change") {
+ console.error("fail");
+ clearInterval(interval);
+ watcher.close();
+ process.exit(1);
+ } else {
+ clearInterval(interval);
+ watcher.close();
+ }
+});
+watcher.on("error", err => {
+ clearInterval(interval);
+ console.error(err.message);
+ process.exit(1);
+});
+
+const interval = setInterval(() => {
+ fs.writeFileSync("relative.txt", "world");
+}, 10);
diff --git a/test/js/node/watch/fixtures/unref.js b/test/js/node/watch/fixtures/unref.js
new file mode 100644
index 000000000..a0c506a04
--- /dev/null
+++ b/test/js/node/watch/fixtures/unref.js
@@ -0,0 +1,7 @@
+import fs from "fs";
+fs.watch(import.meta.path, { signal: AbortSignal.timeout(4000) })
+ .on("error", err => {
+ console.error(err.message);
+ process.exit(1);
+ })
+ .unref();
diff --git a/test/js/node/watch/fs.watch.test.js b/test/js/node/watch/fs.watch.test.js
new file mode 100644
index 000000000..56e1798f1
--- /dev/null
+++ b/test/js/node/watch/fs.watch.test.js
@@ -0,0 +1,424 @@
+import fs from "fs";
+import path from "path";
+import { tempDirWithFiles, bunRun, bunRunAsScript } from "harness";
+import { pathToFileURL } from "bun";
+
+import { describe, expect, test } from "bun:test";
+// Because macOS (and possibly other operating systems) can return a watcher
+// before it is actually watching, we need to repeat the operation to avoid
+// a race condition.
+function repeat(fn) {
+ const interval = setInterval(fn, 20);
+ return interval;
+}
+const encodingFileName = `新建文夹件.txt`;
+const testDir = tempDirWithFiles("watch", {
+ "watch.txt": "hello",
+ "relative.txt": "hello",
+ "abort.txt": "hello",
+ "url.txt": "hello",
+ [encodingFileName]: "hello",
+});
+
+describe("fs.watch", () => {
+ test("non-persistent watcher should not block the event loop", done => {
+ try {
+ // https://github.com/joyent/node/issues/2293 - non-persistent watcher should not block the event loop
+ bunRun(path.join(import.meta.dir, "fixtures", "persistent.js"));
+ done();
+ } catch (e) {
+ done(e);
+ }
+ });
+
+ test("watcher should close and not block the event loop", done => {
+ try {
+ bunRun(path.join(import.meta.dir, "fixtures", "close.js"));
+ done();
+ } catch (e) {
+ done(e);
+ }
+ });
+
+ test("unref watcher should not block the event loop", done => {
+ try {
+ bunRun(path.join(import.meta.dir, "fixtures", "unref.js"));
+ done();
+ } catch (e) {
+ done(e);
+ }
+ });
+
+ test("should work with relative files", done => {
+ try {
+ bunRunAsScript(testDir, path.join(import.meta.dir, "fixtures", "relative.js"));
+ done();
+ } catch (e) {
+ done(e);
+ }
+ });
+
+ test("add file/folder to folder", done => {
+ let count = 0;
+ const root = path.join(testDir, "add-directory");
+ try {
+ fs.mkdirSync(root);
+ } catch {}
+ let err = undefined;
+ const watcher = fs.watch(root, { signal: AbortSignal.timeout(3000) });
+ watcher.on("change", (event, filename) => {
+ count++;
+ try {
+ expect(event).toBe("rename");
+ expect(["new-file.txt", "new-folder.txt"]).toContain(filename);
+ if (count >= 2) {
+ watcher.close();
+ }
+ } catch (e) {
+ err = e;
+ watcher.close();
+ }
+ });
+
+ watcher.on("error", e => (err = e));
+ watcher.on("close", () => {
+ clearInterval(interval);
+ done(err);
+ });
+
+ const interval = repeat(() => {
+ fs.writeFileSync(path.join(root, "new-file.txt"), "hello");
+ fs.mkdirSync(path.join(root, "new-folder.txt"));
+ fs.rmdirSync(path.join(root, "new-folder.txt"));
+ });
+ });
+
+ test("add file/folder to subfolder", done => {
+ let count = 0;
+ const root = path.join(testDir, "add-subdirectory");
+ try {
+ fs.mkdirSync(root);
+ } catch {}
+ const subfolder = path.join(root, "subfolder");
+ fs.mkdirSync(subfolder);
+ const watcher = fs.watch(root, { recursive: true, signal: AbortSignal.timeout(3000) });
+ let err = undefined;
+ watcher.on("change", (event, filename) => {
+ const basename = path.basename(filename);
+ if (basename === "subfolder") return;
+ count++;
+ try {
+ expect(event).toBe("rename");
+ expect(["new-file.txt", "new-folder.txt"]).toContain(basename);
+ if (count >= 2) {
+ watcher.close();
+ }
+ } catch (e) {
+ err = e;
+ watcher.close();
+ }
+ });
+ watcher.on("error", e => (err = e));
+ watcher.on("close", () => {
+ clearInterval(interval);
+ done(err);
+ });
+
+ const interval = repeat(() => {
+ fs.writeFileSync(path.join(subfolder, "new-file.txt"), "hello");
+ fs.mkdirSync(path.join(subfolder, "new-folder.txt"));
+ fs.rmdirSync(path.join(subfolder, "new-folder.txt"));
+ });
+ });
+
+ test("should emit event when file is deleted", done => {
+ const testsubdir = tempDirWithFiles("subdir", {
+ "deleted.txt": "hello",
+ });
+ const filepath = path.join(testsubdir, "deleted.txt");
+ let err = undefined;
+ const watcher = fs.watch(testsubdir, function (event, filename) {
+ try {
+ expect(event).toBe("rename");
+ expect(filename).toBe("deleted.txt");
+ } catch (e) {
+ err = e;
+ } finally {
+ clearInterval(interval);
+ watcher.close();
+ }
+ });
+
+ watcher.once("close", () => {
+ done(err);
+ });
+
+ const interval = repeat(() => {
+ fs.rmSync(filepath, { force: true });
+ const fd = fs.openSync(filepath, "w");
+ fs.closeSync(fd);
+ });
+ });
+
+ test("should emit 'change' event when file is modified", done => {
+ const filepath = path.join(testDir, "watch.txt");
+
+ const watcher = fs.watch(filepath);
+ let err = undefined;
+ watcher.on("change", function (event, filename) {
+ try {
+ expect(event).toBe("change");
+ expect(filename).toBe("watch.txt");
+ } catch (e) {
+ err = e;
+ } finally {
+ clearInterval(interval);
+ watcher.close();
+ }
+ });
+
+ watcher.once("close", () => {
+ done(err);
+ });
+
+ const interval = repeat(() => {
+ fs.writeFileSync(filepath, "world");
+ });
+ });
+
+ test("should error on invalid path", done => {
+ try {
+ fs.watch(path.join(testDir, "404.txt"));
+ done(new Error("should not reach here"));
+ } catch (err) {
+ expect(err).toBeInstanceOf(Error);
+ expect(err.code).toBe("ENOENT");
+ expect(err.syscall).toBe("watch");
+ done();
+ }
+ });
+
+ const encodings = ["utf8", "buffer", "hex", "ascii", "base64", "utf16le", "ucs2", "latin1", "binary"];
+
+ test(`should work with encodings ${encodings.join(", ")}`, async () => {
+ const watchers = [];
+ const filepath = path.join(testDir, encodingFileName);
+
+ const promises = [];
+ encodings.forEach(name => {
+ const encoded_filename =
+ name !== "buffer" ? Buffer.from(encodingFileName, "utf8").toString(name) : Buffer.from(encodingFileName);
+
+ promises.push(
+ new Promise((resolve, reject) => {
+ watchers.push(
+ fs.watch(filepath, { encoding: name }, (event, filename) => {
+ try {
+ expect(event).toBe("change");
+
+ if (name !== "buffer") {
+ expect(filename).toBe(encoded_filename);
+ } else {
+ expect(filename).toBeInstanceOf(Buffer);
+ expect(filename.toString("utf8")).toBe(encodingFileName);
+ }
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ }),
+ );
+ }),
+ );
+ });
+
+ const interval = repeat(() => {
+ fs.writeFileSync(filepath, "world");
+ });
+
+ try {
+ await Promise.all(promises);
+ } finally {
+ clearInterval(interval);
+ watchers.forEach(watcher => watcher.close());
+ }
+ });
+
+ test("should work with url", done => {
+ const filepath = path.join(testDir, "url.txt");
+ try {
+ const watcher = fs.watch(pathToFileURL(filepath));
+ let err = undefined;
+ watcher.on("change", function (event, filename) {
+ try {
+ expect(event).toBe("change");
+ expect(filename).toBe("url.txt");
+ } catch (e) {
+ err = e;
+ } finally {
+ clearInterval(interval);
+ watcher.close();
+ }
+ });
+
+ watcher.once("close", () => {
+ done(err);
+ });
+
+ const interval = repeat(() => {
+ fs.writeFileSync(filepath, "world");
+ });
+ } catch (e) {
+ done(e);
+ }
+ });
+
+ test("Signal aborted after creating the watcher", async () => {
+ const filepath = path.join(testDir, "abort.txt");
+
+ const ac = new AbortController();
+ const promise = new Promise((resolve, reject) => {
+ const watcher = fs.watch(filepath, { signal: ac.signal });
+ watcher.once("error", err => (err.message === "The operation was aborted." ? resolve() : reject(err)));
+ watcher.once("close", () => reject());
+ });
+ await Bun.sleep(10);
+ ac.abort();
+ await promise;
+ });
+
+ test("Signal aborted before creating the watcher", async () => {
+ const filepath = path.join(testDir, "abort.txt");
+
+ const signal = AbortSignal.abort();
+ await new Promise((resolve, reject) => {
+ const watcher = fs.watch(filepath, { signal });
+ watcher.once("error", err => (err.message === "The operation was aborted." ? resolve() : reject(err)));
+ watcher.once("close", () => reject());
+ });
+ });
+});
+
+describe("fs.promises.watchFile", () => {
+ test("add file/folder to folder", async () => {
+ let count = 0;
+ const root = path.join(testDir, "add-promise-directory");
+ try {
+ fs.mkdirSync(root);
+ } catch {}
+ let success = false;
+ let err = undefined;
+ try {
+ const ac = new AbortController();
+ const watcher = fs.promises.watch(root, { signal: ac.signal });
+
+ const interval = repeat(() => {
+ fs.writeFileSync(path.join(root, "new-file.txt"), "hello");
+ fs.mkdirSync(path.join(root, "new-folder.txt"));
+ fs.rmdirSync(path.join(root, "new-folder.txt"));
+ });
+
+ for await (const event of watcher) {
+ count++;
+ try {
+ expect(event.eventType).toBe("rename");
+ expect(["new-file.txt", "new-folder.txt"]).toContain(event.filename);
+
+ if (count >= 2) {
+ success = true;
+ clearInterval(interval);
+ ac.abort();
+ }
+ } catch (e) {
+ err = e;
+ clearInterval(interval);
+ ac.abort();
+ }
+ }
+ } catch (e) {
+ if (!success) {
+ throw err || e;
+ }
+ }
+ });
+
+ test("add file/folder to subfolder", async () => {
+ let count = 0;
+ const root = path.join(testDir, "add-promise-subdirectory");
+ try {
+ fs.mkdirSync(root);
+ } catch {}
+ const subfolder = path.join(root, "subfolder");
+ fs.mkdirSync(subfolder);
+ let success = false;
+ let err = undefined;
+
+ try {
+ const ac = new AbortController();
+ const watcher = fs.promises.watch(root, { recursive: true, signal: ac.signal });
+
+ const interval = repeat(() => {
+ fs.writeFileSync(path.join(subfolder, "new-file.txt"), "hello");
+ fs.mkdirSync(path.join(subfolder, "new-folder.txt"));
+ fs.rmdirSync(path.join(subfolder, "new-folder.txt"));
+ });
+ for await (const event of watcher) {
+ const basename = path.basename(event.filename);
+ if (basename === "subfolder") continue;
+
+ count++;
+ try {
+ expect(event.eventType).toBe("rename");
+ expect(["new-file.txt", "new-folder.txt"]).toContain(basename);
+
+ if (count >= 2) {
+ success = true;
+ clearInterval(interval);
+ ac.abort();
+ }
+ } catch (e) {
+ err = e;
+ clearInterval(interval);
+ ac.abort();
+ }
+ }
+ } catch (e) {
+ if (!success) {
+ throw err || e;
+ }
+ }
+ });
+
+ test("Signal aborted after creating the watcher", async () => {
+ const filepath = path.join(testDir, "abort.txt");
+
+ const ac = new AbortController();
+ const watcher = fs.promises.watch(filepath, { signal: ac.signal });
+
+ const promise = (async () => {
+ try {
+ for await (const _ of watcher);
+ } catch (e) {
+ expect(e.message).toBe("The operation was aborted.");
+ }
+ })();
+ await Bun.sleep(10);
+ ac.abort();
+ await promise;
+ });
+
+ test("Signal aborted before creating the watcher", async () => {
+ const filepath = path.join(testDir, "abort.txt");
+
+ const signal = AbortSignal.abort();
+ const watcher = fs.promises.watch(filepath, { signal });
+ await (async () => {
+ try {
+ for await (const _ of watcher);
+ } catch (e) {
+ expect(e.message).toBe("The operation was aborted.");
+ }
+ })();
+ });
+});