Skip to content

Commit

Permalink
fix(compiler): use file system polling events in watch mode
Browse files Browse the repository at this point in the history
this commit updates stencil to use the polling-based file watcher that
was used prior to the typescript 4.9 upgrade. in ts 4.9, the ts compiler
was updated to use filesystem events. since then, we've received reports
of fs events not playing nicely with certain development environments.
for this reason, we revert back to the polling based implementation.
  • Loading branch information
rwaskiewicz committed Mar 13, 2023
1 parent ebac6f8 commit 007db75
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 14 deletions.
28 changes: 27 additions & 1 deletion src/declarations/stencil-public-compiler.ts
Expand Up @@ -1141,7 +1141,24 @@ export interface CompilerSystem {
statSync(p: string): CompilerFsStats;
tmpDirSync(): string;
watchDirectory?(p: string, callback: CompilerFileWatcherCallback, recursive?: boolean): CompilerFileWatcher;
watchFile?(p: string, callback: CompilerFileWatcherCallback): CompilerFileWatcher;

/**
* A `watchFile` implementation in order to hook into the rest of the {@link CompilerSystem} implementation that is
* used when running Stencil's compiler in "watch mode".
*
* It is analogous to TypeScript's `watchFile` implementation.
*
* Note, this function may be called for full builds of Stencil projects by the TypeScript compiler. It should not
* assume that it will only be called in watch mode.
*
* This function should not perform any file watcher registration itself. Each `path` provided to it when called
* should already have been registered as a file to watch.
*
* @param path the path to the file that is being watched
* @param callback a callback to invoke when a file that is being watched has changed in some way
* @returns an object with a method for unhooking the file watcher from the system
*/
watchFile?(path: string, callback: CompilerFileWatcherCallback): CompilerFileWatcher;
/**
* How many milliseconds to wait after a change before calling watch callbacks.
*/
Expand Down Expand Up @@ -1343,8 +1360,17 @@ export interface CompilerBuildStart {
timestamp: string;
}

/**
* A type describing a function to call when an event is emitted by a file watcher
* @param fileName the path of the file tied to event
* @param eventKind a variant describing the type of event that was emitter (added, edited, etc.)
*/
export type CompilerFileWatcherCallback = (fileName: string, eventKind: CompilerFileWatcherEvent) => void;

/**
* A type describing the different types of events that Stencil expects may happen when a file being watched is altered
* in some way
*/
export type CompilerFileWatcherEvent =
| CompilerEventFileAdd
| CompilerEventFileDelete
Expand Down
78 changes: 65 additions & 13 deletions src/sys/node/node-sys.ts
Expand Up @@ -10,6 +10,8 @@ import type TypeScript from 'typescript';

import { buildEvents } from '../../compiler/events';
import type {
CompilerFileWatcher,
CompilerFileWatcherCallback,
CompilerSystem,
CompilerSystemCreateDirectoryResults,
CompilerSystemRealpathResults,
Expand Down Expand Up @@ -445,6 +447,7 @@ export function createNodeSys(c: { process?: any } = {}): CompilerSystem {
return results;
},
setupCompiler(c) {
// save references to typescript utilities so that we can wrap them
const ts: typeof TypeScript = c.ts;
const tsSysWatchDirectory = ts.sys.watchDirectory;
const tsSysWatchFile = ts.sys.watchFile;
Expand Down Expand Up @@ -476,20 +479,69 @@ export function createNodeSys(c: { process?: any } = {}): CompilerSystem {
};
};

sys.watchFile = (p, callback) => {
const tsFileWatcher = tsSysWatchFile(p, (fileName, tsEventKind) => {
fileName = normalizePath(fileName);
if (tsEventKind === ts.FileWatcherEventKind.Created) {
callback(fileName, 'fileAdd');
sys.events.emit('fileAdd', fileName);
} else if (tsEventKind === ts.FileWatcherEventKind.Changed) {
callback(fileName, 'fileUpdate');
sys.events.emit('fileUpdate', fileName);
} else if (tsEventKind === ts.FileWatcherEventKind.Deleted) {
callback(fileName, 'fileDelete');
sys.events.emit('fileDelete', fileName);
/**
* Wrap the TypeScript `watchFile` implementation in order to hook into the rest of the {@link CompilerSystem}
* implementation that is used when running Stencil's compiler in "watch mode" in Node.
*
* The wrapped function calls the default TypeScript `watchFile` implementation for the provided `path`. Based on
* the type of {@link ts.FileWatcherEventKind} emitted, invoke the provided callback and inform the rest of the
* `CompilerSystem` that the event occurred.
*
* This function does not perform any file watcher registration itself. Each `path` provided to it when called
* has already been registered as a file to watch.
*
* @param path the path to the file that is being watched
* @param callback a callback to invoke. The same callback is invoked for every `ts.FileWatcherEventKind`, only
* with a different event classifier string.
* @returns an object with a method for unhooking the file watcher from the system
*/
sys.watchFile = (path: string, callback: CompilerFileWatcherCallback): CompilerFileWatcher => {
const tsFileWatcher = tsSysWatchFile(
path,
(fileName: string, tsEventKind: TypeScript.FileWatcherEventKind) => {
fileName = normalizePath(fileName);
if (tsEventKind === ts.FileWatcherEventKind.Created) {
callback(fileName, 'fileAdd');
sys.events.emit('fileAdd', fileName);
} else if (tsEventKind === ts.FileWatcherEventKind.Changed) {
callback(fileName, 'fileUpdate');
sys.events.emit('fileUpdate', fileName);
} else if (tsEventKind === ts.FileWatcherEventKind.Deleted) {
callback(fileName, 'fileDelete');
sys.events.emit('fileDelete', fileName);
}
},

/**
* When setting up a watcher, a numeric polling interval (in milliseconds) must be set when using
* {@link ts.WatchFileKind.FixedPollingInterval}. Failing to do so may cause the watch process in the
* TypeScript compiler to crash when files are deleted.
*
* This is the value that was used for files in TypeScript 4.8.4. The value is hardcoded as TS does not
* export this value/make it publicly available.
*/
250,

/**
* As of TypeScript v4.9, the default file watcher implementation is based on file system events, and moves
* away from the previous polling based implementation. When attempting to use the file system events-based
* implementation, issues with the dev server (which runs "watch mode") were reported, stating that the
* compiler was continuously recompiling and reloading the dev server. It was found that in some cases, this
* would be caused by the access time (`atime`) on a non-TypeScript file being update by some process on the
* user's machine. For now, we default back to the poll-based implementation to avoid such issues, and will
* revisit this functionality in the future.
*
* Ref: {@link https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#file-watching-now-uses-file-system-events|TS 4.9 Release Note}
*
* TODO(STENCIL-744): Revisit using file system events for watch mode
*/
{
// TS 4.8 and under defaulted to this type of polling interval for polling-based watchers
watchFile: ts.WatchFileKind.FixedPollingInterval,
// set fallbackPolling so that directories are given the correct watcher variant
fallbackPolling: ts.PollingWatchKind.FixedInterval,
}
});
);

const close = () => {
tsFileWatcher.close();
Expand Down

0 comments on commit 007db75

Please sign in to comment.