diff --git a/packages/cli/package.json b/packages/cli/package.json index 91ab66f36..f50cc9027 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -9,7 +9,7 @@ "stc-format": "bin/stc-format.js" }, "scripts": { - "test": "mocha \"./dist/test/**/*.spec.js\" --timeout 25000" + "test": "mocha \"./dist/test/**/*.spec.js\"" }, "dependencies": { "@file-services/node": "^9.4.1", diff --git a/packages/cli/src/base-generator.ts b/packages/cli/src/base-generator.ts index 69557f039..5a69c185e 100644 --- a/packages/cli/src/base-generator.ts +++ b/packages/cli/src/base-generator.ts @@ -71,12 +71,12 @@ export class IndexGenerator { ); } - public async generateIndexFile(fs: IFileSystem) { + public generateIndexFile(fs: IFileSystem) { const indexFileContent = this.generateIndexSource(); ensureDirectory(fs.dirname(this.indexFileTargetPath), fs); - await tryRun( - () => fs.promises.writeFile(this.indexFileTargetPath, '\n' + indexFileContent + '\n'), + tryRun( + () => fs.writeFileSync(this.indexFileTargetPath, '\n' + indexFileContent + '\n'), 'Write Index File Error', ); diff --git a/packages/cli/src/build-stylable.ts b/packages/cli/src/build-stylable.ts index be40eff65..0e41a876c 100644 --- a/packages/cli/src/build-stylable.ts +++ b/packages/cli/src/build-stylable.ts @@ -1,4 +1,4 @@ -import { nodeFs as fs } from '@file-services/node'; +import { nodeFs } from '@file-services/node'; import { Stylable, StylableConfig } from '@stylable/core'; import { StylableResolverCache, validateDefaultConfig } from '@stylable/core/dist/index-internal'; import { build } from './build'; @@ -12,6 +12,7 @@ import { DiagnosticsManager } from './diagnostics-manager'; import { createDefaultLogger, levels } from './logger'; import type { BuildContext, BuildOptions } from './types'; import { WatchHandler } from './watch-handler'; +import { createWatchService } from './watch-service'; export interface BuildStylableContext extends Partial>, @@ -28,12 +29,12 @@ export interface BuildStylableContext }; } -export async function buildStylable( +export function buildStylable( rootDir: string, { defaultOptions = createDefaultOptions(), overrideBuildOptions = {}, - fs: fileSystem = fs, + fs = nodeFs, log = createDefaultLogger(), watch = false, resolverCache = new Map(), @@ -59,8 +60,9 @@ export async function buildStylable( const { config } = resolveConfig(rootDir, fs, configFilePath) || {}; validateDefaultConfig(config?.defaultConfig); - const projects = await projectsConfig(rootDir, overrideBuildOptions, defaultOptions, config); - const watchHandler = new WatchHandler(fileSystem, { + const projects = projectsConfig(rootDir, overrideBuildOptions, defaultOptions, config); + const watchService = createWatchService(fs); + const watchHandler = new WatchHandler(fs, watchService, { log, resolverCache, outputFiles, @@ -89,7 +91,7 @@ export async function buildStylable( } const stylable = new Stylable({ - fileSystem, + fileSystem: fs, requireModule, projectRoot, resolverCache, @@ -102,11 +104,12 @@ export async function buildStylable( requireModule('@stylable/node').resolveNamespace, }); - const { service, generatedFiles } = await build(buildOptions, { + const { service, generatedFiles } = build(buildOptions, { watch, stylable, log, - fs: fileSystem, + fs, + watchService, rootDir, projectRoot, outputFiles, diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index b5346c797..51c71dfc7 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -14,8 +14,9 @@ import { sortModulesByDepth } from '@stylable/build-tools'; import { StylableOptimizer } from '@stylable/optimizer'; import type { Stylable } from '@stylable/core'; import type { IFileSystem } from '@file-services/types'; +import { createWatchService } from './watch-service'; -export async function build( +export function build( { srcDir, outDir, @@ -49,6 +50,7 @@ export async function build( identifier = _projectRoot, watch, fs, + watchService = createWatchService(fs), stylable, log, outputFiles = new Map(), @@ -87,7 +89,7 @@ export async function build( fs, ); - const service = new DirectoryProcessService(fs, { + const service = new DirectoryProcessService(fs, watchService, { watchMode: watch, autoResetInvalidations: true, watchOptions: { @@ -129,7 +131,7 @@ export async function build( throw error; } }, - async processFiles(_, affectedFiles, deletedFiles, changeOrigin) { + processFiles(_, affectedFiles, deletedFiles, changeOrigin) { if (changeOrigin) { // handle deleted files by removing their generated content if (deletedFiles.size) { @@ -196,7 +198,7 @@ export async function build( // rewire invalidations updateWatcherDependencies(affectedFiles); // rebuild assets from aggregated content: index files and assets - await buildAggregatedEntities(affectedFiles, processGeneratedFiles); + buildAggregatedEntities(affectedFiles, processGeneratedFiles); // rebundle if (bundle) { tryRun(() => { @@ -242,7 +244,7 @@ export async function build( }, }); - await service.init(fullSrcDir); + service.init(fullSrcDir); if (sourceFiles.size === 0) { log(mode, buildMessages.BUILD_SKIPPED(isMultiPackagesProject ? identifier : undefined)); @@ -362,9 +364,9 @@ export async function build( }); } - async function buildAggregatedEntities(affectedFiles: Set, generated: Set) { + function buildAggregatedEntities(affectedFiles: Set, generated: Set) { if (indexFileGenerator) { - await indexFileGenerator.generateIndexFile(fs); + indexFileGenerator.generateIndexFile(fs); generated.add(indexFileGenerator.indexFileTargetPath); outputFiles.set(indexFileGenerator.indexFileTargetPath, affectedFiles); diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 39125af01..cb5956aa9 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -4,64 +4,57 @@ import { buildStylable } from './build-stylable'; import { createDefaultOptions, getCliArguments, resolveCliOptions } from './config/resolve-options'; import { createLogger } from './logger'; -async function main() { - const argv = getCliArguments(); - const { resolve } = fs; - const { - watch, - require: requires, - log: shouldLog, - namespaceResolver, - preserveWatchOutput, - config, - } = argv; - const rootDir = resolve(argv.rootDir); - const explicitResolveNs = - namespaceResolver && - require( - require.resolve(namespaceResolver, { - paths: [rootDir], - }), - ); - - // - const log = createLogger( - (level, ...messages) => { - if (shouldLog || level === 'info') { - const currentTime = new Date().toLocaleTimeString(); - console.log('[Stylable]', `[${currentTime}]`, ...messages); - } - }, - () => !shouldLog && !preserveWatchOutput && console.clear(), +const argv = getCliArguments(); +const { resolve } = fs; +const { + watch, + require: requires, + log: shouldLog, + namespaceResolver, + preserveWatchOutput, + config, +} = argv; +const rootDir = resolve(argv.rootDir); +const explicitResolveNs = + namespaceResolver && + require( + require.resolve(namespaceResolver, { + paths: [rootDir], + }), ); - // execute all require hooks before running the CLI build - for (const request of requires) { - require(request); - } +// +const log = createLogger( + (level, ...messages) => { + if (shouldLog || level === 'info') { + const currentTime = new Date().toLocaleTimeString(); + console.log('[Stylable]', `[${currentTime}]`, ...messages); + } + }, + () => !shouldLog && !preserveWatchOutput && console.clear(), +); - const defaultOptions = createDefaultOptions(); - const overrideBuildOptions = resolveCliOptions(argv, defaultOptions); - const { watchHandler } = await buildStylable(rootDir, { - overrideBuildOptions, - defaultOptions, - fs, - resolveNamespace: explicitResolveNs?.resolveNamespace, - watch, - log, - configFilePath: config, - }); +// execute all require hooks before running the CLI build +for (const request of requires) { + require(request); +} - process.on('SIGTERM', () => { - void watchHandler.stop(); - }); +const defaultOptions = createDefaultOptions(); +const overrideBuildOptions = resolveCliOptions(argv, defaultOptions); +const { watchHandler } = buildStylable(rootDir, { + overrideBuildOptions, + defaultOptions, + fs, + resolveNamespace: explicitResolveNs?.resolveNamespace, + watch, + log, + configFilePath: config, +}); - process.on('SIGINT', () => { - void watchHandler.stop(); - }); -} +process.on('SIGTERM', () => { + watchHandler.stop(); +}); -main().catch((e) => { - process.exitCode = 1; - console.error(e); +process.on('SIGINT', () => { + watchHandler.stop(); }); diff --git a/packages/cli/src/config/projects-config.ts b/packages/cli/src/config/projects-config.ts index 0a9d49884..b00fa339a 100644 --- a/packages/cli/src/config/projects-config.ts +++ b/packages/cli/src/config/projects-config.ts @@ -16,7 +16,7 @@ import { resolveNpmRequests } from './resolve-requests'; import type { StylableConfig } from '@stylable/core'; import type { IFileSystem } from '@file-services/types'; -interface StylableRuntimeConfigs { +export interface StylableRuntimeConfigs { stcConfig?: Configuration | undefined; defaultConfig?: Pick< StylableConfig, @@ -28,12 +28,12 @@ interface StylableRuntimeConfigs { >; } -export async function projectsConfig( +export function projectsConfig( rootDir: string, overrideBuildOptions: Partial, defaultOptions: BuildOptions = createDefaultOptions(), config?: StylableRuntimeConfigs, -): Promise { +): STCProjects { const topLevelOptions = mergeBuildOptions( defaultOptions, config?.stcConfig?.options, @@ -42,29 +42,21 @@ export async function projectsConfig( validateOptions(topLevelOptions); - let projects: STCProjects; - - if (isMultipleConfigProject(config)) { - const { entities } = processProjects(config.stcConfig, { - defaultOptions: topLevelOptions, - }); - - projects = await resolveProjectsRequests({ - rootDir, - entities, - resolveRequests: - config.stcConfig.projectsOptions?.resolveRequests ?? resolveNpmRequests, - }); - } else { - projects = [ - { - projectRoot: rootDir, - options: [topLevelOptions], - }, - ]; - } - - return projects; + return isMultipleConfigProject(config) + ? resolveProjectsRequests({ + rootDir, + entities: processProjects(config.stcConfig, { + defaultOptions: topLevelOptions, + }).entities, + resolveRequests: + config.stcConfig.projectsOptions?.resolveRequests ?? resolveNpmRequests, + }) + : [ + { + projectRoot: rootDir, + options: [topLevelOptions], + }, + ]; } export function resolveConfig(context: string, fs: IFileSystem, request?: string) { @@ -110,7 +102,7 @@ function isMultipleConfigProject( return Boolean(config?.stcConfig?.projects); } -async function resolveProjectsRequests({ +function resolveProjectsRequests({ entities, rootDir, resolveRequests, @@ -118,7 +110,7 @@ async function resolveProjectsRequests({ rootDir: string; entities: Array; resolveRequests: ResolveRequests; -}): Promise { +}): STCProjects { const context: ResolveProjectsContext = { rootDir }; return resolveRequests(entities, context); diff --git a/packages/cli/src/directory-process-service/directory-process-service.ts b/packages/cli/src/directory-process-service/directory-process-service.ts index 09237c637..752de64dc 100644 --- a/packages/cli/src/directory-process-service/directory-process-service.ts +++ b/packages/cli/src/directory-process-service/directory-process-service.ts @@ -1,5 +1,5 @@ -import nodeFs from '@file-services/node'; import type { IFileSystem, IWatchEvent } from '@file-services/types'; +import { createWatchEvent, type WatchService } from '../watch-service'; import { directoryDeepChildren, DirectoryItem } from './walk-fs'; export interface DirectoryProcessServiceOptions { @@ -8,7 +8,7 @@ export interface DirectoryProcessServiceOptions { affectedFiles: Set, deletedFiles: Set, changeOrigin?: IWatchEvent, - ): Promise<{ generatedFiles: Set }> | { generatedFiles: Set }; + ): { generatedFiles: Set }; directoryFilter?(directoryPath: string): boolean; fileFilter?(filePath: string): boolean; onError?(error: Error): void; @@ -24,6 +24,7 @@ export class DirectoryProcessService { public watchedDirectoryFiles = new Map>(); constructor( private fs: IFileSystem, + private watchService: WatchService, private options: DirectoryProcessServiceOptions = {}, ) { if (this.options.watchMode && !this.options.watchOptions?.skipInitialWatch) { @@ -31,32 +32,32 @@ export class DirectoryProcessService { } } public startWatch() { - this.fs.watchService.addGlobalListener(this.watchHandler); + this.watchService.addGlobalListener(this.watchHandler); } - public async dispose() { + public dispose() { for (const path of this.watchedDirectoryFiles.keys()) { - await this.fs.watchService.unwatchPath(path); + this.watchService.unwatchPath(path); } this.invalidationMap.clear(); this.watchedDirectoryFiles.clear(); } - public async init(directoryPath: string) { - await this.watchPath(directoryPath); + public init(directoryPath: string, shouldProcess = true): Set { + this.watchDirectory(directoryPath); const items = directoryDeepChildren(this.fs, directoryPath, this.filterWatchItems); const affectedFiles = new Set(); - for await (const item of items) { + for (const item of items) { if (item.type === 'directory') { - await this.watchPath(item.path); + this.watchDirectory(item.path); } else if (item.type === 'file') { affectedFiles.add(item.path); this.addFileToWatchedDirectory(item.path); this.registerInvalidateOnChange(item.path); } } - if (affectedFiles.size) { + if (shouldProcess && affectedFiles.size) { try { - await this.options.processFiles?.(this, affectedFiles, new Set()); + this.options.processFiles?.(this, affectedFiles, new Set()); } catch (error) { this.options.onError?.(error as Error); } @@ -94,27 +95,27 @@ export class DirectoryProcessService { fileSet.add(filePathToInvalidate); } } - private watchPath(directoryPath: string) { + private watchDirectory(directoryPath: string) { if (!this.options.watchMode) { return; } this.watchedDirectoryFiles.set(directoryPath, new Set()); - return this.fs.watchService.watchPath(directoryPath); + return this.watchService.watchPath(directoryPath); } - public async handleWatchChange( + public handleWatchChange( files: Map, originalEvent: IWatchEvent, - ): Promise<{ + ): { hasChanges: boolean; generatedFiles: Set; - }> { + } { const affectedFiles = new Set(); const deletedFiles = new Set(); for (const event of files.values()) { if (event.stats?.isDirectory()) { if (this.options.directoryFilter?.(event.path) ?? true) { - for (const filePath of await this.init(event.path)) { + for (const filePath of this.init(event.path, false)) { affectedFiles.add(filePath); } } @@ -163,7 +164,7 @@ export class DirectoryProcessService { } if (this.options.processFiles && (affectedFiles.size || deletedFiles.size)) { - const { generatedFiles } = await this.options.processFiles( + const { generatedFiles } = this.options.processFiles( this, affectedFiles, deletedFiles, @@ -203,7 +204,11 @@ export class DirectoryProcessService { files.set(file, createWatchEvent(file, this.fs)); } - this.handleWatchChange(files, event).catch((error) => this.options.onError?.(error)); + try { + this.handleWatchChange(files, event); + } catch (error) { + this.options.onError?.(error as Error); + } }; private filterWatchItems = (event: DirectoryItem): boolean => { const { fileFilter, directoryFilter } = this.options; @@ -215,10 +220,3 @@ export class DirectoryProcessService { return false; }; } - -export function createWatchEvent(filePath: string, fs = nodeFs): IWatchEvent { - return { - path: filePath, - stats: fs.existsSync(filePath) ? fs.statSync(filePath) : null, - }; -} diff --git a/packages/cli/src/directory-process-service/walk-fs.ts b/packages/cli/src/directory-process-service/walk-fs.ts index 2ebffaa58..451aa7f4c 100644 --- a/packages/cli/src/directory-process-service/walk-fs.ts +++ b/packages/cli/src/directory-process-service/walk-fs.ts @@ -13,13 +13,13 @@ export interface DirectoryItem { * @param directoryPath directory to iterate into. * @param basePath base directory to compute relative paths from. defaults to `directoryPath`. */ -export async function* directoryDeepChildren( +export function* directoryDeepChildren( fs: IFileSystem, directoryPath: string, filterItem: (item: DirectoryItem) => boolean = returnsTrue, basePath = directoryPath, -): AsyncGenerator { - for (const item of await fs.promises.readdir(directoryPath, { +): Generator { + for (const item of fs.readdirSync(directoryPath, { withFileTypes: true, })) { const itemName = item.name; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index ff38a0cfc..e319decc4 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -21,9 +21,9 @@ export { DiagnosticsManager } from './diagnostics-manager'; export { DirectoryProcessService, DirectoryProcessServiceOptions, - createWatchEvent, } from './directory-process-service/directory-process-service'; export { STCBuilder } from './stc-builder'; export { BuildStylableContext, buildStylable } from './build-stylable'; export { buildDTS } from './build-single-file'; export type { CodeMod } from './code-mods/types'; +export { createWatchService, createWatchEvent, type WatchService } from './watch-service'; diff --git a/packages/cli/src/stc-builder.ts b/packages/cli/src/stc-builder.ts index 966a13ee6..b8bc509ec 100644 --- a/packages/cli/src/stc-builder.ts +++ b/packages/cli/src/stc-builder.ts @@ -1,7 +1,6 @@ import { nodeFs } from '@file-services/node'; import { buildStylable } from './build-stylable'; import { DiagnosticsManager } from './diagnostics-manager'; -import { createWatchEvent } from './directory-process-service/directory-process-service'; import { createLogger, Log } from './logger'; import type { IFileSystem } from '@file-services/types'; import type { DiagnosticMessages } from './report-diagnostics'; @@ -12,6 +11,7 @@ import { EmitDiagnosticsContext, reportDiagnostic, } from '@stylable/core/dist/index-internal'; +import { createWatchEvent } from './watch-service'; export type STCBuilderFileSystem = Pick; @@ -80,7 +80,7 @@ export class STCBuilder { * * @param modifiedFiles {Iterable} list of absolute file path that have been modified since the last build execution. */ - public rebuild = async (modifiedFiles: Iterable = []): Promise => { + public rebuild = (modifiedFiles: Iterable = []): void => { if (this.watchHandler) { return this.rebuildModifiedFiles(modifiedFiles); } else { @@ -91,8 +91,8 @@ export class STCBuilder { /** * Executes a fresh build of the Stylable project. */ - public build = async () => { - const buildOutput = await buildStylable(this.rootDir, { + public build = () => { + const buildOutput = buildStylable(this.rootDir, { diagnosticsManager: this.diagnosticsManager, log: this.log, configFilePath: this.configFilePath, @@ -182,7 +182,7 @@ export class STCBuilder { * Executes an incremental build of modified files. * @param modifiedFiles {Iterable} list of absolute file path that have been modified since the last build execution. */ - private rebuildModifiedFiles = async (modifiedFiles: Iterable) => { + private rebuildModifiedFiles = (modifiedFiles: Iterable) => { if (!this.watchHandler) { throw createSTCBuilderError(diagnostics.INVALID_WATCH_HANDLER('handleWatchedFiles')); } @@ -190,9 +190,10 @@ export class STCBuilder { for (const filePath of modifiedFiles) { const event = createWatchEvent( this.fs.existsSync(filePath) ? this.fs.realpathSync(filePath) : filePath, + nodeFs, ); - await this.watchHandler.listener(event); + this.watchHandler.listener(event); } }; } diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index b583e1a82..efe5dd10e 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -3,6 +3,7 @@ import type { Stylable } from '@stylable/core'; import type { IndexGenerator } from './base-generator'; import type { DiagnosticsManager, DiagnosticsMode } from './diagnostics-manager'; import type { Log } from './logger'; +import type { WatchService } from './watch-service'; export type PartialBuildOptions = Partial; @@ -48,7 +49,7 @@ export type STCProjects = ProjectEntity[]; export type ResolveRequests = ( projects: Array, context: ResolveProjectsContext, -) => Promise | STCProjects; +) => STCProjects; export interface ResolveProjectsContext { rootDir: string; @@ -186,6 +187,10 @@ export interface BuildContext { projectRoot: string; /** provide a custom file-system for the build */ fs: IFileSystem; + + /** optional watch service to use when watching */ + watchService?: WatchService; + /** provide Stylable instance */ stylable: Stylable; /** log function */ diff --git a/packages/cli/src/watch-debounced.ts b/packages/cli/src/watch-debounced.ts new file mode 100644 index 000000000..dfd928456 --- /dev/null +++ b/packages/cli/src/watch-debounced.ts @@ -0,0 +1,55 @@ +import type { IFileSystem } from '@file-services/types'; + +export interface DebouncedWatcher { + close(): void; +} + +/** + * Watches a target for changes, while debouncing events per path, so that if several "change" events are emitted + * for the same file within `pathEventsDebounce` ms, only one event is emitted. + */ +export function watchDebounced( + fs: IFileSystem, + directoryPath: string, + onEvent: (eventType: 'change' | 'rename', relativePath: string) => void, + pathEventsDebounce = 50, +): DebouncedWatcher { + const watcher = fs.watch(directoryPath); + + const pathToTimer = new Map>(); + + function onChange(eventType: 'change' | 'rename', relativePath: string | Buffer | null): void { + if (typeof relativePath !== 'string') { + return; + } + + const timer = pathToTimer.get(relativePath); + if (timer !== undefined) { + clearTimeout(timer); + } + const timerId = setTimeout(() => { + pathToTimer.delete(relativePath); + onEvent(eventType, relativePath); + }, pathEventsDebounce); + + pathToTimer.set(relativePath, timerId); + } + + watcher.on('change', onChange); + watcher.on('error', () => { + // ignore internal watcher errors. + // we could log them somewhere, but using console.log spams the console + // if no listener, process crashes + }); + + return { + close() { + watcher.off('change', onChange); + watcher.close(); + for (const timerId of pathToTimer.values()) { + clearTimeout(timerId); + } + pathToTimer.clear(); + }, + }; +} diff --git a/packages/cli/src/watch-handler.ts b/packages/cli/src/watch-handler.ts index e1861d0b9..1dc862a65 100644 --- a/packages/cli/src/watch-handler.ts +++ b/packages/cli/src/watch-handler.ts @@ -1,15 +1,13 @@ -import type { IFileSystem, IWatchEvent, WatchEventListener } from '@file-services/types'; +import type { IFileSystem, IWatchEvent } from '@file-services/types'; import type { Stylable } from '@stylable/core'; import type { StylableResolverCache } from '@stylable/core/dist/index-internal'; import type { BuildContext } from './types'; import decache from 'decache'; -import { - createWatchEvent, - DirectoryProcessService, -} from './directory-process-service/directory-process-service'; +import { DirectoryProcessService } from './directory-process-service/directory-process-service'; import { createDefaultLogger, levels, Log } from './logger'; import { buildMessages } from './messages'; import { DiagnosticsManager } from './diagnostics-manager'; +import { createWatchEvent, type WatchService } from './watch-service'; export interface WatchHandlerOptions { log?: Log; @@ -42,6 +40,8 @@ export class WatchHandler { constructor( private fileSystem: IFileSystem, + private watchService: WatchService, + private options: WatchHandlerOptions = {}, ) { this.resolverCache = this.options.resolverCache ?? new Map(); @@ -50,7 +50,7 @@ export class WatchHandler { this.options.diagnosticsManager ?? new DiagnosticsManager({ log: this.log }); } - public readonly listener = async (event: IWatchEvent) => { + public readonly listener = (event: IWatchEvent) => { this.log(buildMessages.CHANGE_EVENT_TRIGGERED(event.path)); if (this.generatedFiles.has(event.path)) { @@ -72,7 +72,7 @@ export class WatchHandler { files.set(path, createWatchEvent(path, this.fileSystem)); } - const { hasChanges, generatedFiles } = await service.handleWatchChange(files, event); + const { hasChanges, generatedFiles } = service.handleWatchChange(files, event); if (hasChanges) { if (!foundChanges) { @@ -126,19 +126,20 @@ export class WatchHandler { public start() { this.log(buildMessages.START_WATCHING(), levels.info); - this.fileSystem.watchService.addGlobalListener(this.listener as WatchEventListener); + this.watchService.addGlobalListener(this.listener); } - public async stop() { + public stop() { this.log(buildMessages.STOP_WATCHING(), levels.info); this.diagnosticsManager.clear(); - this.fileSystem.watchService.removeGlobalListener(this.listener as WatchEventListener); + this.watchService.removeGlobalListener(this.listener); for (const { service } of this.builds) { - await service.dispose(); + service.dispose(); } this.builds = []; + this.watchService.dispose(); } private invalidateCache(filePath: string) { diff --git a/packages/cli/src/watch-service.ts b/packages/cli/src/watch-service.ts new file mode 100644 index 000000000..6a813a5c1 --- /dev/null +++ b/packages/cli/src/watch-service.ts @@ -0,0 +1,81 @@ +import type { IFileSystem, IWatchEvent } from '@file-services/types'; +import { watchDebounced, type DebouncedWatcher } from './watch-debounced'; + +export interface WatchService { + watchPath(path: string): void; + unwatchPath(path: string): void; + dispose(): void; + addGlobalListener(listener: (event: IWatchEvent) => void): void; + removeGlobalListener(listener: (event: IWatchEvent) => void): void; +} + +export function createWatchService(fs: IFileSystem): WatchService { + const watchedPaths = new Map(); + const globalListeners = new Set<(event: IWatchEvent) => void>(); + + const rewatchPath = (filePath: string) => { + const existingWatcher = watchedPaths.get(filePath); + existingWatcher?.close(); + watchedPaths.delete(filePath); + watchService.watchPath(filePath); + }; + + const watchService: WatchService = { + watchPath(path) { + if (watchedPaths.has(path)) { + return; + } + const watcher = watchDebounced(fs, path, (eventType, relativePath) => { + const filePath = fs.join(path, relativePath); + const event = createWatchEvent(filePath, fs); + if (eventType === 'rename' && watchedPaths.has(filePath)) { + if (event.stats) { + rewatchPath(filePath); + } else { + watchedPaths.get(filePath)?.close(); + watchedPaths.delete(filePath); + } + } + for (const listener of globalListeners) { + listener(event); + } + }); + watchedPaths.set(path, watcher); + }, + unwatchPath(path) { + const watcher = watchedPaths.get(path); + if (watcher) { + watcher.close(); + watchedPaths.delete(path); + } + }, + dispose() { + for (const watcher of watchedPaths.values()) { + watcher.close(); + } + watchedPaths.clear(); + }, + addGlobalListener(listener) { + globalListeners.add(listener); + }, + removeGlobalListener(listener) { + globalListeners.delete(listener); + }, + }; + return watchService; +} + +export function createWatchEvent(filePath: string, fs: IFileSystem): IWatchEvent { + return { + path: filePath, + stats: statSyncSafe(filePath, fs), + }; +} + +function statSyncSafe(path: string, fs: IFileSystem) { + try { + return fs.statSync(path, { throwIfNoEntry: false }) ?? null; + } catch { + return null; + } +} diff --git a/packages/cli/test/assets.spec.ts b/packages/cli/test/assets.spec.ts index d74301a84..b01ba5080 100644 --- a/packages/cli/test/assets.spec.ts +++ b/packages/cli/test/assets.spec.ts @@ -4,7 +4,7 @@ import { build } from '@stylable/cli'; import { createMemoryFs } from '@file-services/memory'; describe('assets', function () { - it('should copy imported relative native css', async () => { + it('should copy imported relative native css', () => { const fs = createMemoryFs({ '/package.json': `{"name": "test", "version": "0.0.0"}`, '/src/entry.st.css': ` @@ -31,7 +31,7 @@ describe('assets', function () { }, }); - await build( + build( { srcDir: 'src', outDir: 'dist', @@ -56,7 +56,7 @@ describe('assets', function () { 'custom-resolved.css', ]); }); - it('should create and link native CSS in JS module', async () => { + it('should create and link native CSS in JS module', () => { const fs = createMemoryFs({ '/package.json': `{"name": "test", "version": "0.0.0"}`, '/src/entry.st.css': ` @@ -70,7 +70,7 @@ describe('assets', function () { requireModule: () => ({}), }); - await build( + build( { srcDir: 'src', outDir: 'dist', diff --git a/packages/cli/test/build.spec.ts b/packages/cli/test/build.spec.ts index bc7346548..50e7fccad 100644 --- a/packages/cli/test/build.spec.ts +++ b/packages/cli/test/build.spec.ts @@ -16,7 +16,7 @@ const log = () => { }; describe('build stand alone', () => { - it('should create modules and copy source css files', async () => { + it('should create modules and copy source css files', () => { const fs = createMemoryFs({ '/main.st.css': ` :import{ @@ -41,7 +41,7 @@ describe('build stand alone', () => { requireModule: () => ({}), }); - await build( + build( { outDir: 'lib', srcDir: '.', @@ -69,7 +69,7 @@ describe('build stand alone', () => { // assure no index file was generated by default expect(fs.existsSync('/lib/index.st.css'), '/lib/index.st.css').to.equal(false); }); - it('should import native css files in the js module', async () => { + it('should import native css files in the js module', () => { const fs = createMemoryFs({ '/main.st.css': ` @st-import "./global.css"; @@ -87,7 +87,7 @@ describe('build stand alone', () => { requireModule: () => ({}), }); - await build( + build( { outDir: 'lib', srcDir: '.', @@ -120,7 +120,7 @@ describe('build stand alone', () => { ); }); - it('should use "useNamespaceReference" to maintain a single namespace for all builds using it', async () => { + it('should use "useNamespaceReference" to maintain a single namespace for all builds using it', () => { const fs = createMemoryFs({ '/src/main.st.css': ` :import{ @@ -151,7 +151,7 @@ describe('build stand alone', () => { }, }); - await build( + build( { srcDir: 'src', outDir: 'cjs', @@ -181,7 +181,7 @@ describe('build stand alone', () => { 'st-namespace-reference="../src/main.st.css"', ); - await build( + build( { srcDir: 'cjs', outDir: 'cjs2', @@ -203,7 +203,7 @@ describe('build stand alone', () => { ); }); - it('should report errors originating from stylable (process + transform)', async () => { + it('should report errors originating from stylable (process + transform)', () => { const identifier = 'build-identifier'; const fs = createMemoryFs({ '/comp.st.css': ` @@ -226,7 +226,7 @@ describe('build stand alone', () => { }); const diagnosticsManager = new DiagnosticsManager(); - await build( + build( { outDir: '.', srcDir: '.', @@ -254,7 +254,7 @@ describe('build stand alone', () => { expect(messages[2].message).to.contain(stVarDiagnostics.UNKNOWN_VAR('missingVar')); }); - it('should optimize css (remove empty nodes, remove stylable-directives, remove comments)', async () => { + it('should optimize css (remove empty nodes, remove stylable-directives, remove comments)', () => { const fs = createMemoryFs({ '/comp.st.css': ` .root { @@ -273,7 +273,7 @@ describe('build stand alone', () => { requireModule: () => ({}), }); - await build( + build( { outDir: './dist', srcDir: '.', @@ -297,7 +297,7 @@ describe('build stand alone', () => { expect(builtFile).to.not.contain(`.x`); }); - it('should inline assets into data uri in the js module output', async () => { + it('should inline assets into data uri in the js module output', () => { const rawSvg = ` `; @@ -331,7 +331,7 @@ describe('build stand alone', () => { }, }); - await build( + build( { outDir: './dist', srcDir: './src', @@ -353,7 +353,7 @@ describe('build stand alone', () => { expect(builtFile).to.contain(imageSvg2AsBase64); }); - it('should minify', async () => { + it('should minify', () => { const fs = createMemoryFs({ '/comp.st.css': ` .root { @@ -370,7 +370,7 @@ describe('build stand alone', () => { }, }); - await build( + build( { outDir: './dist', srcDir: '.', @@ -393,7 +393,7 @@ describe('build stand alone', () => { expect(builtFile).to.contain(`.test__root{color:red}`); }); - it('inline runtime', async () => { + it('inline runtime', () => { const fs = createMemoryFs({ '/comp.st.css': ` .root { @@ -415,7 +415,7 @@ describe('build stand alone', () => { }, }); - await build( + build( { outDir: './dist', srcDir: '.', @@ -456,7 +456,7 @@ describe('build stand alone', () => { expect(runtimeMjs).to.eql(`// runtime esm`); }); - it('cjsExt/esmExt', async () => { + it('cjsExt/esmExt', () => { const fs = createMemoryFs({ '/comp.st.css': ` .root { @@ -473,7 +473,7 @@ describe('build stand alone', () => { }, }); - await build( + build( { outDir: './dist', srcDir: '.', @@ -502,7 +502,7 @@ describe('build stand alone', () => { ); }); - it('should inject request to output module', async () => { + it('should inject request to output module', () => { const fs = createMemoryFs({ '/comp.st.css': ` .root { @@ -517,7 +517,7 @@ describe('build stand alone', () => { requireModule: () => ({}), }); - await build( + build( { outDir: './dist', srcDir: '.', @@ -545,7 +545,7 @@ describe('build stand alone', () => { expect(fs.existsSync('/dist/comp.global.css')).to.equal(true); }); - it('DTS only parts', async () => { + it('DTS only parts', () => { const fs = createMemoryFs({ '/main.st.css': ` .root {} @@ -558,7 +558,7 @@ describe('build stand alone', () => { requireModule: () => ({}), }); - await build( + build( { outDir: '.', srcDir: '.', @@ -585,7 +585,7 @@ describe('build stand alone', () => { expect(dtsContent).contains('"part":'); }); - it('DTS with states', async () => { + it('DTS with states', () => { const fs = createMemoryFs({ '/main.st.css': ` .root { -st-states: w; } @@ -600,7 +600,7 @@ describe('build stand alone', () => { requireModule: () => ({}), }); - await build( + build( { outDir: '.', srcDir: '.', @@ -629,7 +629,7 @@ describe('build stand alone', () => { expect(dtsContent).to.contain('"z"?: "on" | "off" | "default";'); }); - it('DTS with mapping', async () => { + it('DTS with mapping', () => { const fs = createMemoryFs({ '/main.st.css': ` @keyframes blah { @@ -656,7 +656,7 @@ describe('build stand alone', () => { requireModule: () => ({}), }); - await build( + build( { outDir: '.', srcDir: '.', @@ -684,7 +684,7 @@ describe('build stand alone', () => { }); describe('build - bundle', () => { - it('should create modules and copy source css files', async () => { + it('should create modules and copy source css files', () => { const fs = createMemoryFs({ '/main.st.css': ` :import{ @@ -712,7 +712,7 @@ describe('build - bundle', () => { }, }); - await build( + build( { outDir: 'lib', srcDir: '.', @@ -733,7 +733,7 @@ describe('build - bundle', () => { '.comp__baga{color:red}.main__gaga{color:#00f}', ); }); - it('should rewrite relative urls', async () => { + it('should rewrite relative urls', () => { const fs = createMemoryFs({ '/components/button/button.st.css': ` :import{ @@ -760,7 +760,7 @@ describe('build - bundle', () => { }, }); - await build( + build( { outDir: 'lib', srcDir: '.', diff --git a/packages/cli/test/cli.spec.ts b/packages/cli/test/cli.spec.ts index b3d61816c..7ee791f4d 100644 --- a/packages/cli/test/cli.spec.ts +++ b/packages/cli/test/cli.spec.ts @@ -16,7 +16,6 @@ import { diagnosticBankReportToStrings } from '@stylable/core-test-kit'; const stVarDiagnostics = diagnosticBankReportToStrings(STVar.diagnostics); describe('Stylable Cli', function () { - this.timeout(25000); let tempDir: ITempDirectory; const testNsrPath = require.resolve('./fixtures/test-ns-resolver'); diff --git a/packages/cli/test/config-options.spec.ts b/packages/cli/test/config-options.spec.ts index 4900f84df..2dd157c02 100644 --- a/packages/cli/test/config-options.spec.ts +++ b/packages/cli/test/config-options.spec.ts @@ -10,7 +10,6 @@ import { } from '@stylable/e2e-test-kit'; describe('Stylable CLI config file options', function () { - this.timeout(25000); let tempDir: ITempDirectory; beforeEach(async () => { diff --git a/packages/cli/test/config-presets.spec.ts b/packages/cli/test/config-presets.spec.ts index 63c78dcc0..1459635a9 100644 --- a/packages/cli/test/config-presets.spec.ts +++ b/packages/cli/test/config-presets.spec.ts @@ -8,7 +8,6 @@ import { } from '@stylable/e2e-test-kit'; describe('Stylable CLI config presets', function () { - this.timeout(25000); let tempDir: ITempDirectory; beforeEach(async () => { diff --git a/packages/cli/test/config-projects.spec.ts b/packages/cli/test/config-projects.spec.ts index 5eb25e4cd..14b0a1335 100644 --- a/packages/cli/test/config-projects.spec.ts +++ b/packages/cli/test/config-projects.spec.ts @@ -14,7 +14,6 @@ import { diagnosticBankReportToStrings } from '@stylable/core-test-kit'; const stVarDiagnostics = diagnosticBankReportToStrings(STVar.diagnostics); describe('Stylable CLI config multiple projects', function () { - this.timeout(25000); let tempDir: ITempDirectory; beforeEach(async () => { diff --git a/packages/cli/test/directory-process-service/directory-process-service.spec.ts b/packages/cli/test/directory-process-service/directory-process-service.spec.ts index 413b879fa..f86a27ce8 100644 --- a/packages/cli/test/directory-process-service/directory-process-service.spec.ts +++ b/packages/cli/test/directory-process-service/directory-process-service.spec.ts @@ -2,7 +2,7 @@ import { createMemoryFs } from '@file-services/memory'; import type { IFileSystem } from '@file-services/types'; import { expect } from 'chai'; import { waitFor } from 'promise-assist'; -import { DirectoryProcessService } from '@stylable/cli'; +import { createWatchService, DirectoryProcessService, type WatchService } from '@stylable/cli'; import { logCalls } from '@stylable/core-test-kit'; const project1 = { @@ -27,13 +27,15 @@ const project1 = { describe('DirectoryWatchService', () => { describe('Empty project', () => { let fs: IFileSystem; + let watchService: WatchService; beforeEach(() => { fs = createMemoryFs({ dist: {} }); + watchService = createWatchService(fs); }); it('should watch added files', async () => { - const watcher = new DirectoryProcessService(fs, { + const watcher = new DirectoryProcessService(fs, watchService, { watchMode: true, fileFilter: isTemplateFile, processFiles(watcher, affectedFiles) { @@ -54,7 +56,7 @@ describe('DirectoryWatchService', () => { }, }); - await watcher.init('/'); + watcher.init('/'); fs.writeFileSync('/0.template.js', `output('0()')`); @@ -94,7 +96,7 @@ describe('DirectoryWatchService', () => { it('should handle directory added after watch started', async () => { const changeSpy = logCalls(); - const watcher = new DirectoryProcessService(fs, { + const watcher = new DirectoryProcessService(fs, watchService, { watchMode: true, fileFilter: isTemplateFile, processFiles(watcher, affectedFiles, _, changeOrigin) { @@ -119,7 +121,7 @@ describe('DirectoryWatchService', () => { }, }); - await watcher.init('/'); + watcher.init('/'); // Nothing happened expect(changeSpy.callCount, 'not been called').to.equal(0); @@ -139,7 +141,7 @@ describe('DirectoryWatchService', () => { }); it('should handle delete files', async () => { - new DirectoryProcessService(fs, { + const watcher = new DirectoryProcessService(fs, watchService, { watchMode: true, fileFilter: isTemplateFile, processFiles(watcher, affectedFiles) { @@ -159,6 +161,8 @@ describe('DirectoryWatchService', () => { }, }); + watcher.init('/'); + fs.writeFileSync('0.template.js', 'output(`0()`)'); await waitFor(() => { @@ -173,7 +177,7 @@ describe('DirectoryWatchService', () => { }); it('should handle delete dirs', async () => { - const watcher = new DirectoryProcessService(fs, { + const watcher = new DirectoryProcessService(fs, watchService, { watchMode: true, fileFilter: isTemplateFile, processFiles(watcher, affectedFiles) { @@ -193,6 +197,8 @@ describe('DirectoryWatchService', () => { }, }); + watcher.init('/'); + fs.ensureDirectorySync('/test/deep'); fs.writeFileSync('test/0.template.js', 'output(`0()`)'); @@ -214,15 +220,17 @@ describe('DirectoryWatchService', () => { describe('Basic watcher init/change API (project1)', () => { let fs: IFileSystem; + let watchService: WatchService; beforeEach(() => { fs = createMemoryFs(project1); + watchService = createWatchService(fs); }); - it('should report affectedFiles and no changeOrigin when watch started', async () => { + it('should report affectedFiles and no changeOrigin when watch started', () => { const changeSpy = logCalls(); - const watcher = new DirectoryProcessService(fs, { + const watcher = new DirectoryProcessService(fs, watchService, { watchMode: true, fileFilter: isTemplateFile, processFiles(_watcher, affectedFiles, _, changeOrigin) { @@ -237,7 +245,7 @@ describe('DirectoryWatchService', () => { }, }); - await watcher.init('/'); + watcher.init('/'); expect(changeSpy.callCount, 'called once').to.equal(1); @@ -254,8 +262,8 @@ describe('DirectoryWatchService', () => { ]); }); - it('should allow hooks to fill in the invalidationMap', async () => { - const watcher = new DirectoryProcessService(fs, { + it('should allow hooks to fill in the invalidationMap', () => { + const watcher = new DirectoryProcessService(fs, watchService, { watchMode: true, fileFilter: isTemplateFile, processFiles(watcher, affectedFiles) { @@ -272,7 +280,7 @@ describe('DirectoryWatchService', () => { }, }); - await watcher.init('/'); + watcher.init('/'); expectInvalidationMap(watcher, { '/0.template.js': [], @@ -284,7 +292,7 @@ describe('DirectoryWatchService', () => { it('should report change for all files affected by the changeOrigin', async () => { const changeSpy = logCalls(); - const watcher = new DirectoryProcessService(fs, { + const watcher = new DirectoryProcessService(fs, watchService, { watchMode: true, fileFilter: isTemplateFile, processFiles(watcher, affectedFiles, _, changeOrigin) { @@ -305,7 +313,7 @@ describe('DirectoryWatchService', () => { }, }); - await watcher.init('/'); + watcher.init('/'); changeSpy.resetHistory(); diff --git a/packages/cli/test/generate-index.spec.ts b/packages/cli/test/generate-index.spec.ts index 2fcb78dab..d1afbe5c5 100644 --- a/packages/cli/test/generate-index.spec.ts +++ b/packages/cli/test/generate-index.spec.ts @@ -9,7 +9,7 @@ const log = () => { }; describe('build index', () => { - it('should create index file importing all matched stylesheets in srcDir', async () => { + it('should create index file importing all matched stylesheets in srcDir', () => { const fs = createMemoryFs({ '/compA.st.css': ` .a{} @@ -25,7 +25,7 @@ describe('build index', () => { requireModule: () => ({}), }); - await build( + build( { outDir: '.', srcDir: '.', @@ -51,7 +51,7 @@ describe('build index', () => { ].join('\n'), ); }); - it('should create index file importing all matched stylesheets in outDir (outputSources)', async () => { + it('should create index file importing all matched stylesheets in outDir (outputSources)', () => { const fs = createMemoryFs({ src: { '/compA.st.css': ` @@ -69,7 +69,7 @@ describe('build index', () => { requireModule: () => ({}), }); - await build( + build( { outDir: './dist', srcDir: './src', @@ -97,7 +97,7 @@ describe('build index', () => { ); }); - it('should create index file importing all matched stylesheets in srcDir when has output cjs files', async () => { + it('should create index file importing all matched stylesheets in srcDir when has output cjs files', () => { const fs = createMemoryFs({ src: { '/compA.st.css': ` @@ -115,7 +115,7 @@ describe('build index', () => { requireModule: () => ({}), }); - await build( + build( { outDir: '.', srcDir: '.', @@ -142,7 +142,7 @@ describe('build index', () => { ].join('\n'), ); }); - it('should create index file using a the default generator', async () => { + it('should create index file using a the default generator', () => { const fs = createMemoryFs({ '/comp-A.st.css': ` .a{} @@ -158,7 +158,7 @@ describe('build index', () => { requireModule: () => ({}), }); - await build( + build( { outDir: '.', srcDir: '.', @@ -184,7 +184,7 @@ describe('build index', () => { ].join('\n'), ); }); - it('should create index file using a custom generator', async () => { + it('should create index file using a custom generator', () => { const fs = createMemoryFs({ '/comp-A.st.css': ` .a{} @@ -200,7 +200,7 @@ describe('build index', () => { requireModule: () => ({}), }); - await build( + build( { outDir: '.', srcDir: '.', @@ -227,7 +227,7 @@ describe('build index', () => { ].join('\n'), ); }); - it('should create index file when srcDir is parent directory of outDir', async () => { + it('should create index file when srcDir is parent directory of outDir', () => { const fs = createMemoryFs({ dist: { 'c/compA.st.css': ` @@ -245,7 +245,7 @@ describe('build index', () => { requireModule: () => ({}), }); - await build( + build( { outDir: '.', srcDir: './dist', @@ -273,7 +273,7 @@ describe('build index', () => { ].join('\n'), ); }); - it('custom generator is able to filter files from the index', async () => { + it('custom generator is able to filter files from the index', () => { const fs = createMemoryFs({ '/comp-A.st.css': ` .a{} @@ -289,7 +289,7 @@ describe('build index', () => { requireModule: () => ({}), }); - await build( + build( { outDir: '.', srcDir: '.', @@ -311,7 +311,7 @@ describe('build index', () => { ':import {-st-from: "./comp-A.st.css";-st-default:Style0;}\n.root Style0{}', ); }); - it('should create index file using a custom generator with named exports generation and @st-namespace', async () => { + it('should create index file using a custom generator with named exports generation and @st-namespace', () => { const fs = createMemoryFs({ '/comp-A.st.css': ` :vars { @@ -333,7 +333,7 @@ describe('build index', () => { requireModule: () => ({}), }); - await build( + build( { outDir: '.', srcDir: '.', @@ -363,7 +363,7 @@ describe('build index', () => { ].join('\n'), ); }); - it('should create non-existing folders in path to the generated indexFile', async () => { + it('should create non-existing folders in path to the generated indexFile', () => { const fs = createMemoryFs({ '/comp.st.css': ` .a{} @@ -375,7 +375,7 @@ describe('build index', () => { fileSystem: fs, requireModule: () => ({}), }); - await build( + build( { outDir: './some-dir/other-dir/', srcDir: '.', @@ -398,7 +398,7 @@ describe('build index', () => { ), ); }); - it('should handle name collisions by failing', async () => { + it('should handle name collisions by failing', () => { const fs = createMemoryFs({ '/comp.st.css': ` .a{} @@ -415,7 +415,7 @@ describe('build index', () => { }); const diagnosticsManager = new DiagnosticsManager(); - await build( + build( { outDir: '.', srcDir: '.', diff --git a/packages/cli/test/watch-multiple-projects.spec.ts b/packages/cli/test/watch-multiple-projects.spec.ts index 54066672d..803d076da 100644 --- a/packages/cli/test/watch-multiple-projects.spec.ts +++ b/packages/cli/test/watch-multiple-projects.spec.ts @@ -2,30 +2,23 @@ import { buildMessages } from '@stylable/cli/dist/messages'; import { STImport } from '@stylable/core/dist/features'; import { createCliTester, + createTempDirectory, + ITempDirectory, loadDirSync, populateDirectorySync, writeToExistingFile, - createTempDirectory, - ITempDirectory, } from '@stylable/e2e-test-kit'; import { expect } from 'chai'; -import { realpathSync, promises } from 'fs'; -import { join, sep } from 'path'; +import { promises } from 'node:fs'; +import { join, sep } from 'node:path'; const { writeFile } = promises; describe('Stylable Cli Watch - Multiple projects', function () { - /** - * https://github.com/livereload/livereload-site/blob/master/livereload.com/_articles/troubleshooting/os-x-fsevents-bug-may-prevent-monitoring-of-certain-folders.md - */ - this.retries(2); - let tempDir: ITempDirectory; const { run, cleanup } = createCliTester(); beforeEach(async () => { tempDir = await createTempDirectory(); - // This is used to make the output paths matching consistent since we use the real path in the logs of the CLI - tempDir.path = realpathSync(tempDir.path); }); afterEach(async () => { cleanup(); diff --git a/packages/cli/test/watch-single-project.spec.ts b/packages/cli/test/watch-single-project.spec.ts index 03e179111..77fee89bf 100644 --- a/packages/cli/test/watch-single-project.spec.ts +++ b/packages/cli/test/watch-single-project.spec.ts @@ -1,32 +1,25 @@ -import { errorMessages, buildMessages } from '@stylable/cli/dist/messages'; +import { buildMessages, errorMessages } from '@stylable/cli/dist/messages'; import { STImport } from '@stylable/core/dist/features'; import { createCliTester, + createTempDirectory, escapeRegExp, + ITempDirectory, loadDirSync, populateDirectorySync, writeToExistingFile, - createTempDirectory, - ITempDirectory, } from '@stylable/e2e-test-kit'; import { expect } from 'chai'; -import { realpathSync, renameSync, rmSync, unlinkSync, promises } from 'fs'; -import { join, sep } from 'path'; +import { promises, renameSync, rmSync, unlinkSync } from 'node:fs'; +import { join, sep } from 'node:path'; const { writeFile } = promises; describe('Stylable Cli Watch - Single project', function () { - /** - * https://github.com/livereload/livereload-site/blob/master/livereload.com/_articles/troubleshooting/os-x-fsevents-bug-may-prevent-monitoring-of-certain-folders.md - */ - this.retries(2); - let tempDir: ITempDirectory; const { run, cleanup } = createCliTester(); beforeEach(async () => { tempDir = await createTempDirectory(); - // This is used to make the output paths matching consistent since we use the real path in the logs of the CLI - tempDir.path = realpathSync(tempDir.path); }); afterEach(async () => { cleanup(); @@ -529,7 +522,7 @@ describe('Stylable Cli Watch - Single project', function () { ), action() { return { - sleep: 2000, + sleep: 100, }; }, }, diff --git a/packages/e2e-test-kit/src/cli-test-kit.ts b/packages/e2e-test-kit/src/cli-test-kit.ts index 19ec2016c..480d89460 100644 --- a/packages/e2e-test-kit/src/cli-test-kit.ts +++ b/packages/e2e-test-kit/src/cli-test-kit.ts @@ -24,7 +24,7 @@ export function createCliTester() { dirPath, args, steps, - timeout = Number(process.env.CLI_WATCH_TEST_TIMEOUT) || 10_000, + timeout = Number(process.env.CLI_WATCH_TEST_TIMEOUT) || 2000, }: ProcessCliOutputParams): Promise<{ output(): string }> { const process = runCli(['--rootDir', dirPath, '--log', ...args], dirPath); const lines: string[] = []; diff --git a/packages/rollup-plugin/src/index.ts b/packages/rollup-plugin/src/index.ts index 3fc68a699..d56d902ab 100644 --- a/packages/rollup-plugin/src/index.ts +++ b/packages/rollup-plugin/src/index.ts @@ -98,7 +98,7 @@ export function stylableRollupPlugin({ let configFromFile: ReturnType | undefined; return { name: 'Stylable', - async buildStart() { + buildStart() { extracted = extracted || new Map(); emittedAssets = emittedAssets || new Map(); @@ -139,7 +139,7 @@ export function stylableRollupPlugin({ watchMode: this.meta.watchMode, }); - await stcBuilder.build(); + stcBuilder.build(); for (const sourceDirectory of stcBuilder.getProjectsSources()) { this.addWatchFile(sourceDirectory); @@ -155,9 +155,9 @@ export function stylableRollupPlugin({ } } }, - async watchChange(id) { + watchChange(id) { if (stcBuilder) { - await stcBuilder.rebuild([id]); + stcBuilder.rebuild([id]); stcBuilder.reportDiagnostics( { diff --git a/packages/webpack-plugin/src/plugin.ts b/packages/webpack-plugin/src/plugin.ts index 761153074..0afc7cc15 100644 --- a/packages/webpack-plugin/src/plugin.ts +++ b/packages/webpack-plugin/src/plugin.ts @@ -221,19 +221,16 @@ export class StylableWebpackPlugin { this.createStcBuilder(compiler); }); - compiler.hooks.beforeRun.tapPromise(StylableWebpackPlugin.name, async () => { - await this.stcBuilder?.build(); + compiler.hooks.beforeRun.tap(StylableWebpackPlugin.name, () => { + this.stcBuilder?.build(); }); - compiler.hooks.watchRun.tapPromise( - { name: StylableWebpackPlugin.name, stage: 0 }, - async (compiler) => { - await this.stcBuilder?.rebuild([ - ...(compiler.modifiedFiles ?? []), - ...(compiler.removedFiles ?? []), - ]); - }, - ); + compiler.hooks.watchRun.tap({ name: StylableWebpackPlugin.name, stage: 0 }, (compiler) => { + this.stcBuilder?.rebuild([ + ...(compiler.modifiedFiles ?? []), + ...(compiler.removedFiles ?? []), + ]); + }); compiler.hooks.thisCompilation.tap(StylableWebpackPlugin.name, (compilation) => { /**