@intlayer/engine 9.5.12 → 9.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/installSkills/skills/cli.md +28 -1
- package/dist/assets/installSkills/skills/compat.md +1 -1
- package/dist/assets/installSkills/skills/dev-tools.md +8 -6
- package/dist/cjs/build.cjs +12 -2
- package/dist/cjs/buildIntlayerDictionary/buildIntlayerDictionary.cjs +12 -11
- package/dist/cjs/buildIntlayerDictionary/buildIntlayerDictionary.cjs.map +1 -1
- package/dist/cjs/buildIntlayerDictionary/index.cjs +4 -1
- package/dist/cjs/buildIntlayerDictionary/rebuildUnmergedDictionaries.cjs +23 -0
- package/dist/cjs/buildIntlayerDictionary/rebuildUnmergedDictionaries.cjs.map +1 -0
- package/dist/cjs/cli.cjs +12 -0
- package/dist/cjs/fetchDistantDictionaries.cjs.map +1 -1
- package/dist/cjs/handleAdditionalContentDeclarationFile.cjs +1 -1
- package/dist/cjs/handleContentDeclarationFileChange.cjs +1 -1
- package/dist/cjs/handleUnlinkedContentDeclarationFile.cjs +1 -1
- package/dist/cjs/init/cms.cjs +18 -0
- package/dist/cjs/init/cms.cjs.map +1 -1
- package/dist/cjs/init/index.cjs +23 -10
- package/dist/cjs/init/index.cjs.map +1 -1
- package/dist/cjs/init/utils/configManipulation.cjs +28 -0
- package/dist/cjs/init/utils/configManipulation.cjs.map +1 -1
- package/dist/cjs/init/utils/contentLayout.cjs +61 -0
- package/dist/cjs/init/utils/contentLayout.cjs.map +1 -0
- package/dist/cjs/init/utils/index.cjs +1 -0
- package/dist/cjs/init/utils/packageManager.cjs +1 -1
- package/dist/cjs/init/utils/packageManager.cjs.map +1 -1
- package/dist/cjs/installMCP/installMCP.cjs +35 -25
- package/dist/cjs/installMCP/installMCP.cjs.map +1 -1
- package/dist/cjs/installSkills/index.cjs +26 -1
- package/dist/cjs/installSkills/index.cjs.map +1 -1
- package/dist/cjs/listDictionariesPath.cjs +2 -5
- package/dist/cjs/listDictionariesPath.cjs.map +1 -1
- package/dist/cjs/listGitFiles.cjs +3 -5
- package/dist/cjs/listGitFiles.cjs.map +1 -1
- package/dist/cjs/listProjects.cjs +1 -2
- package/dist/cjs/listProjects.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/index.cjs +1 -1
- package/dist/cjs/loadDictionaries/loadDictionaries.cjs +1 -1
- package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs.map +1 -1
- package/dist/cjs/utils/buildComponentFilesList.cjs +18 -30
- package/dist/cjs/utils/buildComponentFilesList.cjs.map +1 -1
- package/dist/cjs/utils/chunkJSON.cjs +1 -1
- package/dist/cjs/utils/chunkJSON.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +2 -0
- package/dist/cjs/utils/isDirectoryExcluded.cjs +36 -0
- package/dist/cjs/utils/isDirectoryExcluded.cjs.map +1 -0
- package/dist/cjs/watcher.cjs +5 -2
- package/dist/cjs/watcher.cjs.map +1 -1
- package/dist/cjs/writeConfiguration/index.cjs +23 -0
- package/dist/cjs/writeConfiguration/index.cjs.map +1 -1
- package/dist/cjs/writeContentDeclaration/index.cjs +6 -0
- package/dist/cjs/writeContentDeclaration/processContentDeclarationContent.cjs +20 -13
- package/dist/cjs/writeContentDeclaration/processContentDeclarationContent.cjs.map +1 -1
- package/dist/cjs/writeContentDeclaration/renderContentDeclaration.cjs +213 -0
- package/dist/cjs/writeContentDeclaration/renderContentDeclaration.cjs.map +1 -0
- package/dist/cjs/writeContentDeclaration/writeContentDeclaration.cjs +5 -51
- package/dist/cjs/writeContentDeclaration/writeContentDeclaration.cjs.map +1 -1
- package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs +2 -31
- package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs.map +1 -1
- package/dist/cjs/writeContentDeclaration/writeYamlFile.cjs +2 -8
- package/dist/cjs/writeContentDeclaration/writeYamlFile.cjs.map +1 -1
- package/dist/esm/build.mjs +7 -5
- package/dist/esm/buildIntlayerDictionary/buildIntlayerDictionary.mjs +12 -12
- package/dist/esm/buildIntlayerDictionary/buildIntlayerDictionary.mjs.map +1 -1
- package/dist/esm/buildIntlayerDictionary/index.mjs +3 -2
- package/dist/esm/buildIntlayerDictionary/rebuildUnmergedDictionaries.mjs +22 -0
- package/dist/esm/buildIntlayerDictionary/rebuildUnmergedDictionaries.mjs.map +1 -0
- package/dist/esm/cli.mjs +5 -3
- package/dist/esm/fetchDistantDictionaries.mjs.map +1 -1
- package/dist/esm/handleAdditionalContentDeclarationFile.mjs +1 -1
- package/dist/esm/handleContentDeclarationFileChange.mjs +1 -1
- package/dist/esm/handleUnlinkedContentDeclarationFile.mjs +1 -1
- package/dist/esm/init/cms.mjs +19 -2
- package/dist/esm/init/cms.mjs.map +1 -1
- package/dist/esm/init/index.mjs +24 -11
- package/dist/esm/init/index.mjs.map +1 -1
- package/dist/esm/init/utils/configManipulation.mjs +28 -1
- package/dist/esm/init/utils/configManipulation.mjs.map +1 -1
- package/dist/esm/init/utils/contentLayout.mjs +58 -0
- package/dist/esm/init/utils/contentLayout.mjs.map +1 -0
- package/dist/esm/init/utils/index.mjs +2 -2
- package/dist/esm/init/utils/packageManager.mjs +1 -1
- package/dist/esm/init/utils/packageManager.mjs.map +1 -1
- package/dist/esm/installMCP/installMCP.mjs +35 -25
- package/dist/esm/installMCP/installMCP.mjs.map +1 -1
- package/dist/esm/installSkills/index.mjs +27 -3
- package/dist/esm/installSkills/index.mjs.map +1 -1
- package/dist/esm/listDictionariesPath.mjs +2 -5
- package/dist/esm/listDictionariesPath.mjs.map +1 -1
- package/dist/esm/listGitFiles.mjs +1 -1
- package/dist/esm/listGitFiles.mjs.map +1 -1
- package/dist/esm/listProjects.mjs +1 -1
- package/dist/esm/listProjects.mjs.map +1 -1
- package/dist/esm/loadDictionaries/index.mjs +1 -1
- package/dist/esm/loadDictionaries/loadDictionaries.mjs +1 -1
- package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs.map +1 -1
- package/dist/esm/utils/buildComponentFilesList.mjs +19 -31
- package/dist/esm/utils/buildComponentFilesList.mjs.map +1 -1
- package/dist/esm/utils/chunkJSON.mjs +1 -1
- package/dist/esm/utils/chunkJSON.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +2 -1
- package/dist/esm/utils/isDirectoryExcluded.mjs +35 -0
- package/dist/esm/utils/isDirectoryExcluded.mjs.map +1 -0
- package/dist/esm/utils/runParallel/pidTree.mjs +3 -3
- package/dist/esm/utils/runParallel/pidTree.mjs.map +1 -1
- package/dist/esm/utils/runParallel/ps.mjs +2 -2
- package/dist/esm/utils/runParallel/ps.mjs.map +1 -1
- package/dist/esm/utils/runParallel/wmic.mjs +2 -2
- package/dist/esm/utils/runParallel/wmic.mjs.map +1 -1
- package/dist/esm/watcher.mjs +5 -2
- package/dist/esm/watcher.mjs.map +1 -1
- package/dist/esm/writeConfiguration/index.mjs +23 -1
- package/dist/esm/writeConfiguration/index.mjs.map +1 -1
- package/dist/esm/writeContentDeclaration/index.mjs +2 -1
- package/dist/esm/writeContentDeclaration/processContentDeclarationContent.mjs +20 -13
- package/dist/esm/writeContentDeclaration/processContentDeclarationContent.mjs.map +1 -1
- package/dist/esm/writeContentDeclaration/renderContentDeclaration.mjs +208 -0
- package/dist/esm/writeContentDeclaration/renderContentDeclaration.mjs.map +1 -0
- package/dist/esm/writeContentDeclaration/writeContentDeclaration.mjs +5 -51
- package/dist/esm/writeContentDeclaration/writeContentDeclaration.mjs.map +1 -1
- package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs +2 -31
- package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs.map +1 -1
- package/dist/esm/writeContentDeclaration/writeYamlFile.mjs +2 -8
- package/dist/esm/writeContentDeclaration/writeYamlFile.mjs.map +1 -1
- package/dist/types/build.d.ts +5 -3
- package/dist/types/buildIntlayerDictionary/buildIntlayerDictionary.d.ts +6 -0
- package/dist/types/buildIntlayerDictionary/buildIntlayerDictionary.d.ts.map +1 -1
- package/dist/types/buildIntlayerDictionary/index.d.ts +3 -2
- package/dist/types/buildIntlayerDictionary/rebuildUnmergedDictionaries.d.ts +14 -0
- package/dist/types/buildIntlayerDictionary/rebuildUnmergedDictionaries.d.ts.map +1 -0
- package/dist/types/cli.d.ts +6 -4
- package/dist/types/fetchDistantDictionaries.d.ts +1 -1
- package/dist/types/fetchDistantDictionaries.d.ts.map +1 -1
- package/dist/types/init/cms.d.ts +6 -0
- package/dist/types/init/cms.d.ts.map +1 -1
- package/dist/types/init/index.d.ts +15 -14
- package/dist/types/init/index.d.ts.map +1 -1
- package/dist/types/init/utils/configManipulation.d.ts +8 -0
- package/dist/types/init/utils/configManipulation.d.ts.map +1 -1
- package/dist/types/init/utils/contentLayout.d.ts +42 -0
- package/dist/types/init/utils/contentLayout.d.ts.map +1 -0
- package/dist/types/init/utils/index.d.ts +2 -2
- package/dist/types/init/utils/packageManager.d.ts +9 -3
- package/dist/types/init/utils/packageManager.d.ts.map +1 -1
- package/dist/types/installMCP/installMCP.d.ts +4 -0
- package/dist/types/installMCP/installMCP.d.ts.map +1 -1
- package/dist/types/installSkills/index.d.ts +6 -0
- package/dist/types/installSkills/index.d.ts.map +1 -1
- package/dist/types/listDictionariesPath.d.ts +1 -1
- package/dist/types/listDictionariesPath.d.ts.map +1 -1
- package/dist/types/loadDictionaries/loadRemoteDictionaries.d.ts +1 -1
- package/dist/types/loadDictionaries/loadRemoteDictionaries.d.ts.map +1 -1
- package/dist/types/utils/buildComponentFilesList.d.ts +6 -6
- package/dist/types/utils/buildComponentFilesList.d.ts.map +1 -1
- package/dist/types/utils/chunkJSON.d.ts.map +1 -1
- package/dist/types/utils/index.d.ts +2 -1
- package/dist/types/utils/isDirectoryExcluded.d.ts +15 -0
- package/dist/types/utils/isDirectoryExcluded.d.ts.map +1 -0
- package/dist/types/watcher.d.ts.map +1 -1
- package/dist/types/writeConfiguration/index.d.ts +13 -0
- package/dist/types/writeConfiguration/index.d.ts.map +1 -1
- package/dist/types/writeContentDeclaration/index.d.ts +2 -1
- package/dist/types/writeContentDeclaration/processContentDeclarationContent.d.ts +8 -2
- package/dist/types/writeContentDeclaration/processContentDeclarationContent.d.ts.map +1 -1
- package/dist/types/writeContentDeclaration/renderContentDeclaration.d.ts +61 -0
- package/dist/types/writeContentDeclaration/renderContentDeclaration.d.ts.map +1 -0
- package/dist/types/writeContentDeclaration/writeContentDeclaration.d.ts.map +1 -1
- package/dist/types/writeContentDeclaration/writeMarkdownFile.d.ts.map +1 -1
- package/dist/types/writeContentDeclaration/writeYamlFile.d.ts.map +1 -1
- package/package.json +18 -10
package/dist/esm/watcher.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"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, extname, resolve } from 'node:path';\nimport * as ANSIColor from '@intlayer/config/colors';\nimport { transpileTSToCJS } from '@intlayer/config/file';\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 {\n formatPath,\n getFormatFromExtension,\n parseContentDeclarationFileName,\n} from './utils';\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 try {\n // Validate that the regenerated entry point — and the relative\n // dictionary graph it imports — still resolves, by bundling it\n // in-memory with esbuild (via the shared, freshness-checked\n // transpile cache). This replaces a cache-busting dynamic\n // `import()`, which leaked a permanent record into Node's ESM\n // registry on every rebuild since that registry is never evicted.\n const entryCode = await readFile(event.path, 'utf-8');\n await transpileTSToCJS(entryCode, event.path);\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 // Strip trailing slashes so directory matching stays exact. A user-supplied\n const contentDirs = contentDir\n .map((dir) => normalizePath(dir).replace(/\\/+$/, ''))\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 { key, locale } = parseContentDeclarationFileName(\n filePath,\n configuration\n );\n\n // A markdown declaration carries a single locale body, so it\n // must always state which locale it holds.\n const isMarkdown =\n getFormatFromExtension(extname(filePath)) === 'md';\n const declaredLocale =\n locale ??\n (isMarkdown\n ? configuration.internationalization.defaultLocale\n : undefined);\n\n // Lazy: pulls recast/ast-types, only needed for new empty files\n const { writeContentDeclaration } = await import(\n './writeContentDeclaration'\n );\n\n await writeContentDeclaration(\n {\n key,\n ...(declaredLocale && { locale: declaredLocale }),\n content: {},\n filePath,\n },\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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"}
|
|
1
|
+
{"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, extname, resolve } from 'node:path';\nimport * as ANSIColor from '@intlayer/config/colors';\nimport { transpileTSToCJS } from '@intlayer/config/file';\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 {\n formatPath,\n getFormatFromExtension,\n isDirectoryExcluded,\n parseContentDeclarationFileName,\n} from './utils';\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 try {\n // Validate that the regenerated entry point — and the relative\n // dictionary graph it imports — still resolves, by bundling it\n // in-memory with esbuild (via the shared, freshness-checked\n // transpile cache). This replaces a cache-busting dynamic\n // `import()`, which leaked a permanent record into Node's ESM\n // registry on every rebuild since that registry is never evicted.\n const entryCode = await readFile(event.path, 'utf-8');\n await transpileTSToCJS(entryCode, event.path);\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 // Strip trailing slashes so directory matching stays exact. A user-supplied\n const contentDirs = contentDir\n .map((dir) => normalizePath(dir).replace(/\\/+$/, ''))\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 (the most specific)\n const matchingContentDir = contentDirs\n .filter(\n (dictionary) =>\n path.startsWith(`${dictionary}/`) || path === dictionary\n )\n .reduce<string | undefined>(\n (longest, dictionary) =>\n longest === undefined || dictionary.length > longest.length\n ? dictionary\n : longest,\n undefined\n );\n if (matchingContentDir === undefined) continue;\n\n // Exclusions apply below the content directory only, so an explicit\n // `contentDir` inside e.g. a `dist` folder is still watched\n const relativeDirectory = dirname(\n path.slice(matchingContentDir.length)\n );\n if (isDirectoryExcluded(relativeDirectory, excludedPath)) 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 { key, locale } = parseContentDeclarationFileName(\n filePath,\n configuration\n );\n\n // A markdown declaration carries a single locale body, so it\n // must always state which locale it holds.\n const isMarkdown =\n getFormatFromExtension(extname(filePath)) === 'md';\n const declaredLocale =\n locale ??\n (isMarkdown\n ? configuration.internationalization.defaultLocale\n : undefined);\n\n // Lazy: pulls recast/ast-types, only needed for new empty files\n const { writeContentDeclaration } = await import(\n './writeContentDeclaration'\n );\n\n await writeContentDeclaration(\n {\n key,\n ...(declaredLocale && { locale: declaredLocale }),\n content: {},\n filePath,\n },\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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"}
|
|
@@ -17,7 +17,29 @@ const writeConfiguration = async (configuration) => {
|
|
|
17
17
|
await mkdir(configDir, { recursive: true });
|
|
18
18
|
await Promise.all([writeFileIfChanged(join(configDir, "configuration.mjs"), generateConfigurationContent(configuration, "esm")), writeFileIfChanged(join(configDir, "configuration.cjs"), generateConfigurationContent(configuration, "cjs"))]);
|
|
19
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* Writes the built configuration (`.intlayer/config`) with editor settings
|
|
22
|
+
* overridden, leaving the user configuration file untouched. Lets the visual
|
|
23
|
+
* editor connect to the application when the configuration disables it, or
|
|
24
|
+
* points at another URL. The next configuration build restores the
|
|
25
|
+
* configured values.
|
|
26
|
+
*
|
|
27
|
+
* @returns The overridden keys that differ from the configuration; nothing is
|
|
28
|
+
* written when empty.
|
|
29
|
+
*/
|
|
30
|
+
const overrideBuiltEditorConfiguration = async (configuration, editorOverride) => {
|
|
31
|
+
const overriddenKeys = Object.keys(editorOverride).filter((key) => editorOverride[key] !== void 0 && editorOverride[key] !== configuration.editor[key]);
|
|
32
|
+
if (overriddenKeys.length === 0) return [];
|
|
33
|
+
await writeConfiguration({
|
|
34
|
+
...configuration,
|
|
35
|
+
editor: {
|
|
36
|
+
...configuration.editor,
|
|
37
|
+
...editorOverride
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return overriddenKeys;
|
|
41
|
+
};
|
|
20
42
|
|
|
21
43
|
//#endregion
|
|
22
|
-
export { isCachedConfigurationUpToDate, writeConfiguration };
|
|
44
|
+
export { isCachedConfigurationUpToDate, overrideBuiltEditorConfiguration, writeConfiguration };
|
|
23
45
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/writeConfiguration/index.ts"],"sourcesContent":["import { mkdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { writeFileIfChanged } from '../writeFileIfChanged';\nimport { generateConfigurationContent } from './generateConfigurationContent';\n\nexport const isCachedConfigurationUpToDate = async (\n configuration: IntlayerConfig\n): Promise<boolean | null> => {\n try {\n const mjsPath = join(configuration.system.configDir, 'configuration.mjs');\n const existingContent = await readFile(mjsPath, 'utf8');\n const expectedContent = generateConfigurationContent(configuration, 'esm');\n return existingContent === expectedContent;\n } catch {\n return null; // Can crash if file doesn't exist yet or config is not defined\n }\n};\n\nexport const writeConfiguration = async (configuration: IntlayerConfig) => {\n const { configDir } = configuration.system;\n\n await mkdir(configDir, { recursive: true });\n\n await Promise.all([\n writeFileIfChanged(\n join(configDir, 'configuration.mjs'),\n generateConfigurationContent(configuration, 'esm')\n ),\n writeFileIfChanged(\n join(configDir, 'configuration.cjs'),\n generateConfigurationContent(configuration, 'cjs')\n ),\n ]);\n};\n"],"mappings":";;;;;;AAMA,MAAa,gCAAgC,OAC3C,kBAC4B;CAC5B,IAAI;EACF,MAAM,UAAU,KAAK,cAAc,OAAO,WAAW,mBAAmB;EAGxE,OAAO,MAFuB,SAAS,SAAS,MAAM,MAC9B,6BAA6B,eAAe,KAC3B;CAC3C,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAa,qBAAqB,OAAO,kBAAkC;CACzE,MAAM,EAAE,cAAc,cAAc;CAEpC,MAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;CAE1C,MAAM,QAAQ,IAAI,CAChB,mBACE,KAAK,WAAW,mBAAmB,GACnC,6BAA6B,eAAe,KAAK,CACnD,GACA,mBACE,KAAK,WAAW,mBAAmB,GACnC,6BAA6B,eAAe,KAAK,CACnD,CACF,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/writeConfiguration/index.ts"],"sourcesContent":["import { mkdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { writeFileIfChanged } from '../writeFileIfChanged';\nimport { generateConfigurationContent } from './generateConfigurationContent';\n\nexport const isCachedConfigurationUpToDate = async (\n configuration: IntlayerConfig\n): Promise<boolean | null> => {\n try {\n const mjsPath = join(configuration.system.configDir, 'configuration.mjs');\n const existingContent = await readFile(mjsPath, 'utf8');\n const expectedContent = generateConfigurationContent(configuration, 'esm');\n return existingContent === expectedContent;\n } catch {\n return null; // Can crash if file doesn't exist yet or config is not defined\n }\n};\n\nexport const writeConfiguration = async (configuration: IntlayerConfig) => {\n const { configDir } = configuration.system;\n\n await mkdir(configDir, { recursive: true });\n\n await Promise.all([\n writeFileIfChanged(\n join(configDir, 'configuration.mjs'),\n generateConfigurationContent(configuration, 'esm')\n ),\n writeFileIfChanged(\n join(configDir, 'configuration.cjs'),\n generateConfigurationContent(configuration, 'cjs')\n ),\n ]);\n};\n\n/** Editor settings the built configuration can be overridden with. */\nexport type BuiltEditorOverride = Partial<\n Pick<IntlayerConfig['editor'], 'enabled' | 'editorURL'>\n>;\n\n/**\n * Writes the built configuration (`.intlayer/config`) with editor settings\n * overridden, leaving the user configuration file untouched. Lets the visual\n * editor connect to the application when the configuration disables it, or\n * points at another URL. The next configuration build restores the\n * configured values.\n *\n * @returns The overridden keys that differ from the configuration; nothing is\n * written when empty.\n */\nexport const overrideBuiltEditorConfiguration = async (\n configuration: IntlayerConfig,\n editorOverride: BuiltEditorOverride\n): Promise<(keyof BuiltEditorOverride)[]> => {\n const overriddenKeys = (\n Object.keys(editorOverride) as (keyof BuiltEditorOverride)[]\n ).filter(\n (key) =>\n editorOverride[key] !== undefined &&\n editorOverride[key] !== configuration.editor[key]\n );\n\n if (overriddenKeys.length === 0) return [];\n\n await writeConfiguration({\n ...configuration,\n editor: { ...configuration.editor, ...editorOverride },\n });\n\n return overriddenKeys;\n};\n"],"mappings":";;;;;;AAMA,MAAa,gCAAgC,OAC3C,kBAC4B;CAC5B,IAAI;EACF,MAAM,UAAU,KAAK,cAAc,OAAO,WAAW,mBAAmB;EAGxE,OAAO,MAFuB,SAAS,SAAS,MAAM,MAC9B,6BAA6B,eAAe,KAC3B;CAC3C,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAa,qBAAqB,OAAO,kBAAkC;CACzE,MAAM,EAAE,cAAc,cAAc;CAEpC,MAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;CAE1C,MAAM,QAAQ,IAAI,CAChB,mBACE,KAAK,WAAW,mBAAmB,GACnC,6BAA6B,eAAe,KAAK,CACnD,GACA,mBACE,KAAK,WAAW,mBAAmB,GACnC,6BAA6B,eAAe,KAAK,CACnD,CACF,CAAC;AACH;;;;;;;;;;;AAiBA,MAAa,mCAAmC,OAC9C,eACA,mBAC2C;CAC3C,MAAM,iBACJ,OAAO,KAAK,cAAc,CAAC,CAC3B,QACC,QACC,eAAe,SAAS,UACxB,eAAe,SAAS,cAAc,OAAO,IACjD;CAEA,IAAI,eAAe,WAAW,GAAG,OAAO,CAAC;CAEzC,MAAM,mBAAmB;EACvB,GAAG;EACH,QAAQ;GAAE,GAAG,cAAc;GAAQ,GAAG;EAAe;CACvD,CAAC;CAED,OAAO;AACT"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { detectExportedComponentName } from "./detectExportedComponentName.mjs";
|
|
2
2
|
import { transformJSFile } from "./transformJSFile.mjs";
|
|
3
|
+
import { formatContentDeclaration, isRenderableContentDeclarationPath, renderContentDeclaration, renderMarkdownContentDeclaration, renderYamlContentDeclaration } from "./renderContentDeclaration.mjs";
|
|
3
4
|
import { writeJSFile } from "./writeJSFile.mjs";
|
|
4
5
|
import { writeContentDeclaration } from "./writeContentDeclaration.mjs";
|
|
5
6
|
|
|
6
|
-
export { detectExportedComponentName, transformJSFile, writeContentDeclaration, writeJSFile };
|
|
7
|
+
export { detectExportedComponentName, formatContentDeclaration, isRenderableContentDeclarationPath, renderContentDeclaration, renderMarkdownContentDeclaration, renderYamlContentDeclaration, transformJSFile, writeContentDeclaration, writeJSFile };
|
|
@@ -6,9 +6,22 @@ import { deepTransformNode } from "@intlayer/core/interpreter";
|
|
|
6
6
|
|
|
7
7
|
//#region src/writeContentDeclaration/processContentDeclarationContent.ts
|
|
8
8
|
/**
|
|
9
|
+
* Writes the content of a `file()` node to disk.
|
|
10
|
+
*/
|
|
11
|
+
const writeExternalFileToDisk = (filePath, fileContent) => {
|
|
12
|
+
try {
|
|
13
|
+
const absoluteFilePath = join(process.cwd(), filePath);
|
|
14
|
+
const fileDirectory = dirname(absoluteFilePath);
|
|
15
|
+
if (!existsSync(fileDirectory)) mkdirSync(fileDirectory, { recursive: true });
|
|
16
|
+
writeFileSync(absoluteFilePath, fileContent);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new Error(`${x} Error writing file to ${colorizePath(filePath)}: ${error}`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
9
22
|
* Write file plugin
|
|
10
23
|
*/
|
|
11
|
-
const
|
|
24
|
+
const getWriteFilePlugin = (onExternalFile) => ({
|
|
12
25
|
id: "write-file-plugin",
|
|
13
26
|
canHandle: (node) => typeof node === "object" && node?.nodeType === NodeTypes.FILE,
|
|
14
27
|
transform: (node) => {
|
|
@@ -16,20 +29,13 @@ const writeFilePlugin = {
|
|
|
16
29
|
const filePath = node.fixedPath;
|
|
17
30
|
if (typeof fileContent !== "string") throw new Error("File content must be a string");
|
|
18
31
|
if (typeof filePath !== "string") throw new Error("File path must be a string");
|
|
19
|
-
|
|
20
|
-
const absoluteFilePath = join(process.cwd(), filePath);
|
|
21
|
-
const fileDirectory = dirname(absoluteFilePath);
|
|
22
|
-
if (!existsSync(fileDirectory)) mkdirSync(fileDirectory, { recursive: true });
|
|
23
|
-
writeFileSync(absoluteFilePath, fileContent);
|
|
24
|
-
} catch (error) {
|
|
25
|
-
throw new Error(`${x} Error writing file to ${colorizePath(filePath)}: ${error}`);
|
|
26
|
-
}
|
|
32
|
+
onExternalFile(filePath, fileContent);
|
|
27
33
|
return {
|
|
28
34
|
nodeType: NodeTypes.FILE,
|
|
29
35
|
[NodeTypes.FILE]: node[NodeTypes.FILE]
|
|
30
36
|
};
|
|
31
37
|
}
|
|
32
|
-
};
|
|
38
|
+
});
|
|
33
39
|
/**
|
|
34
40
|
* Markdown file plugin
|
|
35
41
|
*/
|
|
@@ -74,13 +80,14 @@ const htmlFilePlugin = {
|
|
|
74
80
|
*
|
|
75
81
|
* It filter node that are autogenerated by intlayer to do not rewrite them in the content declaration file.
|
|
76
82
|
*
|
|
77
|
-
* And write external sources as file content if necessary.
|
|
83
|
+
* And write external sources as file content if necessary. Pass
|
|
84
|
+
* `onExternalFile` to collect them instead of writing them to disk.
|
|
78
85
|
*/
|
|
79
|
-
const processContentDeclarationContent = async (dictionary) => deepTransformNode(dictionary, {
|
|
86
|
+
const processContentDeclarationContent = async (dictionary, onExternalFile = writeExternalFileToDisk) => deepTransformNode(dictionary, {
|
|
80
87
|
dictionaryKey: dictionary.key,
|
|
81
88
|
keyPath: [],
|
|
82
89
|
plugins: [
|
|
83
|
-
|
|
90
|
+
getWriteFilePlugin(onExternalFile),
|
|
84
91
|
markdownFilePlugin,
|
|
85
92
|
insertionFilePlugin,
|
|
86
93
|
htmlFilePlugin
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"processContentDeclarationContent.mjs","names":[],"sources":["../../../src/writeContentDeclaration/processContentDeclarationContent.ts"],"sourcesContent":["import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { colorizePath, x } from '@intlayer/config/logger';\nimport { deepTransformNode, type Plugins } from '@intlayer/core/interpreter';\nimport type {\n FileContent,\n FileContentConstructor,\n HTMLContent,\n HTMLContentConstructor,\n InsertionContent,\n InsertionContentConstructor,\n MarkdownContent,\n MarkdownContentConstructor,\n} from '@intlayer/core/transpiler';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport * as NodeTypes from '@intlayer/types/nodeType';\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"processContentDeclarationContent.mjs","names":[],"sources":["../../../src/writeContentDeclaration/processContentDeclarationContent.ts"],"sourcesContent":["import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { colorizePath, x } from '@intlayer/config/logger';\nimport { deepTransformNode, type Plugins } from '@intlayer/core/interpreter';\nimport type {\n FileContent,\n FileContentConstructor,\n HTMLContent,\n HTMLContentConstructor,\n InsertionContent,\n InsertionContentConstructor,\n MarkdownContent,\n MarkdownContentConstructor,\n} from '@intlayer/core/transpiler';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport * as NodeTypes from '@intlayer/types/nodeType';\n\n/**\n * Receives the content of a `file()` node instead of writing it to disk.\n * `filePath` is relative to the project base directory.\n */\nexport type ExternalFileHandler = (filePath: string, content: string) => void;\n\n/**\n * Writes the content of a `file()` node to disk.\n */\nconst writeExternalFileToDisk: ExternalFileHandler = (\n filePath,\n fileContent\n) => {\n try {\n const absoluteFilePath = join(process.cwd(), filePath);\n\n // Create the file directory if it doesn't exist\n const fileDirectory = dirname(absoluteFilePath);\n\n if (!existsSync(fileDirectory)) {\n mkdirSync(fileDirectory, { recursive: true });\n }\n\n // Write the file\n writeFileSync(absoluteFilePath, fileContent);\n } catch (error) {\n throw new Error(\n `${x} Error writing file to ${colorizePath(filePath)}: ${error}`\n );\n }\n};\n\n/**\n * Write file plugin\n */\n\nconst getWriteFilePlugin = (onExternalFile: ExternalFileHandler): Plugins => ({\n id: 'write-file-plugin',\n canHandle: (node) =>\n typeof node === 'object' && node?.nodeType === NodeTypes.FILE,\n transform: (node: FileContent) => {\n const fileContent = node.content;\n const filePath = node.fixedPath;\n\n if (typeof fileContent !== 'string') {\n throw new Error('File content must be a string');\n }\n\n if (typeof filePath !== 'string') {\n throw new Error('File path must be a string');\n }\n\n onExternalFile(filePath, fileContent);\n\n const transformedFileContent: FileContentConstructor = {\n nodeType: NodeTypes.FILE,\n [NodeTypes.FILE]: node[NodeTypes.FILE],\n };\n\n return transformedFileContent;\n },\n});\n\n/**\n * Markdown file plugin\n */\n\nconst markdownFilePlugin: Plugins = {\n id: 'markdown-file-plugin',\n canHandle: (node) =>\n typeof node === 'object' && node?.nodeType === NodeTypes.MARKDOWN,\n transform: (node: MarkdownContent, props, deepTransformNode) => {\n const simplifiedMarkdownNode: MarkdownContentConstructor = {\n nodeType: NodeTypes.MARKDOWN,\n [NodeTypes.MARKDOWN]: deepTransformNode(node[NodeTypes.MARKDOWN], props),\n };\n\n return simplifiedMarkdownNode;\n },\n};\n\n/**\n * Insertion file plugin\n */\n\nconst insertionFilePlugin: Plugins = {\n id: 'insertion-file-plugin',\n canHandle: (node) =>\n typeof node === 'object' && node?.nodeType === NodeTypes.INSERTION,\n transform: (node: InsertionContent, props, deepTransformNode) => {\n const simplifiedInsertionNode: InsertionContentConstructor = {\n nodeType: NodeTypes.INSERTION,\n [NodeTypes.INSERTION]: deepTransformNode(\n node[NodeTypes.INSERTION],\n props\n ),\n };\n\n return simplifiedInsertionNode;\n },\n};\n\n/**\n * HTML file plugin\n */\n\nconst htmlFilePlugin: Plugins = {\n id: 'html-file-plugin',\n canHandle: (node) =>\n typeof node === 'object' && node?.nodeType === NodeTypes.HTML,\n transform: (node: HTMLContent, props, deepTransformNode) => {\n const simplifiedHTMLNode: HTMLContentConstructor = {\n nodeType: NodeTypes.HTML,\n [NodeTypes.HTML]: deepTransformNode(node[NodeTypes.HTML], props),\n };\n\n return simplifiedHTMLNode;\n },\n};\n\n/**\n * Process content declaration content\n *\n * It filter node that are autogenerated by intlayer to do not rewrite them in the content declaration file.\n *\n * And write external sources as file content if necessary. Pass\n * `onExternalFile` to collect them instead of writing them to disk.\n */\nexport const processContentDeclarationContent = async (\n dictionary: Dictionary,\n onExternalFile: ExternalFileHandler = writeExternalFileToDisk\n) =>\n deepTransformNode(dictionary, {\n dictionaryKey: dictionary.key,\n keyPath: [],\n plugins: [\n getWriteFilePlugin(onExternalFile),\n markdownFilePlugin,\n insertionFilePlugin,\n htmlFilePlugin,\n ],\n });\n"],"mappings":";;;;;;;;;;AA0BA,MAAM,2BACJ,UACA,gBACG;CACH,IAAI;EACF,MAAM,mBAAmB,KAAK,QAAQ,IAAI,GAAG,QAAQ;EAGrD,MAAM,gBAAgB,QAAQ,gBAAgB;EAE9C,IAAI,CAAC,WAAW,aAAa,GAC3B,UAAU,eAAe,EAAE,WAAW,KAAK,CAAC;EAI9C,cAAc,kBAAkB,WAAW;CAC7C,SAAS,OAAO;EACd,MAAM,IAAI,MACR,GAAG,EAAE,yBAAyB,aAAa,QAAQ,EAAE,IAAI,OAC3D;CACF;AACF;;;;AAMA,MAAM,sBAAsB,oBAAkD;CAC5E,IAAI;CACJ,YAAY,SACV,OAAO,SAAS,YAAY,MAAM,aAAa,UAAU;CAC3D,YAAY,SAAsB;EAChC,MAAM,cAAc,KAAK;EACzB,MAAM,WAAW,KAAK;EAEtB,IAAI,OAAO,gBAAgB,UACzB,MAAM,IAAI,MAAM,+BAA+B;EAGjD,IAAI,OAAO,aAAa,UACtB,MAAM,IAAI,MAAM,4BAA4B;EAG9C,eAAe,UAAU,WAAW;EAOpC,OAAO;GAJL,UAAU,UAAU;IACnB,UAAU,OAAO,KAAK,UAAU;EAGP;CAC9B;AACF;;;;AAMA,MAAM,qBAA8B;CAClC,IAAI;CACJ,YAAY,SACV,OAAO,SAAS,YAAY,MAAM,aAAa,UAAU;CAC3D,YAAY,MAAuB,OAAO,sBAAsB;EAM9D,OAAO;GAJL,UAAU,UAAU;IACnB,UAAU,WAAW,kBAAkB,KAAK,UAAU,WAAW,KAAK;EAG7C;CAC9B;AACF;;;;AAMA,MAAM,sBAA+B;CACnC,IAAI;CACJ,YAAY,SACV,OAAO,SAAS,YAAY,MAAM,aAAa,UAAU;CAC3D,YAAY,MAAwB,OAAO,sBAAsB;EAS/D,OAAO;GAPL,UAAU,UAAU;IACnB,UAAU,YAAY,kBACrB,KAAK,UAAU,YACf,KACF;EAG2B;CAC/B;AACF;;;;AAMA,MAAM,iBAA0B;CAC9B,IAAI;CACJ,YAAY,SACV,OAAO,SAAS,YAAY,MAAM,aAAa,UAAU;CAC3D,YAAY,MAAmB,OAAO,sBAAsB;EAM1D,OAAO;GAJL,UAAU,UAAU;IACnB,UAAU,OAAO,kBAAkB,KAAK,UAAU,OAAO,KAAK;EAGzC;CAC1B;AACF;;;;;;;;;AAUA,MAAa,mCAAmC,OAC9C,YACA,iBAAsC,4BAEtC,kBAAkB,YAAY;CAC5B,eAAe,WAAW;CAC1B,SAAS,CAAC;CACV,SAAS;EACP,mBAAmB,cAAc;EACjC;EACA;EACA;CACF;AACF,CAAC"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { getFormatFromExtension } from "../utils/getFormatFromExtension.mjs";
|
|
2
|
+
import { processContentDeclarationContent } from "./processContentDeclarationContent.mjs";
|
|
3
|
+
import { transformJSFile } from "./transformJSFile.mjs";
|
|
4
|
+
import { transformJSONFile } from "./transformJSONFile.mjs";
|
|
5
|
+
import { MARKDOWN } from "@intlayer/types/nodeType";
|
|
6
|
+
import { getFilteredLocalesDictionary, getPerLocaleDictionary } from "@intlayer/core/plugins";
|
|
7
|
+
import { parseYaml, stringifyYaml } from "@intlayer/core/utils";
|
|
8
|
+
|
|
9
|
+
//#region src/writeContentDeclaration/renderContentDeclaration.ts
|
|
10
|
+
/**
|
|
11
|
+
* Fields that are internal to the build and must never be persisted in a
|
|
12
|
+
* content declaration file.
|
|
13
|
+
*/
|
|
14
|
+
const INTERNAL_FIELDS = /* @__PURE__ */ new Set([
|
|
15
|
+
"$schema",
|
|
16
|
+
"filePath",
|
|
17
|
+
"localId",
|
|
18
|
+
"localIds",
|
|
19
|
+
"projectIds"
|
|
20
|
+
]);
|
|
21
|
+
/** Fields that are auto-generated or runtime-only, not persisted in YAML. */
|
|
22
|
+
const EXCLUDED_YAML_KEYS = /* @__PURE__ */ new Set([
|
|
23
|
+
"$schema",
|
|
24
|
+
"id",
|
|
25
|
+
"filePath"
|
|
26
|
+
]);
|
|
27
|
+
/** Fields that must not appear in a markdown frontmatter. */
|
|
28
|
+
const EXCLUDED_FRONTMATTER_KEYS = /* @__PURE__ */ new Set([...INTERNAL_FIELDS, "content"]);
|
|
29
|
+
const JS_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
30
|
+
".ts",
|
|
31
|
+
".tsx",
|
|
32
|
+
".js",
|
|
33
|
+
".jsx",
|
|
34
|
+
".mjs",
|
|
35
|
+
".cjs"
|
|
36
|
+
]);
|
|
37
|
+
const JSON_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
38
|
+
".json",
|
|
39
|
+
".jsonc",
|
|
40
|
+
".json5"
|
|
41
|
+
]);
|
|
42
|
+
const MARKDOWN_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx"]);
|
|
43
|
+
const YAML_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
|
|
44
|
+
const getExtension = (filePath) => {
|
|
45
|
+
const fileName = filePath.split(/[\\/]/).pop() ?? "";
|
|
46
|
+
const dotIndex = fileName.lastIndexOf(".");
|
|
47
|
+
return dotIndex === -1 ? "" : fileName.slice(dotIndex).toLowerCase();
|
|
48
|
+
};
|
|
49
|
+
const omitFields = (record, excludedFields) => Object.fromEntries(Object.entries(record).filter(([fieldName, value]) => !excludedFields.has(fieldName) && value !== void 0));
|
|
50
|
+
/**
|
|
51
|
+
* Whether a content declaration file can be rewritten from a dictionary.
|
|
52
|
+
* Other extensions (e.g. `.po`) are owned by plugins.
|
|
53
|
+
*/
|
|
54
|
+
const isRenderableContentDeclarationPath = (filePath) => {
|
|
55
|
+
const extension = getExtension(filePath);
|
|
56
|
+
return JS_EXTENSIONS.has(extension) || JSON_EXTENSIONS.has(extension) || MARKDOWN_EXTENSIONS.has(extension) || YAML_EXTENSIONS.has(extension);
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Cleans a dictionary before persisting it: strips auto-generated node
|
|
60
|
+
* metadata, filters locales, and applies the plugins `formatOutput` hooks.
|
|
61
|
+
*/
|
|
62
|
+
const formatContentDeclaration = async (dictionary, options = {}) => {
|
|
63
|
+
const { configuration, localeList, onExternalFile } = options;
|
|
64
|
+
const processedDictionary = await processContentDeclarationContent(dictionary, onExternalFile);
|
|
65
|
+
let content = processedDictionary.content;
|
|
66
|
+
if (dictionary.locale) content = getPerLocaleDictionary(processedDictionary, dictionary.locale).content;
|
|
67
|
+
else if (localeList) content = getFilteredLocalesDictionary(processedDictionary, localeList).content;
|
|
68
|
+
let pluginFormatResult = {
|
|
69
|
+
...dictionary,
|
|
70
|
+
content
|
|
71
|
+
};
|
|
72
|
+
for await (const plugin of configuration?.plugins ?? []) if (plugin.formatOutput && configuration) {
|
|
73
|
+
const formattedResult = await plugin.formatOutput({
|
|
74
|
+
dictionary: pluginFormatResult,
|
|
75
|
+
configuration
|
|
76
|
+
});
|
|
77
|
+
if (formattedResult) pluginFormatResult = formattedResult;
|
|
78
|
+
}
|
|
79
|
+
if (!(pluginFormatResult.content && pluginFormatResult.key)) return pluginFormatResult;
|
|
80
|
+
let result = {
|
|
81
|
+
...omitFields(dictionary, INTERNAL_FIELDS),
|
|
82
|
+
content
|
|
83
|
+
};
|
|
84
|
+
const extension = dictionary.filePath ? getExtension(dictionary.filePath) : ".json";
|
|
85
|
+
if (getFormatFromExtension(extension) === "json") result = {
|
|
86
|
+
$schema: "https://intlayer.org/schema.json",
|
|
87
|
+
...result
|
|
88
|
+
};
|
|
89
|
+
return result;
|
|
90
|
+
};
|
|
91
|
+
const stringifyYamlFrontmatter = (fields) => Object.entries(fields).filter(([, value]) => value !== void 0 && value !== null).map(([fieldName, value]) => {
|
|
92
|
+
if (Array.isArray(value)) return `${fieldName}:\n${value.map((item) => ` - ${JSON.stringify(item)}`).join("\n")}`;
|
|
93
|
+
if (typeof value === "string" && (value.includes(":") || value.includes("\n") || value.includes("#"))) return `${fieldName}: ${JSON.stringify(value)}`;
|
|
94
|
+
return `${fieldName}: ${value}`;
|
|
95
|
+
}).join("\n");
|
|
96
|
+
/**
|
|
97
|
+
* Splits a markdown string into its YAML frontmatter and its body.
|
|
98
|
+
*/
|
|
99
|
+
const splitMarkdown = (markdown) => {
|
|
100
|
+
const lines = markdown.split(/\r?\n/);
|
|
101
|
+
const firstNonEmptyIndex = lines.findIndex((line) => line.trim() !== "");
|
|
102
|
+
if (firstNonEmptyIndex === -1 || lines[firstNonEmptyIndex]?.trim() !== "---") return {
|
|
103
|
+
frontmatter: void 0,
|
|
104
|
+
body: markdown
|
|
105
|
+
};
|
|
106
|
+
const endIndex = lines.findIndex((line, index) => index > firstNonEmptyIndex && line.trim() === "---");
|
|
107
|
+
if (endIndex === -1) return {
|
|
108
|
+
frontmatter: void 0,
|
|
109
|
+
body: markdown
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
frontmatter: lines.slice(firstNonEmptyIndex + 1, endIndex).join("\n"),
|
|
113
|
+
body: lines.slice(endIndex + 1).join("\n").trimStart()
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Renders a `.content.md` file. When the current file is provided, its
|
|
118
|
+
* frontmatter fields are kept and only overridden by the dictionary ones; the
|
|
119
|
+
* original frontmatter text is reused verbatim when nothing changed.
|
|
120
|
+
*/
|
|
121
|
+
const renderMarkdownContentDeclaration = (dictionary, existingFileContent) => {
|
|
122
|
+
const content = dictionary.content;
|
|
123
|
+
const markdownRaw = typeof content === "object" && content?.nodeType === MARKDOWN ? String(content[MARKDOWN] ?? "") : "";
|
|
124
|
+
const { body } = splitMarkdown(markdownRaw);
|
|
125
|
+
const dictionaryFields = omitFields(dictionary, EXCLUDED_FRONTMATTER_KEYS);
|
|
126
|
+
const existingFrontmatter = existingFileContent === void 0 ? void 0 : splitMarkdown(existingFileContent).frontmatter;
|
|
127
|
+
if (existingFrontmatter === void 0) return `---\n${stringifyYamlFrontmatter(dictionaryFields)}\n---\n\n${body}`;
|
|
128
|
+
const existingFields = parseYaml(existingFrontmatter) ?? {};
|
|
129
|
+
const mergedFields = {
|
|
130
|
+
...existingFields,
|
|
131
|
+
...dictionaryFields
|
|
132
|
+
};
|
|
133
|
+
const mergedFrontmatter = stringifyYamlFrontmatter(mergedFields);
|
|
134
|
+
return `---\n${stringifyYamlFrontmatter(existingFields) === mergedFrontmatter ? existingFrontmatter : mergedFrontmatter}\n---\n\n${body}`;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Renders a `.content.yaml` file. When the current file is provided, its
|
|
138
|
+
* fields are kept and only overridden by the dictionary ones; the original
|
|
139
|
+
* text is returned untouched when nothing changed.
|
|
140
|
+
*/
|
|
141
|
+
const renderYamlContentDeclaration = (dictionary, existingFileContent) => {
|
|
142
|
+
const dictionaryFields = omitFields(dictionary, EXCLUDED_YAML_KEYS);
|
|
143
|
+
if (existingFileContent === void 0) return stringifyYaml(dictionaryFields);
|
|
144
|
+
const existingFields = parseYaml(existingFileContent) ?? {};
|
|
145
|
+
const mergedFields = {
|
|
146
|
+
...existingFields,
|
|
147
|
+
...dictionaryFields
|
|
148
|
+
};
|
|
149
|
+
if (stringifyYaml(existingFields) === stringifyYaml(mergedFields)) return existingFileContent;
|
|
150
|
+
return stringifyYaml(mergedFields);
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Whether a content declaration source declares only the content (compiler
|
|
154
|
+
* `noMetadata` mode) rather than a `{ key, content }` dictionary.
|
|
155
|
+
*/
|
|
156
|
+
const isContentOnlyDeclaration = (fileContent, extension) => {
|
|
157
|
+
if (JSON_EXTENSIONS.has(extension)) try {
|
|
158
|
+
const parsed = JSON.parse(fileContent);
|
|
159
|
+
return !("content" in parsed) && !("key" in parsed);
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return !/(^|[\s{,])['"]?content['"]?\s*:/m.test(fileContent);
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* Renders the new source of an existing content declaration file from a
|
|
167
|
+
* dictionary, without touching the file system. JS/TS and JSON sources are
|
|
168
|
+
* updated through their AST so untouched code keeps its formatting.
|
|
169
|
+
*
|
|
170
|
+
* Returns `undefined` for formats owned by plugins (e.g. `.po`).
|
|
171
|
+
*/
|
|
172
|
+
const renderContentDeclaration = async (dictionary, options) => {
|
|
173
|
+
const { filePath, fileContent, configuration, localeList } = options;
|
|
174
|
+
const extension = getExtension(filePath);
|
|
175
|
+
if (!isRenderableContentDeclarationPath(filePath)) return void 0;
|
|
176
|
+
const externalFiles = {};
|
|
177
|
+
const formattedDictionary = await formatContentDeclaration({
|
|
178
|
+
...dictionary,
|
|
179
|
+
filePath
|
|
180
|
+
}, {
|
|
181
|
+
configuration,
|
|
182
|
+
localeList,
|
|
183
|
+
onExternalFile: (externalFilePath, externalFileContent) => {
|
|
184
|
+
externalFiles[externalFilePath] = externalFileContent;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
if (MARKDOWN_EXTENSIONS.has(extension)) return {
|
|
188
|
+
fileContent: renderMarkdownContentDeclaration(formattedDictionary, fileContent),
|
|
189
|
+
externalFiles
|
|
190
|
+
};
|
|
191
|
+
if (YAML_EXTENSIONS.has(extension)) return {
|
|
192
|
+
fileContent: renderYamlContentDeclaration(formattedDictionary, fileContent),
|
|
193
|
+
externalFiles
|
|
194
|
+
};
|
|
195
|
+
const noMetadata = options.noMetadata ?? isContentOnlyDeclaration(fileContent, extension);
|
|
196
|
+
if (JSON_EXTENSIONS.has(extension)) return {
|
|
197
|
+
fileContent: transformJSONFile(fileContent, formattedDictionary, noMetadata),
|
|
198
|
+
externalFiles
|
|
199
|
+
};
|
|
200
|
+
return {
|
|
201
|
+
fileContent: await transformJSFile(fileContent, formattedDictionary, dictionary.locale, noMetadata),
|
|
202
|
+
externalFiles
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
//#endregion
|
|
207
|
+
export { formatContentDeclaration, isRenderableContentDeclarationPath, renderContentDeclaration, renderMarkdownContentDeclaration, renderYamlContentDeclaration };
|
|
208
|
+
//# sourceMappingURL=renderContentDeclaration.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderContentDeclaration.mjs","names":[],"sources":["../../../src/writeContentDeclaration/renderContentDeclaration.ts"],"sourcesContent":["import {\n getFilteredLocalesDictionary,\n getPerLocaleDictionary,\n} from '@intlayer/core/plugins';\nimport { parseYaml, stringifyYaml } from '@intlayer/core/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport type { LocalesValues } from '@intlayer/types/module_augmentation';\nimport { MARKDOWN } from '@intlayer/types/nodeType';\nimport {\n type Extension,\n getFormatFromExtension,\n} from '../utils/getFormatFromExtension';\nimport {\n type ExternalFileHandler,\n processContentDeclarationContent,\n} from './processContentDeclarationContent';\nimport { transformJSFile } from './transformJSFile';\nimport { transformJSONFile } from './transformJSONFile';\n\n/**\n * Fields that are internal to the build and must never be persisted in a\n * content declaration file.\n */\nconst INTERNAL_FIELDS = new Set<string>([\n '$schema',\n 'filePath',\n 'localId',\n 'localIds',\n 'projectIds',\n]);\n\n/** Fields that are auto-generated or runtime-only, not persisted in YAML. */\nconst EXCLUDED_YAML_KEYS = new Set<string>(['$schema', 'id', 'filePath']);\n\n/** Fields that must not appear in a markdown frontmatter. */\nconst EXCLUDED_FRONTMATTER_KEYS = new Set<string>([\n ...INTERNAL_FIELDS,\n 'content',\n]);\n\nconst JS_EXTENSIONS = new Set<string>([\n '.ts',\n '.tsx',\n '.js',\n '.jsx',\n '.mjs',\n '.cjs',\n]);\n\nconst JSON_EXTENSIONS = new Set<string>(['.json', '.jsonc', '.json5']);\n\nconst MARKDOWN_EXTENSIONS = new Set<string>(['.md', '.mdx']);\n\nconst YAML_EXTENSIONS = new Set<string>(['.yaml', '.yml']);\n\nconst getExtension = (filePath: string): string => {\n const fileName = filePath.split(/[\\\\/]/).pop() ?? '';\n const dotIndex = fileName.lastIndexOf('.');\n\n return dotIndex === -1 ? '' : fileName.slice(dotIndex).toLowerCase();\n};\n\nconst omitFields = (\n record: Record<string, unknown>,\n excludedFields: Set<string>\n): Record<string, unknown> =>\n Object.fromEntries(\n Object.entries(record).filter(\n ([fieldName, value]) =>\n !excludedFields.has(fieldName) && value !== undefined\n )\n );\n\n/**\n * Whether a content declaration file can be rewritten from a dictionary.\n * Other extensions (e.g. `.po`) are owned by plugins.\n */\nexport const isRenderableContentDeclarationPath = (\n filePath: string\n): boolean => {\n const extension = getExtension(filePath);\n\n return (\n JS_EXTENSIONS.has(extension) ||\n JSON_EXTENSIONS.has(extension) ||\n MARKDOWN_EXTENSIONS.has(extension) ||\n YAML_EXTENSIONS.has(extension)\n );\n};\n\ntype FormatContentDeclarationOptions = {\n /** Plugins `formatOutput` hooks are applied when provided. */\n configuration?: IntlayerConfig;\n localeList?: LocalesValues[];\n /** Collects `file()` node contents instead of writing them to disk. */\n onExternalFile?: ExternalFileHandler;\n};\n\n/**\n * Cleans a dictionary before persisting it: strips auto-generated node\n * metadata, filters locales, and applies the plugins `formatOutput` hooks.\n */\nexport const formatContentDeclaration = async (\n dictionary: Dictionary,\n options: FormatContentDeclarationOptions = {}\n): Promise<Dictionary> => {\n const { configuration, localeList, onExternalFile } = options;\n\n const processedDictionary = await processContentDeclarationContent(\n dictionary,\n onExternalFile\n );\n\n let content = processedDictionary.content;\n\n if (dictionary.locale) {\n content = getPerLocaleDictionary(\n processedDictionary,\n dictionary.locale\n ).content;\n } else if (localeList) {\n content = getFilteredLocalesDictionary(\n processedDictionary,\n localeList\n ).content;\n }\n\n let pluginFormatResult: any = {\n ...dictionary,\n content,\n } satisfies Dictionary;\n\n for await (const plugin of configuration?.plugins ?? []) {\n if (plugin.formatOutput && configuration) {\n const formattedResult = await plugin.formatOutput({\n dictionary: pluginFormatResult,\n configuration,\n });\n\n if (formattedResult) {\n pluginFormatResult = formattedResult;\n }\n }\n }\n\n const isDictionaryFormat =\n pluginFormatResult.content && pluginFormatResult.key;\n\n if (!isDictionaryFormat) return pluginFormatResult;\n\n // Build result from the original dictionary so that extra user-defined\n // fields (e.g. custom frontmatter in markdown files) are preserved.\n let result = {\n ...omitFields(dictionary as Record<string, unknown>, INTERNAL_FIELDS),\n content,\n } as Dictionary;\n\n const extension = (\n dictionary.filePath ? getExtension(dictionary.filePath) : '.json'\n ) as Extension;\n\n if (getFormatFromExtension(extension) === 'json') {\n result = {\n $schema: 'https://intlayer.org/schema.json',\n ...result,\n } as Dictionary;\n }\n\n return result;\n};\n\nconst stringifyYamlFrontmatter = (fields: Record<string, unknown>): string =>\n Object.entries(fields)\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([fieldName, value]) => {\n if (Array.isArray(value)) {\n return `${fieldName}:\\n${value.map((item) => ` - ${JSON.stringify(item)}`).join('\\n')}`;\n }\n if (\n typeof value === 'string' &&\n (value.includes(':') || value.includes('\\n') || value.includes('#'))\n ) {\n return `${fieldName}: ${JSON.stringify(value)}`;\n }\n return `${fieldName}: ${value}`;\n })\n .join('\\n');\n\ntype SplitMarkdown = {\n /** Raw frontmatter text, without the `---` fences. */\n frontmatter: string | undefined;\n body: string;\n};\n\n/**\n * Splits a markdown string into its YAML frontmatter and its body.\n */\nconst splitMarkdown = (markdown: string): SplitMarkdown => {\n const lines = markdown.split(/\\r?\\n/);\n const firstNonEmptyIndex = lines.findIndex((line) => line.trim() !== '');\n\n if (\n firstNonEmptyIndex === -1 ||\n lines[firstNonEmptyIndex]?.trim() !== '---'\n ) {\n return { frontmatter: undefined, body: markdown };\n }\n\n const endIndex = lines.findIndex(\n (line, index) => index > firstNonEmptyIndex && line.trim() === '---'\n );\n\n if (endIndex === -1) return { frontmatter: undefined, body: markdown };\n\n return {\n frontmatter: lines.slice(firstNonEmptyIndex + 1, endIndex).join('\\n'),\n body: lines\n .slice(endIndex + 1)\n .join('\\n')\n .trimStart(),\n };\n};\n\n/**\n * Renders a `.content.md` file. When the current file is provided, its\n * frontmatter fields are kept and only overridden by the dictionary ones; the\n * original frontmatter text is reused verbatim when nothing changed.\n */\nexport const renderMarkdownContentDeclaration = (\n dictionary: Dictionary,\n existingFileContent?: string\n): string => {\n const content = dictionary.content as Record<string, unknown> | undefined;\n const markdownRaw =\n typeof content === 'object' && content?.nodeType === MARKDOWN\n ? String(content[MARKDOWN] ?? '')\n : '';\n // content.markdown stores the full file (frontmatter + body)\n const { body } = splitMarkdown(markdownRaw);\n\n const dictionaryFields = omitFields(\n dictionary as Record<string, unknown>,\n EXCLUDED_FRONTMATTER_KEYS\n );\n\n const existingFrontmatter =\n existingFileContent === undefined\n ? undefined\n : splitMarkdown(existingFileContent).frontmatter;\n\n if (existingFrontmatter === undefined) {\n return `---\\n${stringifyYamlFrontmatter(dictionaryFields)}\\n---\\n\\n${body}`;\n }\n\n const existingFields =\n parseYaml<Record<string, unknown>>(existingFrontmatter) ?? {};\n const mergedFields = { ...existingFields, ...dictionaryFields };\n\n const mergedFrontmatter = stringifyYamlFrontmatter(mergedFields);\n const frontmatter =\n stringifyYamlFrontmatter(existingFields) === mergedFrontmatter\n ? existingFrontmatter\n : mergedFrontmatter;\n\n return `---\\n${frontmatter}\\n---\\n\\n${body}`;\n};\n\n/**\n * Renders a `.content.yaml` file. When the current file is provided, its\n * fields are kept and only overridden by the dictionary ones; the original\n * text is returned untouched when nothing changed.\n */\nexport const renderYamlContentDeclaration = (\n dictionary: Dictionary,\n existingFileContent?: string\n): string => {\n const dictionaryFields = omitFields(\n dictionary as Record<string, unknown>,\n EXCLUDED_YAML_KEYS\n );\n\n if (existingFileContent === undefined) {\n return stringifyYaml(dictionaryFields);\n }\n\n const existingFields =\n parseYaml<Record<string, unknown>>(existingFileContent) ?? {};\n const mergedFields = { ...existingFields, ...dictionaryFields };\n\n if (stringifyYaml(existingFields) === stringifyYaml(mergedFields)) {\n return existingFileContent;\n }\n\n return stringifyYaml(mergedFields);\n};\n\n/**\n * Whether a content declaration source declares only the content (compiler\n * `noMetadata` mode) rather than a `{ key, content }` dictionary.\n */\nconst isContentOnlyDeclaration = (\n fileContent: string,\n extension: string\n): boolean => {\n if (JSON_EXTENSIONS.has(extension)) {\n try {\n const parsed = JSON.parse(fileContent) as Record<string, unknown>;\n\n return !('content' in parsed) && !('key' in parsed);\n } catch {\n return false;\n }\n }\n\n return !/(^|[\\s{,])['\"]?content['\"]?\\s*:/m.test(fileContent);\n};\n\nexport type RenderContentDeclarationOptions = {\n /** Path of the content declaration file, used to pick its format. */\n filePath: string;\n /** Current source of the file. */\n fileContent: string;\n /** Applies the plugins `formatOutput` hooks when provided. */\n configuration?: IntlayerConfig;\n localeList?: LocalesValues[];\n /** Forces the `noMetadata` mode; detected from the source when omitted. */\n noMetadata?: boolean;\n};\n\nexport type RenderedContentDeclaration = {\n /** New source of the content declaration file. */\n fileContent: string;\n /** Content of the `file()` nodes, keyed by path relative to the base dir. */\n externalFiles: Record<string, string>;\n};\n\n/**\n * Renders the new source of an existing content declaration file from a\n * dictionary, without touching the file system. JS/TS and JSON sources are\n * updated through their AST so untouched code keeps its formatting.\n *\n * Returns `undefined` for formats owned by plugins (e.g. `.po`).\n */\nexport const renderContentDeclaration = async (\n dictionary: Dictionary,\n options: RenderContentDeclarationOptions\n): Promise<RenderedContentDeclaration | undefined> => {\n const { filePath, fileContent, configuration, localeList } = options;\n const extension = getExtension(filePath);\n\n if (!isRenderableContentDeclarationPath(filePath)) return undefined;\n\n const externalFiles: Record<string, string> = {};\n\n const formattedDictionary = await formatContentDeclaration(\n { ...dictionary, filePath },\n {\n configuration,\n localeList,\n onExternalFile: (externalFilePath, externalFileContent) => {\n externalFiles[externalFilePath] = externalFileContent;\n },\n }\n );\n\n if (MARKDOWN_EXTENSIONS.has(extension)) {\n return {\n fileContent: renderMarkdownContentDeclaration(\n formattedDictionary,\n fileContent\n ),\n externalFiles,\n };\n }\n\n if (YAML_EXTENSIONS.has(extension)) {\n return {\n fileContent: renderYamlContentDeclaration(\n formattedDictionary,\n fileContent\n ),\n externalFiles,\n };\n }\n\n const noMetadata =\n options.noMetadata ?? isContentOnlyDeclaration(fileContent, extension);\n\n if (JSON_EXTENSIONS.has(extension)) {\n return {\n fileContent: transformJSONFile(\n fileContent,\n formattedDictionary,\n noMetadata\n ),\n externalFiles,\n };\n }\n\n return {\n fileContent: await transformJSFile(\n fileContent,\n formattedDictionary,\n dictionary.locale as Locale | undefined,\n noMetadata\n ),\n externalFiles,\n };\n};\n"],"mappings":"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"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { getFormatFromExtension } from "../utils/getFormatFromExtension.mjs";
|
|
2
1
|
import { detectFormatCommand } from "../detectFormatCommand.mjs";
|
|
3
2
|
import { readDictionariesFromDisk } from "../utils/readDictionariesFromDisk.mjs";
|
|
4
|
-
import { processContentDeclarationContent } from "./processContentDeclarationContent.mjs";
|
|
5
3
|
import { transformJSONFile } from "./transformJSONFile.mjs";
|
|
4
|
+
import { formatContentDeclaration } from "./renderContentDeclaration.mjs";
|
|
6
5
|
import { writeJSFile } from "./writeJSFile.mjs";
|
|
7
6
|
import { writeMarkdownFile } from "./writeMarkdownFile.mjs";
|
|
8
7
|
import { writeYamlFile } from "./writeYamlFile.mjs";
|
|
@@ -11,57 +10,9 @@ import { COMPILER_NO_METADATA } from "@intlayer/config/defaultValues";
|
|
|
11
10
|
import { existsSync } from "node:fs";
|
|
12
11
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
13
12
|
import { execSync } from "node:child_process";
|
|
14
|
-
import { getFilteredLocalesDictionary, getPerLocaleDictionary } from "@intlayer/core/plugins";
|
|
15
13
|
import { isDeepStrictEqual } from "node:util";
|
|
16
14
|
|
|
17
15
|
//#region src/writeContentDeclaration/writeContentDeclaration.ts
|
|
18
|
-
const formatContentDeclaration = async (dictionary, configuration, localeList) => {
|
|
19
|
-
/**
|
|
20
|
-
* Clean Markdown, Insertion, File, etc. node metadata
|
|
21
|
-
*/
|
|
22
|
-
const processedDictionary = await processContentDeclarationContent(dictionary);
|
|
23
|
-
let content = processedDictionary.content;
|
|
24
|
-
/**
|
|
25
|
-
* Filter locales content
|
|
26
|
-
*/
|
|
27
|
-
if (dictionary.locale) content = getPerLocaleDictionary(processedDictionary, dictionary.locale).content;
|
|
28
|
-
else if (localeList) content = getFilteredLocalesDictionary(processedDictionary, localeList).content;
|
|
29
|
-
let pluginFormatResult = {
|
|
30
|
-
...dictionary,
|
|
31
|
-
content
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Format the dictionary with the plugins
|
|
35
|
-
*/
|
|
36
|
-
for await (const plugin of configuration.plugins ?? []) if (plugin.formatOutput) {
|
|
37
|
-
const formattedResult = await plugin.formatOutput?.({
|
|
38
|
-
dictionary: pluginFormatResult,
|
|
39
|
-
configuration
|
|
40
|
-
});
|
|
41
|
-
if (formattedResult) pluginFormatResult = formattedResult;
|
|
42
|
-
}
|
|
43
|
-
if (!(pluginFormatResult.content && pluginFormatResult.key)) return pluginFormatResult;
|
|
44
|
-
const INTERNAL_FIELDS = /* @__PURE__ */ new Set([
|
|
45
|
-
"$schema",
|
|
46
|
-
"filePath",
|
|
47
|
-
"localId",
|
|
48
|
-
"localIds",
|
|
49
|
-
"projectIds"
|
|
50
|
-
]);
|
|
51
|
-
let result = {
|
|
52
|
-
...Object.fromEntries(Object.entries(dictionary).filter(([k]) => !INTERNAL_FIELDS.has(k))),
|
|
53
|
-
content
|
|
54
|
-
};
|
|
55
|
-
/**
|
|
56
|
-
* Add $schema to JSON dictionaries
|
|
57
|
-
*/
|
|
58
|
-
const extension = dictionary.filePath ? extname(dictionary.filePath) : ".json";
|
|
59
|
-
if (getFormatFromExtension(extension) === "json" && pluginFormatResult.content && pluginFormatResult.key) result = {
|
|
60
|
-
$schema: "https://intlayer.org/schema.json",
|
|
61
|
-
...result
|
|
62
|
-
};
|
|
63
|
-
return result;
|
|
64
|
-
};
|
|
65
16
|
const defaultOptions = { newDictionariesPath: "intlayer-dictionaries" };
|
|
66
17
|
const writeContentDeclaration = async (dictionary, configuration, options) => {
|
|
67
18
|
const { system, compiler } = configuration;
|
|
@@ -73,7 +24,10 @@ const writeContentDeclaration = async (dictionary, configuration, options) => {
|
|
|
73
24
|
};
|
|
74
25
|
const newDictionaryLocationPath = join(baseDir, newDictionariesPath);
|
|
75
26
|
const existingDictionary = readDictionariesFromDisk(configuration.system.unmergedDictionariesDir)[dictionary.key]?.find((el) => el.localId === dictionary.localId);
|
|
76
|
-
const formattedContentDeclaration = await formatContentDeclaration(dictionary,
|
|
27
|
+
const formattedContentDeclaration = await formatContentDeclaration(dictionary, {
|
|
28
|
+
configuration,
|
|
29
|
+
localeList
|
|
30
|
+
});
|
|
77
31
|
if (existingDictionary?.filePath) {
|
|
78
32
|
const isSameContent = isDeepStrictEqual(existingDictionary, dictionary);
|
|
79
33
|
const filePath = resolve(configuration.system.baseDir, existingDictionary.filePath);
|