UNPKG

@intlayer/chokidar

Version:

Scans and builds Intlayer declaration files into dictionaries based on Intlayer configuration.

1 lines 18.6 kB
{"version":3,"file":"watcher.mjs","names":["fsWatch"],"sources":["../../src/watcher.ts"],"sourcesContent":["import { existsSync, watch as fsWatch } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { basename, dirname, resolve } 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 { 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 = {\n configuration?: IntlayerConfig;\n configOptions?: GetConfigurationOptions;\n skipPrepare?: boolean;\n persistent?: boolean;\n};\n\n// awaitWriteFinish equivalent: debounce per path until the file is stable\nconst STABILITY_THRESHOLD = 1000;\n\nconst createStabilityDebounce = () => {\n const pending = new Map<string, NodeJS.Timeout>();\n return (path: string, handler: () => void) => {\n const existing = pending.get(path);\n if (existing) clearTimeout(existing);\n pending.set(\n path,\n setTimeout(() => {\n pending.delete(path);\n handler();\n }, STABILITY_THRESHOLD)\n );\n };\n};\n\n// Initialize @parcel/watcher (non-persistent until subscribed)\nexport const watch = async (options?: WatchOptions) => {\n const { subscribe } = await import('@parcel/watcher');\n\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 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 normalizedMainDir = normalizePath(mainDir);\n const normalizedIntlayerDir = normalizePath(dirname(mainDir));\n\n const subscriptions: { unsubscribe: () => Promise<void> }[] = [];\n\n const scheduleStable = createStabilityDebounce();\n\n // ── mainDir watcher (depth 0) ──────────────────────────────────────────────\n // Detects broken or missing entry-point files inside .intlayer/main\n if (existsSync(mainDir)) {\n const mainDirSub = await subscribe(normalizedMainDir, (err, events) => {\n if (err || isProcessing) return;\n\n for (const event of events) {\n const eventPath = normalizePath(event.path);\n // depth-0 filter: only files directly inside mainDir\n const rel = eventPath.slice(normalizedMainDir.length + 1);\n if (!rel || rel.includes('/')) continue;\n // Temp files written by the bundler (write-then-rename) are build-internal;\n // their deletion must not trigger a clean rebuild.\n if (rel.endsWith('.tmp')) continue;\n\n if (event.type === 'update') {\n processEvent(async () => {\n clearModuleCache(event.path);\n try {\n const fileUrl = pathToFileURL(event.path).href;\n await import(`${fileUrl}?update=${Date.now()}`);\n } catch {\n appLogger(\n `Entry point ${basename(event.path)} failed to load, running clean rebuild...`,\n { level: 'warn' }\n );\n await prepareIntlayer(configuration, {\n clean: true,\n forceRun: true,\n });\n }\n });\n } else if (event.type === 'delete') {\n processEvent(async () => {\n appLogger(\n [\n 'Entry point',\n formatPath(basename(event.path)),\n 'was removed, running clean rebuild...',\n ],\n { level: 'warn' }\n );\n await prepareIntlayer(configuration, {\n clean: true,\n forceRun: true,\n });\n });\n }\n }\n });\n subscriptions.push(mainDirSub);\n }\n\n // ── baseDir watcher (depth 0) — detect .intlayer directory removal ─────────\n // Native fs.watch is non-recursive and ideal for single-directory depth-0 detection.\n const intlayerDirName = basename(normalizedIntlayerDir);\n const fsDirWatcher = fsWatch(\n baseDir,\n { persistent: isWatchMode },\n (eventType, filename) => {\n if (isProcessing || !filename) return;\n if (filename !== intlayerDirName) return;\n\n const fullPath = normalizePath(resolve(baseDir, filename));\n if (fullPath !== normalizedIntlayerDir) return;\n\n if (eventType === 'rename' && !existsSync(normalizedIntlayerDir)) {\n appLogger([\n formatPath('.intlayer'),\n 'directory removed, running clean rebuild...',\n ]);\n processEvent(() =>\n prepareIntlayer(configuration, { clean: true, forceRun: true })\n );\n }\n }\n );\n subscriptions.push({\n unsubscribe: async () => {\n fsDirWatcher.close();\n },\n });\n\n // ── main content watcher ───────────────────────────────────────────────────\n // Ignore patterns for @parcel/watcher (micromatch globs)\n const ignorePatterns = excludedSegments.map((s) => `**/${s}/**`);\n\n const contentDirs = contentDir\n .map((dir) => normalizePath(dir))\n .filter(existsSync);\n\n // Collect unique directories to subscribe to (dirs only, not file paths)\n const dirsToWatch = new Set<string>(contentDirs);\n if (normalizedConfigPath) {\n dirsToWatch.add(normalizePath(dirname(normalizedConfigPath)));\n }\n\n const contentHandler = (\n err: Error | null,\n events: Array<{ type: string; path: string }>\n ) => {\n if (err) {\n appLogger(`Watcher error: ${err}`, { level: 'error' });\n appLogger('Restarting watcher');\n prepareIntlayer(configuration);\n return;\n }\n\n for (const event of events) {\n const filePath = event.path;\n const path = normalizePath(filePath);\n\n const isConfigFile =\n normalizedConfigPath && path === normalizedConfigPath;\n\n if (!isConfigFile) {\n // Must originate from a watched content directory\n const isInContentDir = contentDirs.some(\n (d) => path.startsWith(`${d}/`) || path === d\n );\n if (!isInContentDir) continue;\n\n if (excludedSegments.some((segment) => path.includes(`/${segment}`)))\n continue;\n\n if (!fileExtensions.some((extension) => path.endsWith(extension)))\n continue;\n }\n\n if (event.type === 'create') {\n const fileName = basename(filePath);\n\n // Move detection must happen synchronously before any debounce\n let isMove = false;\n let matchedOldPath: string | undefined;\n\n for (const [oldPath] of pendingUnlinks) {\n if (basename(oldPath) === fileName) {\n matchedOldPath = oldPath;\n break;\n }\n }\n\n if (!matchedOldPath && pendingUnlinks.size === 1) {\n matchedOldPath = pendingUnlinks.keys().next().value;\n }\n\n if (matchedOldPath) {\n const pending = pendingUnlinks.get(matchedOldPath);\n if (pending) {\n clearTimeout(pending.timer);\n pendingUnlinks.delete(matchedOldPath);\n }\n isMove = true;\n appLogger(`File moved from ${matchedOldPath} to ${filePath}`);\n }\n\n if (isMove && matchedOldPath) {\n processEvent(async () => {\n await handleContentDeclarationFileMoved(\n matchedOldPath!,\n filePath,\n configuration\n );\n });\n } else {\n // Debounce: wait for write to finish before reading the file\n scheduleStable(path, () => {\n processEvent(async () => {\n const fileContent = await readFile(filePath, 'utf-8');\n const isEmpty = fileContent === '';\n\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 { key: name, content: {}, filePath },\n configuration\n );\n }\n\n await handleAdditionalContentDeclarationFile(\n filePath,\n configuration\n );\n });\n });\n }\n } else if (event.type === 'update') {\n scheduleStable(path, () => {\n processEvent(async () => {\n if (isConfigFile) {\n appLogger('Configuration file changed, repreparing Intlayer');\n\n clearModuleCache(filePath);\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 } else if (event.type === 'delete') {\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 'create' event\n\n pendingUnlinks.set(filePath, { timer, oldPath: filePath });\n }\n }\n };\n\n for (const dir of dirsToWatch) {\n const sub = await subscribe(dir, contentHandler, {\n ignore: ignorePatterns,\n });\n\n subscriptions.push(sub);\n }\n\n return subscriptions;\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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"}