-
-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: removed "node-watch" dependency in favor of custom implemen…
…tation Yes, I understand the trade offs and that this means more maintenance. However: - node-watch relies on the temp directory, which may not always work well on all hosting platforms, such as W3Schools Spaces - This helps reduce package size further - We don't need all the features node-watch has - This implementation is similar in that we recursively support Linux and symlinks - The API can be altered to our liking - Easier to build in new features without complex wrappers
- Loading branch information
Showing
6 changed files
with
159 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@roboplay/robo.js': patch | ||
--- | ||
|
||
refactor: removed "node-watch" dependency in favor of custom implementation |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import { promises as fs, watch, FSWatcher } from 'node:fs' | ||
import path from 'node:path' | ||
import os from 'node:os' | ||
import { logger } from '../../core/logger.js' | ||
import { hasProperties } from './utils.js' | ||
|
||
// Defining the possible values for file changes. | ||
type ChangeType = 'added' | 'removed' | 'changed' | ||
|
||
// The interface for options parameter allowing to exclude certain directories. | ||
interface Options { | ||
exclude?: string[] | ||
} | ||
|
||
// The interface for the callback function. | ||
interface Callback { | ||
(changeType: ChangeType, filename: string, dirPath: string): void | ||
} | ||
|
||
// Watcher class will monitor files and directories for changes. | ||
export default class Watcher { | ||
// Map to keep track of FSWatcher instances for each watched file. | ||
private watchers: Map<string, FSWatcher> = new Map() | ||
// Map to keep track of last modification date for each watched file. | ||
private watchedFiles: Map<string, Date> = new Map() | ||
// A flag to avoid triggering callbacks on the initial setup. | ||
private isFirstTime = true | ||
|
||
// Initialize with paths to watch and options. | ||
constructor(private paths: string[], private options: Options) {} | ||
|
||
// Start the watcher. Files are read, and callbacks are set up. | ||
async start(callback: Callback) { | ||
await Promise.all( | ||
this.paths.map((filePath) => { | ||
return this.watchPath(filePath, this.options, callback) | ||
}) | ||
) | ||
// After setting up, mark this as no longer the first time. | ||
this.isFirstTime = false | ||
} | ||
|
||
// Stop the watcher. Close all FSWatcher instances and clear the map. | ||
stop() { | ||
this.watchers.forEach((watcher) => { | ||
watcher.close() | ||
}) | ||
this.watchers.clear() | ||
} | ||
|
||
// Retry function that repeats a promise-returning function up to a number of times. | ||
private async retry<T>(fn: () => Promise<T>, retries = 2): Promise<T> { | ||
try { | ||
return await fn() | ||
} catch (err) { | ||
if (retries === 0) { | ||
throw err | ||
} | ||
await new Promise((resolve) => setTimeout(resolve, 1000)) | ||
return await this.retry(fn, retries - 1) | ||
} | ||
} | ||
|
||
// Set up watching a path. Recursively applies to directories, unless excluded by options. | ||
private async watchPath(targetPath: string, options: Options, callback: Callback) { | ||
const stats = await fs.lstat(targetPath) | ||
|
||
if (stats.isFile()) { | ||
// If a file, start watching the file. | ||
this.watchFile(targetPath, callback) | ||
// Fire the callback if not first time. | ||
if (!this.isFirstTime) { | ||
callback('added', path.basename(targetPath), path.dirname(targetPath)) | ||
} | ||
} else if ( | ||
stats.isDirectory() && | ||
(!options.exclude || !options.exclude.includes(path.basename(targetPath))) | ||
) { | ||
// If a directory, read all the contents and watch them. | ||
const files = await this.retry(() => fs.readdir(targetPath, { withFileTypes: true })) | ||
|
||
for (const file of files) { | ||
const filePath = path.join(targetPath, file.name) | ||
await this.watchPath(filePath, options, callback) | ||
} | ||
|
||
// Special handling for Linux: Also watch the directory for new files. | ||
if (os.platform() === 'linux') { | ||
const watcher = watch(targetPath, async (event, filename) => { | ||
if (filename) { | ||
const newFilePath = path.join(targetPath, filename) | ||
const stats = await fs.lstat(newFilePath) | ||
if (stats.isDirectory() && !this.watchers.has(newFilePath)) { | ||
await this.watchPath(newFilePath, options, callback) | ||
} | ||
} | ||
}) | ||
|
||
this.watchers.set(targetPath, watcher) | ||
} | ||
} else if (stats.isSymbolicLink()) { | ||
// If a symlink, resolve the real path and watch that. | ||
const realPath = await fs.realpath(targetPath) | ||
await this.watchPath(realPath, options, callback) | ||
} | ||
} | ||
|
||
// Watch a single file. Set up the FSWatcher and callback for changes. | ||
private watchFile(filePath: string, callback: Callback) { | ||
const watcher = watch(filePath, async (event, filename) => { | ||
if (event === 'rename') { | ||
// If the file is renamed, try to access it. If it exists, it was added. Otherwise, removed. | ||
try { | ||
await this.retry(() => fs.access(filePath, fs.constants.F_OK)) | ||
if (!this.isFirstTime) { | ||
callback('added', filename, path.dirname(filePath)) | ||
} | ||
} catch (e) { | ||
// If the file is not found, it was removed. | ||
if (hasProperties<{ code: unknown }>(e, ['code']) && e.code === 'ENOENT') { | ||
const watcher = this.watchers.get(filePath) | ||
if (watcher) { | ||
callback('removed', filename, path.dirname(filePath)) | ||
watcher.close() | ||
this.watchers.delete(filePath) | ||
} | ||
} else { | ||
logger.error(`Unable to access file: ${filePath}`) | ||
} | ||
} | ||
} else if (event === 'change') { | ||
// If the file changed, check the modification time and trigger the callback if it's a new change. | ||
const stat = await fs.lstat(filePath) | ||
if (this.watchedFiles.get(filePath)?.getTime() !== stat.mtime.getTime()) { | ||
this.watchedFiles.set(filePath, stat.mtime) | ||
if (!this.isFirstTime) { | ||
callback('changed', filename, path.dirname(filePath)) | ||
} | ||
} | ||
} | ||
}) | ||
|
||
this.watchers.set(filePath, watcher) | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
2da1fba
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
robo – ./
robo-git-main-wave-play.vercel.app
docs.roboplay.dev
robo-wave-play.vercel.app
robo-alpha.vercel.app