@intlayer/chokidar
Version:
Uses chokidar to scan and build Intlayer declaration files into dictionaries based on Intlayer configuration.
1 lines • 15.8 kB
Source Map (JSON)
{"version":3,"file":"watcher.mjs","names":[],"sources":["../../src/watcher.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { basename, dirname } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport * as ANSIColor from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n getConfigurationAndFilePath,\n} from '@intlayer/config/node';\nimport {\n clearAllCache,\n clearDiskCacheMemory,\n clearModuleCache,\n normalizePath,\n} from '@intlayer/config/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { ChokidarOptions } from 'chokidar';\nimport { handleAdditionalContentDeclarationFile } from './handleAdditionalContentDeclarationFile';\nimport { handleContentDeclarationFileChange } from './handleContentDeclarationFileChange';\nimport { handleContentDeclarationFileMoved } from './handleContentDeclarationFileMoved';\nimport { handleUnlinkedContentDeclarationFile } from './handleUnlinkedContentDeclarationFile';\nimport { prepareIntlayer } from './prepareIntlayer';\nimport { formatPath } from './utils';\nimport { writeContentDeclaration } from './writeContentDeclaration';\n\n// Map to track files that were recently unlinked: oldPath -> { timer, timestamp }\nconst pendingUnlinks = new Map<\n string,\n { timer: NodeJS.Timeout; oldPath: string }\n>();\n\n// Array-based sequential task queue — no Promise chain accumulation, no race conditions\nconst taskQueue: (() => Promise<void>)[] = [];\nlet isProcessing = false;\n\nconst processQueue = async () => {\n if (isProcessing) return;\n isProcessing = true;\n while (taskQueue.length > 0) {\n const task = taskQueue.shift()!;\n try {\n await task();\n } catch (error) {\n console.error(error);\n }\n }\n isProcessing = false;\n};\n\nconst processEvent = (task: () => Promise<void>) => {\n taskQueue.push(task);\n processQueue();\n};\n\ntype WatchOptions = ChokidarOptions & {\n configuration?: IntlayerConfig;\n configOptions?: GetConfigurationOptions;\n skipPrepare?: boolean;\n};\n\n// Initialize chokidar watcher (non-persistent)\nexport const watch = async (options?: WatchOptions) => {\n const { watch: chokidarWatch } = await import('chokidar');\n const configResult = getConfigurationAndFilePath(options?.configOptions);\n const configurationFilePath = configResult.configurationFilePath;\n let configuration: IntlayerConfig =\n options?.configuration ?? configResult.configuration;\n const appLogger = getAppLogger(configuration);\n\n const {\n watch: isWatchMode,\n fileExtensions,\n contentDir,\n excludedPath,\n } = configuration.content;\n\n // chokidar v5 dropped glob support — use fs to resolve dirs, filter extensions via ignored\n const pathsToWatch = [\n ...contentDir.map((dir) => normalizePath(dir)).filter(existsSync),\n ...(configurationFilePath ? [configurationFilePath] : []),\n ];\n\n if (!configuration.content.watch) return;\n\n appLogger('Watching Intlayer content declarations');\n\n if (configuration.build.optimize === true) {\n appLogger(\n [\n `Build optimization is forced to ${colorize('true', ANSIColor.GREY)}, but watching is enabled too.`,\n 'It may lead to dev mode performance degradation as well as import errors.',\n 'Its recommended to keep the',\n colorize('`build.optimized`', ANSIColor.BLUE),\n 'option',\n colorize('undefined', ANSIColor.GREY),\n 'to get the best dev mode experience',\n ],\n {\n level: 'warn',\n }\n );\n }\n\n // Strip glob markers from excludedPath entries to get plain segments (e.g. 'node_modules')\n const excludedSegments = excludedPath.map((segment) =>\n segment.replace(/^\\*\\*\\//, '').replace(/\\/\\*\\*$/, '')\n );\n\n const normalizedConfigPath = configurationFilePath\n ? normalizePath(configurationFilePath)\n : null;\n\n const { mainDir, baseDir } = configuration.system;\n const normalizedIntlayerDir = normalizePath(dirname(mainDir));\n\n // Watch mainDir to detect broken or missing entry point files\n if (existsSync(mainDir)) {\n chokidarWatch(mainDir, {\n persistent: isWatchMode,\n ignoreInitial: true,\n depth: 0,\n })\n .on('change', async (filePath) => {\n if (isProcessing) return;\n\n processEvent(async () => {\n clearModuleCache(filePath);\n try {\n // Convert absolute path to a valid file:// URL\n const fileUrl = pathToFileURL(filePath).href;\n\n // Append a timestamp to bypass the ESM cache\n await import(`${fileUrl}?update=${Date.now()}`);\n } catch {\n appLogger(\n `Entry point ${basename(filePath)} failed to load, running clean rebuild...`,\n { level: 'warn' }\n );\n await prepareIntlayer(configuration, {\n clean: true,\n forceRun: true,\n });\n }\n });\n })\n .on('unlink', async (filePath) => {\n if (isProcessing) return;\n\n processEvent(async () => {\n appLogger(\n [\n 'Entry point',\n formatPath(basename(filePath)),\n 'was removed, running clean rebuild...',\n ],\n { level: 'warn' }\n );\n await prepareIntlayer(configuration, { clean: true, forceRun: true });\n });\n });\n }\n\n // Watch baseDir at depth 0 to detect the entire .intlayer folder being removed\n chokidarWatch(baseDir, {\n persistent: isWatchMode,\n ignoreInitial: true,\n depth: 0,\n ignored: (filePath: string) => {\n const path = normalizePath(filePath);\n return path !== normalizePath(baseDir) && path !== normalizedIntlayerDir;\n },\n }).on('unlinkDir', async (dirPath) => {\n if (isProcessing) return;\n\n if (normalizePath(dirPath) === normalizedIntlayerDir) {\n appLogger([\n formatPath('.intlayer'),\n 'directory removed, running clean rebuild...',\n ]);\n\n processEvent(() =>\n prepareIntlayer(configuration, { clean: true, forceRun: true })\n );\n }\n });\n\n return chokidarWatch(pathsToWatch, {\n persistent: isWatchMode, // Make the watcher persistent\n ignoreInitial: true, // Process existing files\n awaitWriteFinish: {\n stabilityThreshold: 1000,\n pollInterval: 100,\n },\n ignored: (filePath: string, stats?: import('node:fs').Stats) => {\n const path = normalizePath(filePath);\n\n if (normalizedConfigPath && path === normalizedConfigPath) return false;\n\n if (excludedSegments.some((segment) => path.includes(`/${segment}`)))\n return true;\n\n if (stats?.isFile()) {\n return !fileExtensions.some((extension) => path.endsWith(extension));\n }\n\n return false;\n },\n ...options,\n })\n .on('add', async (filePath) => {\n const fileName = basename(filePath);\n let isMove = false;\n\n // Check if this Add corresponds to a pending Unlink (Move/Rename detection)\n // Heuristic:\n // - Priority A: Exact basename match (Moved to different folder)\n // - Priority B: Single entry in pendingUnlinks (Renamed file)\n let matchedOldPath: string | undefined;\n\n // Search for basename match\n for (const [oldPath] of pendingUnlinks) {\n if (basename(oldPath) === fileName) {\n matchedOldPath = oldPath;\n break;\n }\n }\n\n // If no basename match, but exactly one file was recently unlinked, assume it's a rename\n if (!matchedOldPath && pendingUnlinks.size === 1) {\n matchedOldPath = pendingUnlinks.keys().next().value;\n }\n\n if (matchedOldPath) {\n // It is a move! Cancel the unlink handler\n const pending = pendingUnlinks.get(matchedOldPath);\n if (pending) {\n clearTimeout(pending.timer);\n pendingUnlinks.delete(matchedOldPath);\n }\n\n isMove = true;\n appLogger(`File moved from ${matchedOldPath} to ${filePath}`);\n }\n\n processEvent(async () => {\n if (isMove && matchedOldPath) {\n await handleContentDeclarationFileMoved(\n matchedOldPath,\n filePath,\n configuration\n );\n } else {\n const fileContent = await readFile(filePath, 'utf-8');\n const isEmpty = fileContent === '';\n\n // Fill template content declaration file if it is empty\n if (isEmpty) {\n const extensionPattern = fileExtensions\n .map((ext) => ext.replace(/\\./g, '\\\\.'))\n .join('|');\n const name = fileName.replace(\n new RegExp(`(${extensionPattern})$`),\n ''\n );\n\n await writeContentDeclaration(\n {\n key: name,\n content: {},\n filePath,\n },\n configuration\n );\n }\n\n await handleAdditionalContentDeclarationFile(filePath, configuration);\n }\n });\n })\n .on('change', async (filePath) =>\n processEvent(async () => {\n if (configurationFilePath && filePath === configurationFilePath) {\n appLogger('Configuration file changed, repreparing Intlayer');\n\n clearModuleCache(configurationFilePath);\n clearAllCache();\n\n const { configuration: newConfiguration } =\n getConfigurationAndFilePath(options?.configOptions);\n\n configuration = options?.configuration ?? newConfiguration;\n\n await prepareIntlayer(configuration, { clean: false });\n } else {\n // Clear module cache for the changed file to avoid stale require() results\n clearModuleCache(filePath);\n // Evict in-memory caches so loadContentDeclaration picks up fresh content\n clearAllCache();\n clearDiskCacheMemory();\n await handleContentDeclarationFileChange(filePath, configuration);\n }\n })\n )\n .on('unlink', async (filePath) => {\n // Delay unlink processing to see if an 'add' event occurs (indicating a move)\n const timer = setTimeout(async () => {\n // If timer fires, the file was genuinely removed\n pendingUnlinks.delete(filePath);\n processEvent(async () =>\n handleUnlinkedContentDeclarationFile(filePath, configuration)\n );\n }, 200); // 200ms window to catch the 'add' event\n\n pendingUnlinks.set(filePath, { timer, oldPath: filePath });\n })\n .on('error', async (error) => {\n appLogger(`Watcher error: ${error}`, {\n level: 'error',\n });\n\n appLogger('Restarting watcher');\n\n await prepareIntlayer(configuration);\n });\n};\n\nexport const buildAndWatchIntlayer = async (options?: WatchOptions) => {\n const { skipPrepare, ...rest } = options ?? {};\n const configuration =\n options?.configuration ?? getConfiguration(options?.configOptions);\n\n if (!skipPrepare) {\n await prepareIntlayer(configuration, { forceRun: true });\n }\n\n // Only enter watch mode when the caller explicitly opts in via `persistent`.\n // `configuration.content.watch` is the dev-mode signal consumed by bundler\n // plugins (e.g. vite-intlayer's `configureServer`); it must not coerce\n // `intlayer build` (which passes `persistent: false`) into a persistent\n // watcher, since that prevents the build command from ever exiting.\n if (options?.persistent) {\n await watch({ ...rest, configuration });\n }\n};\n"],"mappings":"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"}