diff --git a/src/declarations/stencil-public-compiler.ts b/src/declarations/stencil-public-compiler.ts index a846c88be63..5c863962fec 100644 --- a/src/declarations/stencil-public-compiler.ts +++ b/src/declarations/stencil-public-compiler.ts @@ -1098,7 +1098,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. */ @@ -1300,8 +1317,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 diff --git a/src/sys/node/node-sys.ts b/src/sys/node/node-sys.ts index e92be17ba78..eb60257282d 100644 --- a/src/sys/node/node-sys.ts +++ b/src/sys/node/node-sys.ts @@ -10,6 +10,8 @@ import type TypeScript from 'typescript'; import { buildEvents } from '../../compiler/events'; import type { + CompilerFileWatcher, + CompilerFileWatcherCallback, CompilerSystem, CompilerSystemCreateDirectoryResults, CompilerSystemRealpathResults, @@ -432,6 +434,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; @@ -463,20 +466,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();