@intlayer/engine 9.5.10 → 9.5.12
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/bundle-optimization.md +123 -0
- package/dist/assets/installSkills/skills/compat.md +0 -4
- package/dist/assets/installSkills/skills/content.md +8 -2
- package/dist/assets/installSkills/skills/next-js.md +2 -2
- package/dist/assets/installSkills/skills/usage.md +1 -1
- package/dist/cjs/build.cjs +5 -0
- package/dist/cjs/cli.cjs +1 -0
- package/dist/cjs/docReview/alignBlocks.cjs +53 -36
- package/dist/cjs/docReview/alignBlocks.cjs.map +1 -1
- package/dist/cjs/docReview/rebuildDocument.cjs +3 -1
- package/dist/cjs/docReview/rebuildDocument.cjs.map +1 -1
- package/dist/cjs/docReview/segmentDocument.cjs +8 -5
- package/dist/cjs/docReview/segmentDocument.cjs.map +1 -1
- package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs +4 -1
- package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs.map +1 -1
- package/dist/cjs/init/index.cjs +2 -12
- package/dist/cjs/init/index.cjs.map +1 -1
- package/dist/cjs/init/utils/backendPackages.cjs +15 -0
- package/dist/cjs/init/utils/backendPackages.cjs.map +1 -0
- package/dist/cjs/init/utils/index.cjs +2 -3
- package/dist/cjs/installMCP/installMCP.cjs +1 -1
- package/dist/cjs/installMCP/installMCP.cjs.map +1 -1
- package/dist/cjs/installSkills/index.cjs +2 -0
- package/dist/cjs/installSkills/index.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/loadDictionaries.cjs +9 -8
- package/dist/cjs/loadDictionaries/loadDictionaries.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs +3 -1
- package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs +4 -6
- package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/log.cjs +1 -1
- package/dist/cjs/loadDictionaries/log.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs +19 -12
- package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/remoteDictionaryClient.cjs +18 -0
- package/dist/cjs/loadDictionaries/remoteDictionaryClient.cjs.map +1 -0
- package/dist/cjs/prepareIntlayerServer.cjs +132 -0
- package/dist/cjs/prepareIntlayerServer.cjs.map +1 -0
- package/dist/cjs/scan/analyzeBundleContent.cjs +26 -12
- package/dist/cjs/scan/analyzeBundleContent.cjs.map +1 -1
- package/dist/cjs/scan/calculateScore.cjs +4 -1
- package/dist/cjs/scan/calculateScore.cjs.map +1 -1
- package/dist/cjs/scan/checks.cjs +394 -221
- package/dist/cjs/scan/checks.cjs.map +1 -1
- package/dist/cjs/scan/detection/checkDetails.cjs +56 -0
- package/dist/cjs/scan/detection/checkDetails.cjs.map +1 -0
- package/dist/cjs/scan/detection/classifyInternalLinks.cjs +47 -0
- package/dist/cjs/scan/detection/classifyInternalLinks.cjs.map +1 -0
- package/dist/cjs/scan/detection/detectRoutingStrategy.cjs +143 -0
- package/dist/cjs/scan/detection/detectRoutingStrategy.cjs.map +1 -0
- package/dist/cjs/scan/detection/detectTechnologies.cjs +89 -0
- package/dist/cjs/scan/detection/detectTechnologies.cjs.map +1 -0
- package/dist/cjs/scan/detection/index.cjs +36 -0
- package/dist/cjs/scan/detection/localeCode.cjs +118 -0
- package/dist/cjs/scan/detection/localeCode.cjs.map +1 -0
- package/dist/cjs/scan/detection/localizedPages.cjs +48 -0
- package/dist/cjs/scan/detection/localizedPages.cjs.map +1 -0
- package/dist/cjs/scan/detection/technologySignatures.cjs +564 -0
- package/dist/cjs/scan/detection/technologySignatures.cjs.map +1 -0
- package/dist/cjs/scan/detection/url.cjs +53 -0
- package/dist/cjs/scan/detection/url.cjs.map +1 -0
- package/dist/cjs/scan/fetchText.cjs +56 -0
- package/dist/cjs/scan/fetchText.cjs.map +1 -0
- package/dist/cjs/scan/index.cjs +59 -3
- package/dist/cjs/scan/parseHtml.cjs +55 -10
- package/dist/cjs/scan/parseHtml.cjs.map +1 -1
- package/dist/cjs/scan/robots.cjs +65 -0
- package/dist/cjs/scan/robots.cjs.map +1 -0
- package/dist/cjs/scan/runScanChecks.cjs +89 -0
- package/dist/cjs/scan/runScanChecks.cjs.map +1 -0
- package/dist/cjs/scan/scanWebsite.cjs +58 -52
- package/dist/cjs/scan/scanWebsite.cjs.map +1 -1
- package/dist/cjs/scan/sitemap.cjs +109 -0
- package/dist/cjs/scan/sitemap.cjs.map +1 -0
- package/dist/cjs/utils/chunkJSON.cjs +24 -9
- package/dist/cjs/utils/chunkJSON.cjs.map +1 -1
- package/dist/cjs/utils/getChunk.cjs +2 -2
- package/dist/cjs/utils/getChunk.cjs.map +1 -1
- package/dist/cjs/utils/getProcessChainCommands.cjs +117 -0
- package/dist/cjs/utils/getProcessChainCommands.cjs.map +1 -0
- package/dist/cjs/utils/index.cjs +2 -0
- package/dist/cjs/utils/runParallel/index.cjs +1 -1
- package/dist/cjs/utils/runParallel/index.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/pidTree.cjs +23 -11
- package/dist/cjs/utils/runParallel/pidTree.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/ps.cjs +8 -3
- package/dist/cjs/utils/runParallel/ps.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/runTask.cjs +2 -1
- package/dist/cjs/utils/runParallel/runTask.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/wmic.cjs +8 -3
- package/dist/cjs/utils/runParallel/wmic.cjs.map +1 -1
- package/dist/cjs/utils/startContentWatcher.cjs +99 -0
- package/dist/cjs/utils/startContentWatcher.cjs.map +1 -0
- package/dist/cjs/watcher.cjs +2 -2
- package/dist/cjs/watcher.cjs.map +1 -1
- package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs +7 -4
- package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs.map +1 -1
- package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs +2 -2
- package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs.map +1 -1
- package/dist/cjs/writeJsonIfChanged.cjs.map +1 -1
- package/dist/esm/build.mjs +2 -1
- package/dist/esm/cli.mjs +2 -2
- package/dist/esm/docReview/alignBlocks.mjs +53 -36
- package/dist/esm/docReview/alignBlocks.mjs.map +1 -1
- package/dist/esm/docReview/rebuildDocument.mjs +3 -1
- package/dist/esm/docReview/rebuildDocument.mjs.map +1 -1
- package/dist/esm/docReview/segmentDocument.mjs +8 -5
- package/dist/esm/docReview/segmentDocument.mjs.map +1 -1
- package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs +4 -1
- package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs.map +1 -1
- package/dist/esm/init/index.mjs +1 -11
- package/dist/esm/init/index.mjs.map +1 -1
- package/dist/esm/init/utils/backendPackages.mjs +14 -0
- package/dist/esm/init/utils/backendPackages.mjs.map +1 -0
- package/dist/esm/init/utils/index.mjs +2 -2
- package/dist/esm/installMCP/installMCP.mjs +1 -1
- package/dist/esm/installMCP/installMCP.mjs.map +1 -1
- package/dist/esm/installSkills/index.mjs +2 -0
- package/dist/esm/installSkills/index.mjs.map +1 -1
- package/dist/esm/loadDictionaries/loadDictionaries.mjs +9 -8
- package/dist/esm/loadDictionaries/loadDictionaries.mjs.map +1 -1
- package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs +3 -1
- package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs.map +1 -1
- package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs +3 -5
- package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs.map +1 -1
- package/dist/esm/loadDictionaries/log.mjs +1 -1
- package/dist/esm/loadDictionaries/log.mjs.map +1 -1
- package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs +19 -12
- package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs.map +1 -1
- package/dist/esm/loadDictionaries/remoteDictionaryClient.mjs +16 -0
- package/dist/esm/loadDictionaries/remoteDictionaryClient.mjs.map +1 -0
- package/dist/esm/prepareIntlayerServer.mjs +128 -0
- package/dist/esm/prepareIntlayerServer.mjs.map +1 -0
- package/dist/esm/scan/analyzeBundleContent.mjs +26 -12
- package/dist/esm/scan/analyzeBundleContent.mjs.map +1 -1
- package/dist/esm/scan/calculateScore.mjs +4 -1
- package/dist/esm/scan/calculateScore.mjs.map +1 -1
- package/dist/esm/scan/checks.mjs +387 -220
- package/dist/esm/scan/checks.mjs.map +1 -1
- package/dist/esm/scan/detection/checkDetails.mjs +54 -0
- package/dist/esm/scan/detection/checkDetails.mjs.map +1 -0
- package/dist/esm/scan/detection/classifyInternalLinks.mjs +46 -0
- package/dist/esm/scan/detection/classifyInternalLinks.mjs.map +1 -0
- package/dist/esm/scan/detection/detectRoutingStrategy.mjs +141 -0
- package/dist/esm/scan/detection/detectRoutingStrategy.mjs.map +1 -0
- package/dist/esm/scan/detection/detectTechnologies.mjs +87 -0
- package/dist/esm/scan/detection/detectTechnologies.mjs.map +1 -0
- package/dist/esm/scan/detection/index.mjs +10 -0
- package/dist/esm/scan/detection/localeCode.mjs +108 -0
- package/dist/esm/scan/detection/localeCode.mjs.map +1 -0
- package/dist/esm/scan/detection/localizedPages.mjs +46 -0
- package/dist/esm/scan/detection/localizedPages.mjs.map +1 -0
- package/dist/esm/scan/detection/technologySignatures.mjs +563 -0
- package/dist/esm/scan/detection/technologySignatures.mjs.map +1 -0
- package/dist/esm/scan/detection/url.mjs +47 -0
- package/dist/esm/scan/detection/url.mjs.map +1 -0
- package/dist/esm/scan/fetchText.mjs +55 -0
- package/dist/esm/scan/fetchText.mjs.map +1 -0
- package/dist/esm/scan/index.mjs +15 -3
- package/dist/esm/scan/parseHtml.mjs +52 -11
- package/dist/esm/scan/parseHtml.mjs.map +1 -1
- package/dist/esm/scan/robots.mjs +63 -0
- package/dist/esm/scan/robots.mjs.map +1 -0
- package/dist/esm/scan/runScanChecks.mjs +88 -0
- package/dist/esm/scan/runScanChecks.mjs.map +1 -0
- package/dist/esm/scan/scanWebsite.mjs +59 -53
- package/dist/esm/scan/scanWebsite.mjs.map +1 -1
- package/dist/esm/scan/sitemap.mjs +105 -0
- package/dist/esm/scan/sitemap.mjs.map +1 -0
- package/dist/esm/utils/chunkJSON.mjs +24 -9
- package/dist/esm/utils/chunkJSON.mjs.map +1 -1
- package/dist/esm/utils/getChunk.mjs +2 -2
- package/dist/esm/utils/getChunk.mjs.map +1 -1
- package/dist/esm/utils/getProcessChainCommands.mjs +115 -0
- package/dist/esm/utils/getProcessChainCommands.mjs.map +1 -0
- package/dist/esm/utils/index.mjs +2 -1
- package/dist/esm/utils/runParallel/index.mjs +1 -1
- package/dist/esm/utils/runParallel/index.mjs.map +1 -1
- package/dist/esm/utils/runParallel/pidTree.mjs +23 -11
- package/dist/esm/utils/runParallel/pidTree.mjs.map +1 -1
- package/dist/esm/utils/runParallel/ps.mjs +8 -3
- package/dist/esm/utils/runParallel/ps.mjs.map +1 -1
- package/dist/esm/utils/runParallel/runTask.mjs +2 -1
- package/dist/esm/utils/runParallel/runTask.mjs.map +1 -1
- package/dist/esm/utils/runParallel/wmic.mjs +8 -3
- package/dist/esm/utils/runParallel/wmic.mjs.map +1 -1
- package/dist/esm/utils/startContentWatcher.mjs +98 -0
- package/dist/esm/utils/startContentWatcher.mjs.map +1 -0
- package/dist/esm/watcher.mjs +1 -1
- package/dist/esm/watcher.mjs.map +1 -1
- package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs +7 -4
- package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs.map +1 -1
- package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs +2 -2
- package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs.map +1 -1
- package/dist/esm/writeJsonIfChanged.mjs +1 -1
- package/dist/esm/writeJsonIfChanged.mjs.map +1 -1
- package/dist/types/build.d.ts +2 -1
- package/dist/types/cli.d.ts +2 -2
- package/dist/types/docReview/rebuildDocument.d.ts.map +1 -1
- package/dist/types/docReview/segmentDocument.d.ts.map +1 -1
- package/dist/types/init/frameworkSetup/nextAppRouter/transforms.d.ts.map +1 -1
- package/dist/types/init/index.d.ts +1 -1
- package/dist/types/init/index.d.ts.map +1 -1
- package/dist/types/init/utils/backendPackages.d.ts +5 -0
- package/dist/types/init/utils/backendPackages.d.ts.map +1 -0
- package/dist/types/init/utils/index.d.ts +2 -2
- package/dist/types/installSkills/index.d.ts +1 -0
- package/dist/types/installSkills/index.d.ts.map +1 -1
- package/dist/types/loadDictionaries/loadRemoteDictionaries.d.ts.map +1 -1
- package/dist/types/loadDictionaries/logTypeScriptErrors.d.ts.map +1 -1
- package/dist/types/loadDictionaries/remoteDictionaryClient.d.ts +14 -0
- package/dist/types/loadDictionaries/remoteDictionaryClient.d.ts.map +1 -0
- package/dist/types/prepareIntlayerServer.d.ts +67 -0
- package/dist/types/prepareIntlayerServer.d.ts.map +1 -0
- package/dist/types/scan/analyzeBundleContent.d.ts.map +1 -1
- package/dist/types/scan/calculateScore.d.ts +4 -1
- package/dist/types/scan/calculateScore.d.ts.map +1 -1
- package/dist/types/scan/checks.d.ts +114 -21
- package/dist/types/scan/checks.d.ts.map +1 -1
- package/dist/types/scan/detection/checkDetails.d.ts +29 -0
- package/dist/types/scan/detection/checkDetails.d.ts.map +1 -0
- package/dist/types/scan/detection/classifyInternalLinks.d.ts +44 -0
- package/dist/types/scan/detection/classifyInternalLinks.d.ts.map +1 -0
- package/dist/types/scan/detection/detectRoutingStrategy.d.ts +60 -0
- package/dist/types/scan/detection/detectRoutingStrategy.d.ts.map +1 -0
- package/dist/types/scan/detection/detectTechnologies.d.ts +60 -0
- package/dist/types/scan/detection/detectTechnologies.d.ts.map +1 -0
- package/dist/types/scan/detection/index.d.ts +9 -0
- package/dist/types/scan/detection/localeCode.d.ts +49 -0
- package/dist/types/scan/detection/localeCode.d.ts.map +1 -0
- package/dist/types/scan/detection/localizedPages.d.ts +40 -0
- package/dist/types/scan/detection/localizedPages.d.ts.map +1 -0
- package/dist/types/scan/detection/technologySignatures.d.ts +48 -0
- package/dist/types/scan/detection/technologySignatures.d.ts.map +1 -0
- package/dist/types/scan/detection/url.d.ts +27 -0
- package/dist/types/scan/detection/url.d.ts.map +1 -0
- package/dist/types/scan/fetchText.d.ts +30 -0
- package/dist/types/scan/fetchText.d.ts.map +1 -0
- package/dist/types/scan/index.d.ts +16 -4
- package/dist/types/scan/parseHtml.d.ts +17 -0
- package/dist/types/scan/parseHtml.d.ts.map +1 -1
- package/dist/types/scan/robots.d.ts +23 -0
- package/dist/types/scan/robots.d.ts.map +1 -0
- package/dist/types/scan/runScanChecks.d.ts +43 -0
- package/dist/types/scan/runScanChecks.d.ts.map +1 -0
- package/dist/types/scan/scanWebsite.d.ts.map +1 -1
- package/dist/types/scan/sitemap.d.ts +49 -0
- package/dist/types/scan/sitemap.d.ts.map +1 -0
- package/dist/types/scan/types.d.ts +15 -0
- package/dist/types/scan/types.d.ts.map +1 -1
- package/dist/types/utils/chunkJSON.d.ts.map +1 -1
- package/dist/types/utils/getProcessChainCommands.d.ts +35 -0
- package/dist/types/utils/getProcessChainCommands.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +2 -1
- package/dist/types/utils/runParallel/pidTree.d.ts.map +1 -1
- package/dist/types/utils/startContentWatcher.d.ts +40 -0
- package/dist/types/utils/startContentWatcher.d.ts.map +1 -0
- package/dist/types/watcher.d.ts.map +1 -1
- package/dist/types/writeContentDeclaration/writeMarkdownFile.d.ts.map +1 -1
- package/package.json +15 -7
- package/dist/cjs/init/utils/devScript.cjs +0 -57
- package/dist/cjs/init/utils/devScript.cjs.map +0 -1
- package/dist/esm/init/utils/devScript.mjs +0 -55
- package/dist/esm/init/utils/devScript.mjs.map +0 -1
- package/dist/types/init/utils/devScript.d.ts +0 -48
- package/dist/types/init/utils/devScript.d.ts.map +0 -1
package/dist/cjs/watcher.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watcher.cjs","names":["getConfigurationAndFilePath","getAppLogger","colorize","ANSIColor","normalizePath","dirname","existsSync","readFile","transpileTSToCJS","basename","prepareIntlayer","formatPath","fsWatch","resolve","handleContentDeclarationFileMoved","parseContentDeclarationFileName","getFormatFromExtension","extname","writeContentDeclaration","handleAdditionalContentDeclarationFile","handleContentDeclarationFileChange","handleUnlinkedContentDeclarationFile","getConfiguration"],"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';\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 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 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.cjs","names":["getConfigurationAndFilePath","getAppLogger","colorize","ANSIColor","normalizePath","dirname","existsSync","readFile","transpileTSToCJS","basename","prepareIntlayer","formatPath","fsWatch","resolve","handleContentDeclarationFileMoved","parseContentDeclarationFileName","getFormatFromExtension","extname","handleAdditionalContentDeclarationFile","handleContentDeclarationFileChange","handleUnlinkedContentDeclarationFile","getConfiguration"],"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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"}
|
|
@@ -14,12 +14,15 @@ const detectExportedComponentName = (fileText) => {
|
|
|
14
14
|
const cjsDefaultVarRegex = /exports\.default\s*=\s*(\w+)/;
|
|
15
15
|
const namedExportRegex = /export\s+(?:const|function)\s+(\w+)/g;
|
|
16
16
|
const defaultEsmFnMatch = fileText.match(defaultEsmFnRegex);
|
|
17
|
-
if (defaultEsmFnMatch) return defaultEsmFnMatch[1];
|
|
17
|
+
if (defaultEsmFnMatch?.[1]) return defaultEsmFnMatch[1];
|
|
18
18
|
const defaultEsmVarMatch = fileText.match(defaultEsmVarRegex);
|
|
19
|
-
if (defaultEsmVarMatch) return defaultEsmVarMatch[1];
|
|
19
|
+
if (defaultEsmVarMatch?.[1]) return defaultEsmVarMatch[1];
|
|
20
20
|
const cjsDefaultMatch = fileText.match(cjsDefaultRegex) || fileText.match(cjsDefaultVarRegex);
|
|
21
|
-
if (cjsDefaultMatch) return cjsDefaultMatch[1];
|
|
22
|
-
for (const match of fileText.matchAll(namedExportRegex))
|
|
21
|
+
if (cjsDefaultMatch?.[1]) return cjsDefaultMatch[1];
|
|
22
|
+
for (const match of fileText.matchAll(namedExportRegex)) {
|
|
23
|
+
const name = match[1];
|
|
24
|
+
if (name && /^[A-Z]/.test(name)) return name;
|
|
25
|
+
}
|
|
23
26
|
return null;
|
|
24
27
|
};
|
|
25
28
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detectExportedComponentName.cjs","names":[],"sources":["../../../src/writeContentDeclaration/detectExportedComponentName.ts"],"sourcesContent":["/**\n * Attempt to detect an exported React component name in the file text.\n * Looks for patterns like:\n * - export const MyComponent = ...\n * - export function MyComponent(...)\n * - export default function MyComponent(...)\n */\nexport const detectExportedComponentName = (\n fileText: string\n): string | null => {\n // Added regexes for default ESM, default CJS, and named exports\n const defaultEsmFnRegex = /export\\s+default\\s+function\\s+(\\w+)/;\n const defaultEsmVarRegex = /export\\s+default\\s+(\\w+)/;\n const cjsDefaultRegex = /module\\.exports\\s*=\\s*(\\w+)/;\n const cjsDefaultVarRegex = /exports\\.default\\s*=\\s*(\\w+)/;\n const namedExportRegex = /export\\s+(?:const|function)\\s+(\\w+)/g;\n\n // Check for default ESM function or variable\n const defaultEsmFnMatch = fileText.match(defaultEsmFnRegex);\n if (defaultEsmFnMatch) {\n return defaultEsmFnMatch[1];\n }\n\n const defaultEsmVarMatch = fileText.match(defaultEsmVarRegex);\n if (defaultEsmVarMatch) {\n return defaultEsmVarMatch[1];\n }\n\n // Check for default CJS\n const cjsDefaultMatch =\n fileText.match(cjsDefaultRegex) || fileText.match(cjsDefaultVarRegex);\n\n if (cjsDefaultMatch) return cjsDefaultMatch[1];\n\n // Otherwise, look for capitalized named exports\n for (const match of fileText.matchAll(namedExportRegex)) {\n if (/^[A-Z]/.test(
|
|
1
|
+
{"version":3,"file":"detectExportedComponentName.cjs","names":[],"sources":["../../../src/writeContentDeclaration/detectExportedComponentName.ts"],"sourcesContent":["/**\n * Attempt to detect an exported React component name in the file text.\n * Looks for patterns like:\n * - export const MyComponent = ...\n * - export function MyComponent(...)\n * - export default function MyComponent(...)\n */\nexport const detectExportedComponentName = (\n fileText: string\n): string | null => {\n // Added regexes for default ESM, default CJS, and named exports\n const defaultEsmFnRegex = /export\\s+default\\s+function\\s+(\\w+)/;\n const defaultEsmVarRegex = /export\\s+default\\s+(\\w+)/;\n const cjsDefaultRegex = /module\\.exports\\s*=\\s*(\\w+)/;\n const cjsDefaultVarRegex = /exports\\.default\\s*=\\s*(\\w+)/;\n const namedExportRegex = /export\\s+(?:const|function)\\s+(\\w+)/g;\n\n // Check for default ESM function or variable\n const defaultEsmFnMatch = fileText.match(defaultEsmFnRegex);\n\n if (defaultEsmFnMatch?.[1]) {\n return defaultEsmFnMatch[1];\n }\n\n const defaultEsmVarMatch = fileText.match(defaultEsmVarRegex);\n if (defaultEsmVarMatch?.[1]) {\n return defaultEsmVarMatch[1];\n }\n\n // Check for default CJS\n const cjsDefaultMatch =\n fileText.match(cjsDefaultRegex) || fileText.match(cjsDefaultVarRegex);\n\n if (cjsDefaultMatch?.[1]) return cjsDefaultMatch[1];\n\n // Otherwise, look for capitalized named exports\n for (const match of fileText.matchAll(namedExportRegex)) {\n const name = match[1];\n if (name && /^[A-Z]/.test(name)) return name;\n }\n\n // If we can’t find it, return null\n return null;\n};\n"],"mappings":";;;;;;;;;AAOA,MAAa,+BACX,aACkB;CAElB,MAAM,oBAAoB;CAC1B,MAAM,qBAAqB;CAC3B,MAAM,kBAAkB;CACxB,MAAM,qBAAqB;CAC3B,MAAM,mBAAmB;CAGzB,MAAM,oBAAoB,SAAS,MAAM,iBAAiB;CAE1D,IAAI,oBAAoB,IACtB,OAAO,kBAAkB;CAG3B,MAAM,qBAAqB,SAAS,MAAM,kBAAkB;CAC5D,IAAI,qBAAqB,IACvB,OAAO,mBAAmB;CAI5B,MAAM,kBACJ,SAAS,MAAM,eAAe,KAAK,SAAS,MAAM,kBAAkB;CAEtE,IAAI,kBAAkB,IAAI,OAAO,gBAAgB;CAGjD,KAAK,MAAM,SAAS,SAAS,SAAS,gBAAgB,GAAG;EACvD,MAAM,OAAO,MAAM;EACnB,IAAI,QAAQ,SAAS,KAAK,IAAI,GAAG,OAAO;CAC1C;CAGA,OAAO;AACT"}
|
|
@@ -14,9 +14,9 @@ const stringifyYamlFrontmatter = (fields) => Object.entries(fields).filter(([, v
|
|
|
14
14
|
const getMarkdownBody = (content) => {
|
|
15
15
|
const lines = content.split(/\r?\n/);
|
|
16
16
|
const firstNonEmptyIndex = lines.findIndex((line) => line.trim() !== "");
|
|
17
|
-
if (firstNonEmptyIndex === -1 || lines[firstNonEmptyIndex]
|
|
17
|
+
if (firstNonEmptyIndex === -1 || lines[firstNonEmptyIndex]?.trim() !== "---") return content;
|
|
18
18
|
let endIndex = -1;
|
|
19
|
-
for (let i = firstNonEmptyIndex + 1; i < lines.length; i++) if (lines[i]
|
|
19
|
+
for (let i = firstNonEmptyIndex + 1; i < lines.length; i++) if (lines[i]?.trim() === "---") {
|
|
20
20
|
endIndex = i;
|
|
21
21
|
break;
|
|
22
22
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeMarkdownFile.cjs","names":["MARKDOWN","dirname","mkdir","basename","join","writeFile","rename","rm","detectFormatCommand"],"sources":["../../../src/writeContentDeclaration/writeMarkdownFile.ts"],"sourcesContent":["import { execSync } from 'node:child_process';\nimport { mkdir, rename, rm, writeFile } from 'node:fs/promises';\nimport { basename, dirname, join } from 'node:path';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport { MARKDOWN } from '@intlayer/types/nodeType';\nimport { detectFormatCommand } from '../detectFormatCommand';\n\nconst stringifyYamlFrontmatter = (fields: Record<string, any>): string =>\n Object.entries(fields)\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([key, value]) => {\n if (Array.isArray(value)) {\n return `${key}:\\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 `${key}: ${JSON.stringify(value)}`;\n }\n return `${key}: ${value}`;\n })\n .join('\\n');\n\n// Strips YAML frontmatter from a markdown string, returning only the body\nconst getMarkdownBody = (content: string): string => {\n const lines = content.split(/\\r?\\n/);\n const firstNonEmptyIndex = lines.findIndex((line) => line.trim() !== '');\n\n if (firstNonEmptyIndex === -1
|
|
1
|
+
{"version":3,"file":"writeMarkdownFile.cjs","names":["MARKDOWN","dirname","mkdir","basename","join","writeFile","rename","rm","detectFormatCommand"],"sources":["../../../src/writeContentDeclaration/writeMarkdownFile.ts"],"sourcesContent":["import { execSync } from 'node:child_process';\nimport { mkdir, rename, rm, writeFile } from 'node:fs/promises';\nimport { basename, dirname, join } from 'node:path';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport { MARKDOWN } from '@intlayer/types/nodeType';\nimport { detectFormatCommand } from '../detectFormatCommand';\n\nconst stringifyYamlFrontmatter = (fields: Record<string, any>): string =>\n Object.entries(fields)\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([key, value]) => {\n if (Array.isArray(value)) {\n return `${key}:\\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 `${key}: ${JSON.stringify(value)}`;\n }\n return `${key}: ${value}`;\n })\n .join('\\n');\n\n// Strips YAML frontmatter from a markdown string, returning only the body\nconst getMarkdownBody = (content: string): string => {\n const lines = content.split(/\\r?\\n/);\n const firstNonEmptyIndex = lines.findIndex((line) => line.trim() !== '');\n\n if (\n firstNonEmptyIndex === -1 ||\n lines[firstNonEmptyIndex]?.trim() !== '---'\n ) {\n return content;\n }\n\n let endIndex = -1;\n for (let i = firstNonEmptyIndex + 1; i < lines.length; i++) {\n if (lines[i]?.trim() === '---') {\n endIndex = i;\n break;\n }\n }\n\n if (endIndex === -1) return content;\n\n return lines\n .slice(endIndex + 1)\n .join('\\n')\n .trimStart();\n};\n\n// Fields that are auto-generated / internal and must not appear in frontmatter\nconst EXCLUDED_FRONTMATTER_KEYS = new Set<string>([\n 'content',\n '$schema',\n 'filePath',\n 'localId',\n 'localIds',\n 'projectIds',\n]);\n\nexport const writeMarkdownFile = async (\n absoluteFilePath: string,\n dictionary: Dictionary,\n configuration: IntlayerConfig\n): Promise<void> => {\n const content = dictionary.content as any;\n const markdownRaw =\n typeof content === 'object' && content?.nodeType === MARKDOWN\n ? (content[MARKDOWN] ?? '')\n : '';\n // content.markdown now stores the full file (frontmatter + body); extract only the body\n const markdownBody = getMarkdownBody(markdownRaw);\n\n const frontmatterFields = Object.fromEntries(\n Object.entries(dictionary).filter(\n ([k, v]) => !EXCLUDED_FRONTMATTER_KEYS.has(k) && v !== undefined\n )\n );\n\n const frontmatterStr = stringifyYamlFrontmatter(frontmatterFields);\n const fileContent = `---\\n${frontmatterStr}\\n---\\n\\n${markdownBody}`;\n\n const dir = dirname(absoluteFilePath);\n await mkdir(dir, { recursive: true });\n\n const tempDir = configuration.system?.tempDir;\n if (tempDir) await mkdir(tempDir, { recursive: true });\n\n const tempFileName = `${basename(absoluteFilePath)}.${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`;\n const tempPath = tempDir\n ? join(tempDir, tempFileName)\n : `${absoluteFilePath}.${tempFileName}`;\n\n try {\n await writeFile(tempPath, fileContent, 'utf-8');\n await rename(tempPath, absoluteFilePath);\n } catch (error) {\n try {\n await rm(tempPath, { force: true });\n } catch {\n // ignore\n }\n throw error;\n }\n\n const formatCommand = detectFormatCommand(configuration);\n if (formatCommand) {\n try {\n execSync(formatCommand.replace('{{file}}', absoluteFilePath), {\n stdio: 'inherit',\n cwd: configuration.system.baseDir,\n });\n } catch (error) {\n console.error(error);\n }\n }\n};\n"],"mappings":";;;;;;;;AAQA,MAAM,4BAA4B,WAChC,OAAO,QAAQ,MAAM,CAAC,CACnB,QAAQ,GAAG,WAAW,UAAU,UAAa,UAAU,IAAI,CAAC,CAC5D,KAAK,CAAC,KAAK,WAAW;CACrB,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,GAAG,IAAI,KAAK,MAAM,KAAK,SAAS,OAAO,KAAK,UAAU,IAAI,GAAG,CAAC,CAAC,KAAK,IAAI;CAEjF,IACE,OAAO,UAAU,aAChB,MAAM,SAAS,GAAG,KAAK,MAAM,SAAS,IAAI,KAAK,MAAM,SAAS,GAAG,IAElE,OAAO,GAAG,IAAI,IAAI,KAAK,UAAU,KAAK;CAExC,OAAO,GAAG,IAAI,IAAI;AACpB,CAAC,CAAC,CACD,KAAK,IAAI;AAGd,MAAM,mBAAmB,YAA4B;CACnD,MAAM,QAAQ,QAAQ,MAAM,OAAO;CACnC,MAAM,qBAAqB,MAAM,WAAW,SAAS,KAAK,KAAK,MAAM,EAAE;CAEvE,IACE,uBAAuB,MACvB,MAAM,mBAAmB,EAAE,KAAK,MAAM,OAEtC,OAAO;CAGT,IAAI,WAAW;CACf,KAAK,IAAI,IAAI,qBAAqB,GAAG,IAAI,MAAM,QAAQ,KACrD,IAAI,MAAM,EAAE,EAAE,KAAK,MAAM,OAAO;EAC9B,WAAW;EACX;CACF;CAGF,IAAI,aAAa,IAAI,OAAO;CAE5B,OAAO,MACJ,MAAM,WAAW,CAAC,CAAC,CACnB,KAAK,IAAI,CAAC,CACV,UAAU;AACf;AAGA,MAAM,4CAA4B,IAAI,IAAY;CAChD;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,MAAa,oBAAoB,OAC/B,kBACA,YACA,kBACkB;CAClB,MAAM,UAAU,WAAW;CAC3B,MAAM,cACJ,OAAO,YAAY,YAAY,SAAS,aAAaA,oCAChD,QAAQA,sCAAa,KACtB;CAEN,MAAM,eAAe,gBAAgB,WAAW;CAEhD,MAAM,oBAAoB,OAAO,YAC/B,OAAO,QAAQ,UAAU,CAAC,CAAC,QACxB,CAAC,GAAG,OAAO,CAAC,0BAA0B,IAAI,CAAC,KAAK,MAAM,MACzD,CACF;CAGA,MAAM,cAAc,QADG,yBAAyB,iBACP,EAAE,WAAW;CAEtD,MAAM,UAAMC,mBAAQ,gBAAgB;CACpC,UAAMC,wBAAM,KAAK,EAAE,WAAW,KAAK,CAAC;CAEpC,MAAM,UAAU,cAAc,QAAQ;CACtC,IAAI,SAAS,UAAMA,wBAAM,SAAS,EAAE,WAAW,KAAK,CAAC;CAErD,MAAM,eAAe,OAAGC,oBAAS,gBAAgB,EAAE,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE;CACxG,MAAM,WAAW,cACbC,gBAAK,SAAS,YAAY,IAC1B,GAAG,iBAAiB,GAAG;CAE3B,IAAI;EACF,UAAMC,4BAAU,UAAU,aAAa,OAAO;EAC9C,UAAMC,yBAAO,UAAU,gBAAgB;CACzC,SAAS,OAAO;EACd,IAAI;GACF,UAAMC,qBAAG,UAAU,EAAE,OAAO,KAAK,CAAC;EACpC,QAAQ,CAER;EACA,MAAM;CACR;CAEA,MAAM,gBAAgBC,gDAAoB,aAAa;CACvD,IAAI,eACF,IAAI;EACF,iCAAS,cAAc,QAAQ,YAAY,gBAAgB,GAAG;GAC5D,OAAO;GACP,KAAK,cAAc,OAAO;EAC5B,CAAC;CACH,SAAS,OAAO;EACd,QAAQ,MAAM,KAAK;CACrB;AAEJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeJsonIfChanged.cjs","names":["writeFileIfChanged"],"sources":["../../src/writeJsonIfChanged.ts"],"sourcesContent":["import { writeFileIfChanged } from './writeFileIfChanged';\n\nexport const writeJsonIfChanged = async <T>(\n path: string,\n data: T,\n { pretty = process.env.NODE_ENV === 'development' } = {}\n): Promise<boolean> => {\n const space = pretty ? 2 : undefined;\n\n let json = '{}';\n\n try {\n json = JSON.stringify(data, null, space);\n } catch (error) {\n console.error(`Error while parsing data to JSON for ${path}:`, error);\n }\n\n return await writeFileIfChanged(path, json);\n};\n"],"mappings":";;;;AAEA,MAAa,qBAAqB,OAChC,MACA,MACA,EAAE,
|
|
1
|
+
{"version":3,"file":"writeJsonIfChanged.cjs","names":["writeFileIfChanged"],"sources":["../../src/writeJsonIfChanged.ts"],"sourcesContent":["import { writeFileIfChanged } from './writeFileIfChanged';\n\nexport const writeJsonIfChanged = async <T>(\n path: string,\n data: T,\n { pretty = process.env.NODE_ENV === 'development' } = {}\n): Promise<boolean> => {\n const space = pretty ? 2 : undefined;\n\n let json = '{}';\n\n try {\n json = JSON.stringify(data, null, space);\n } catch (error) {\n console.error(`Error while parsing data to JSON for ${path}:`, error);\n }\n\n return await writeFileIfChanged(path, json);\n};\n"],"mappings":";;;;AAEA,MAAa,qBAAqB,OAChC,MACA,MACA,EAAE,kCAAkC,kBAAkB,CAAC,MAClC;CACrB,MAAM,QAAQ,SAAS,IAAI;CAE3B,IAAI,OAAO;CAEX,IAAI;EACF,OAAO,KAAK,UAAU,MAAM,MAAM,KAAK;CACzC,SAAS,OAAO;EACd,QAAQ,MAAM,wCAAwC,KAAK,IAAI,KAAK;CACtE;CAEA,OAAO,MAAMA,8CAAmB,MAAM,IAAI;AAC5C"}
|
package/dist/esm/build.mjs
CHANGED
|
@@ -18,9 +18,10 @@ import { loadDictionaries } from "./loadDictionaries/loadDictionaries.mjs";
|
|
|
18
18
|
import { loadLocalDictionaries } from "./loadDictionaries/loadLocalDictionaries.mjs";
|
|
19
19
|
import { isCachedConfigurationUpToDate, writeConfiguration } from "./writeConfiguration/index.mjs";
|
|
20
20
|
import { prepareIntlayer } from "./prepareIntlayer.mjs";
|
|
21
|
+
import { INTLAYER_WATCH_ENV_VAR, getIsDevWatcherCommand, getIsServerDevelopment, prepareIntlayerServer } from "./prepareIntlayerServer.mjs";
|
|
21
22
|
import { detectExportedComponentName } from "./writeContentDeclaration/detectExportedComponentName.mjs";
|
|
22
23
|
import { transformJSFile } from "./writeContentDeclaration/transformJSFile.mjs";
|
|
23
24
|
import { writeJSFile } from "./writeContentDeclaration/writeJSFile.mjs";
|
|
24
25
|
import { writeContentDeclaration } from "./writeContentDeclaration/writeContentDeclaration.mjs";
|
|
25
26
|
|
|
26
|
-
export { buildDictionary, cleanOutputDir, createDictionaryEntryPoint, createModuleAugmentation, createTypes, detectExportedComponentName, ensureIntlayerBundle, formatDictionaries, formatDictionariesOutput, formatDictionary, formatDictionaryOutput, formatDistantDictionaries, formatDomains, formatLocalDictionaries, formatRewriteRules, generateDictionaryListContent, generateTypeScriptType, getBuiltDictionariesPath, getBuiltDynamicDictionariesPath, getBuiltFetchDictionariesPath, getBuiltRemoteDictionariesPath, getBuiltUnmergedDictionariesPath, getTypeName, isCachedConfigurationUpToDate, loadContentDeclaration, loadContentDeclarations, loadDictionaries, loadLocalDictionaries, loadRemoteDictionaries, prepareIntlayer, processContentDeclaration, readDictionariesFromDisk, transformJSFile, writeConfiguration, writeContentDeclaration, writeJSFile };
|
|
27
|
+
export { INTLAYER_WATCH_ENV_VAR, buildDictionary, cleanOutputDir, createDictionaryEntryPoint, createModuleAugmentation, createTypes, detectExportedComponentName, ensureIntlayerBundle, formatDictionaries, formatDictionariesOutput, formatDictionary, formatDictionaryOutput, formatDistantDictionaries, formatDomains, formatLocalDictionaries, formatRewriteRules, generateDictionaryListContent, generateTypeScriptType, getBuiltDictionariesPath, getBuiltDynamicDictionariesPath, getBuiltFetchDictionariesPath, getBuiltRemoteDictionariesPath, getBuiltUnmergedDictionariesPath, getIsDevWatcherCommand, getIsServerDevelopment, getTypeName, isCachedConfigurationUpToDate, loadContentDeclaration, loadContentDeclarations, loadDictionaries, loadLocalDictionaries, loadRemoteDictionaries, prepareIntlayer, prepareIntlayerServer, processContentDeclaration, readDictionariesFromDisk, transformJSFile, writeConfiguration, writeContentDeclaration, writeJSFile };
|
package/dist/esm/cli.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { writeContentDeclaration } from "./writeContentDeclaration/writeContentD
|
|
|
10
10
|
import { getGitRootDir, listProjects } from "./listProjects.mjs";
|
|
11
11
|
import { enableEditorInConfig, setupCmsCredentials, writeCmsCredentialsToEnv } from "./init/cms.mjs";
|
|
12
12
|
import { DocumentationRouter, getDocumentationUrl } from "./init/documentationRouter.mjs";
|
|
13
|
-
import { COMPAT_I18N_LIBRARIES, detectCompatI18nLibraries, detectPackageManager, hasLintTooling, installPackages } from "./init/utils/packageManager.mjs";
|
|
13
|
+
import { COMPAT_I18N_LIBRARIES, detectCompatI18nLibraries, detectPackageManager, findLockFileDir, hasLintTooling, installPackages } from "./init/utils/packageManager.mjs";
|
|
14
14
|
import { NEXT_INTLAYER_BABEL_CONFIG_CONTENT } from "./init/utils/nextCompilerBabel.mjs";
|
|
15
15
|
import { hasUrlRoutingFramework } from "./init/utils/urlRoutingFramework.mjs";
|
|
16
16
|
import { upgradeIntlayerPackages } from "./init/upgradeIntlayerPackages.mjs";
|
|
@@ -20,4 +20,4 @@ import { PLATFORMS, PLATFORMS_METADATA, SKILLS, SKILLS_METADATA, getInitialSkill
|
|
|
20
20
|
import { installMCP } from "./installMCP/installMCP.mjs";
|
|
21
21
|
import { listGitFiles, listGitLines } from "./listGitFiles.mjs";
|
|
22
22
|
|
|
23
|
-
export { COMPAT_I18N_LIBRARIES, DocumentationRouter, NEXT_INTLAYER_BABEL_CONFIG_CONTENT, PLATFORMS, PLATFORMS_METADATA, SKILLS, SKILLS_METADATA, detectCompatI18nLibraries, detectExportedComponentName, detectFormatCommand, detectPackageManager, enableEditorInConfig, getContentDeclarationFileTemplate, getDefaultApplicationURL, getDocumentationUrl, getGitRootDir, getInitialSkills, hasLintTooling, hasUrlRoutingFramework, initIntlayer, installLSP, installMCP, installPackages, installSkills, listDictionaries, listDictionariesWithStats, listGitFiles, listGitLines, listProjects, logConfigDetails, logInitSuccessMessage, prepareIntlayer, setupCmsCredentials, transformJSFile, upgradeIntlayerPackages, writeCmsCredentialsToEnv, writeContentDeclaration, writeJSFile };
|
|
23
|
+
export { COMPAT_I18N_LIBRARIES, DocumentationRouter, NEXT_INTLAYER_BABEL_CONFIG_CONTENT, PLATFORMS, PLATFORMS_METADATA, SKILLS, SKILLS_METADATA, detectCompatI18nLibraries, detectExportedComponentName, detectFormatCommand, detectPackageManager, enableEditorInConfig, findLockFileDir, getContentDeclarationFileTemplate, getDefaultApplicationURL, getDocumentationUrl, getGitRootDir, getInitialSkills, hasLintTooling, hasUrlRoutingFramework, initIntlayer, installLSP, installMCP, installPackages, installSkills, listDictionaries, listDictionariesWithStats, listGitFiles, listGitLines, listProjects, logConfigDetails, logInitSuccessMessage, prepareIntlayer, setupCmsCredentials, transformJSFile, upgradeIntlayerPackages, writeCmsCredentialsToEnv, writeContentDeclaration, writeJSFile };
|
|
@@ -62,6 +62,7 @@ const alignBaseAndTargetBlocks = (baseBlocks, targetBlocks) => {
|
|
|
62
62
|
const computeMatchScore = (baseIndex, targetIndex) => {
|
|
63
63
|
const baseBlock = baseBlocks[baseIndex];
|
|
64
64
|
const targetBlock = targetBlocks[targetIndex];
|
|
65
|
+
if (!baseBlock || !targetBlock) return STRUCTURAL_MISMATCH_PENALTY;
|
|
65
66
|
const hasComparableHeadingDepths = baseBlock.headingDepth !== null && targetBlock.headingDepth !== null;
|
|
66
67
|
if (hasComparableHeadingDepths && baseBlock.headingDepth !== targetBlock.headingDepth) return STRUCTURAL_MISMATCH_PENALTY;
|
|
67
68
|
const baseBodyLength = measureBodyLength(baseBlock);
|
|
@@ -75,49 +76,65 @@ const alignBaseAndTargetBlocks = (baseBlocks, targetBlocks) => {
|
|
|
75
76
|
return headingDepthBonus + typeBonus + lengthBonus + anchorSimilarity * 8;
|
|
76
77
|
};
|
|
77
78
|
for (let i = 1; i <= baseLength; i += 1) {
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
const row = scoreMatrix[i];
|
|
80
|
+
const prevRow = scoreMatrix[i - 1];
|
|
81
|
+
const traceRow = traceMatrix[i];
|
|
82
|
+
if (row && prevRow) row[0] = (prevRow[0] ?? 0) + GAP_PENALTY;
|
|
83
|
+
if (traceRow) traceRow[0] = "up";
|
|
80
84
|
}
|
|
85
|
+
const firstRow = scoreMatrix[0];
|
|
86
|
+
const firstTraceRow = traceMatrix[0];
|
|
81
87
|
for (let j = 1; j <= targetLength; j += 1) {
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
if (firstRow) firstRow[j] = (firstRow[j - 1] ?? 0) + GAP_PENALTY;
|
|
89
|
+
if (firstTraceRow) firstTraceRow[j] = "left";
|
|
84
90
|
}
|
|
85
|
-
for (let i = 1; i <= baseLength; i += 1)
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
for (let i = 1; i <= baseLength; i += 1) {
|
|
92
|
+
const currentRow = scoreMatrix[i];
|
|
93
|
+
const prevRow = scoreMatrix[i - 1];
|
|
94
|
+
const currentTraceRow = traceMatrix[i];
|
|
95
|
+
if (!currentRow || !prevRow || !currentTraceRow) continue;
|
|
96
|
+
for (let j = 1; j <= targetLength; j += 1) {
|
|
97
|
+
const match = (prevRow[j - 1] ?? 0) + computeMatchScore(i - 1, j - 1);
|
|
98
|
+
const deleteGap = (prevRow[j] ?? 0) + GAP_PENALTY;
|
|
99
|
+
const insertGap = (currentRow[j - 1] ?? 0) + GAP_PENALTY;
|
|
100
|
+
const best = Math.max(match, deleteGap, insertGap);
|
|
101
|
+
currentRow[j] = best;
|
|
102
|
+
currentTraceRow[j] = best === match ? "diagonal" : best === deleteGap ? "up" : "left";
|
|
103
|
+
}
|
|
92
104
|
}
|
|
93
105
|
const result = [];
|
|
94
106
|
let i = baseLength;
|
|
95
107
|
let j = targetLength;
|
|
96
|
-
while (i > 0 || j > 0)
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
baseIndex
|
|
102
|
-
targetIndex
|
|
103
|
-
similarityScore
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
108
|
+
while (i > 0 || j > 0) {
|
|
109
|
+
const direction = traceMatrix[i]?.[j];
|
|
110
|
+
if (i > 0 && j > 0 && direction === "diagonal") {
|
|
111
|
+
const baseIndex = i - 1;
|
|
112
|
+
const targetIndex = j - 1;
|
|
113
|
+
const baseAnchor = baseBlocks[baseIndex]?.anchorText ?? "";
|
|
114
|
+
const targetAnchor = targetBlocks[targetIndex]?.anchorText ?? "";
|
|
115
|
+
const similarityScore = computeJaccardSimilarity(baseAnchor, targetAnchor, 3);
|
|
116
|
+
result.unshift({
|
|
117
|
+
baseIndex,
|
|
118
|
+
targetIndex,
|
|
119
|
+
similarityScore
|
|
120
|
+
});
|
|
121
|
+
i -= 1;
|
|
122
|
+
j -= 1;
|
|
123
|
+
} else if (i > 0 && (j === 0 || direction === "up")) {
|
|
124
|
+
result.unshift({
|
|
125
|
+
baseIndex: i - 1,
|
|
126
|
+
targetIndex: null,
|
|
127
|
+
similarityScore: 0
|
|
128
|
+
});
|
|
129
|
+
i -= 1;
|
|
130
|
+
} else if (j > 0 && (i === 0 || direction === "left")) {
|
|
131
|
+
result.unshift({
|
|
132
|
+
baseIndex: -1,
|
|
133
|
+
targetIndex: j - 1,
|
|
134
|
+
similarityScore: 0
|
|
135
|
+
});
|
|
136
|
+
j -= 1;
|
|
137
|
+
}
|
|
121
138
|
}
|
|
122
139
|
return result;
|
|
123
140
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"alignBlocks.mjs","names":[],"sources":["../../../src/docReview/alignBlocks.ts"],"sourcesContent":["import { computeJaccardSimilarity } from './computeSimilarity';\nimport type { AlignmentPair, FingerprintedBlock } from './types';\n\n/** Cost of leaving a block unaligned (an insertion or a deletion). */\nconst GAP_PENALTY = -2;\n\n/**\n * Score of a pair that structural evidence rules out.\n *\n * Strictly below the cost of two gaps (`2 * GAP_PENALTY`) so the aligner always\n * prefers reporting an insertion plus a deletion over such a pair. Every other\n * score is positive, which means that without an explicit veto the aligner would\n * rather pair two unrelated blocks than leave them unaligned — and a bogus pair\n * is planned as `reuse`, which keeps the stale translation verbatim *and*\n * inserts the freshly translated base block next to it, producing the duplicated\n * headings this penalty exists to prevent.\n */\nconst STRUCTURAL_MISMATCH_PENALTY = GAP_PENALTY * 2 - 1;\n\n/** Reward for two blocks opened by a heading of the very same depth. */\nconst HEADING_DEPTH_MATCH_BONUS = 3;\n\n/** Minimum length ratio for the (small) \"comparable size\" reward. */\nconst COMPARABLE_LENGTH_RATIO = 0.75;\n\n/**\n * Body length from which a section is considered to carry content of its own.\n *\n * Kept above a single sentence so a translator's short lead-in paragraph is not\n * mistaken for a whole section body.\n */\nconst SUBSTANTIAL_BODY_LENGTH = 80;\n\n/**\n * Length of the body a block carries below its opening heading.\n *\n * A section that only holds its heading (because subsections carry all of its\n * content) is structurally different from one that holds a full body, and that\n * difference survives translation.\n *\n * @param block - The block to measure.\n * @returns The trimmed length of everything below the opening heading line.\n */\nconst measureBodyLength = (block: FingerprintedBlock): number => {\n if (block.headingDepth === null) return block.content.trim().length;\n\n const [, ...bodyLines] = block.content.split('\\n');\n\n return bodyLines.join('\\n').trim().length;\n};\n\n/**\n * Align the blocks of a base document with the blocks of its translation using a\n * Needleman–Wunsch global alignment over heading depth, anchor similarity and\n * block type.\n *\n * Because prose differs across languages, the score is weighted toward the\n * structural signals — heading depth first, then the anchor (digits and symbols)\n * — rather than the words themselves.\n *\n * @param baseBlocks - Blocks of the base (source) document.\n * @param targetBlocks - Blocks of the target (translated) document.\n * @returns The ordered list of alignment pairs, including insertions and deletions.\n */\nexport const alignBaseAndTargetBlocks = (\n baseBlocks: FingerprintedBlock[],\n targetBlocks: FingerprintedBlock[]\n): AlignmentPair[] => {\n const baseLength = baseBlocks.length;\n const targetLength = targetBlocks.length;\n\n const scoreMatrix: number[][] = Array.from({ length: baseLength + 1 }, () =>\n Array.from({ length: targetLength + 1 }, () => 0)\n );\n const traceMatrix: ('diagonal' | 'up' | 'left')[][] = Array.from(\n { length: baseLength + 1 },\n () => Array.from({ length: targetLength + 1 }, () => 'diagonal')\n );\n\n const computeMatchScore = (\n baseIndex: number,\n targetIndex: number\n ): number => {\n const baseBlock = baseBlocks[baseIndex]!;\n const targetBlock = targetBlocks[targetIndex]!;\n\n // Translating a document never changes its heading depths, so two headings\n // of different depths cannot be counterparts, however similar their content.\n const hasComparableHeadingDepths =\n baseBlock.headingDepth !== null && targetBlock.headingDepth !== null;\n\n if (\n hasComparableHeadingDepths &&\n baseBlock.headingDepth !== targetBlock.headingDepth\n ) {\n return STRUCTURAL_MISMATCH_PENALTY;\n }\n\n // A section reduced to its bare heading (its content living in subsections)\n // is not the translation of a section holding a full body. Pairing them\n // reuses that whole body while the base heading is translated again right\n // next to it — which is exactly how a duplicated heading appears.\n const baseBodyLength = measureBodyLength(baseBlock);\n const targetBodyLength = measureBodyLength(targetBlock);\n const isBodyPresenceMismatched =\n (baseBodyLength === 0 && targetBodyLength >= SUBSTANTIAL_BODY_LENGTH) ||\n (targetBodyLength === 0 && baseBodyLength >= SUBSTANTIAL_BODY_LENGTH);\n\n if (isBodyPresenceMismatched) return STRUCTURAL_MISMATCH_PENALTY;\n\n const lengthRatio =\n Math.min(baseBlock.content.length, targetBlock.content.length) /\n Math.max(baseBlock.content.length, targetBlock.content.length);\n\n const headingDepthBonus = hasComparableHeadingDepths\n ? HEADING_DEPTH_MATCH_BONUS\n : 0;\n const typeBonus = baseBlock.type === targetBlock.type ? 2 : 0;\n const anchorSimilarity = computeJaccardSimilarity(\n baseBlock.anchorText,\n targetBlock.anchorText,\n 3\n );\n const lengthBonus = lengthRatio > COMPARABLE_LENGTH_RATIO ? 1 : 0;\n\n // weighted toward the structural signals (heading depth, then anchor)\n return headingDepthBonus + typeBonus + lengthBonus + anchorSimilarity * 8;\n };\n\n // initialize first row and column\n for (let i = 1; i <= baseLength; i += 1) {\n scoreMatrix[i][0] = scoreMatrix[i - 1][0] + GAP_PENALTY;\n traceMatrix[i][0] = 'up';\n }\n for (let j = 1; j <= targetLength; j += 1) {\n scoreMatrix[0][j] = scoreMatrix[0][j - 1] + GAP_PENALTY;\n traceMatrix[0][j] = 'left';\n }\n\n // fill\n for (let i = 1; i <= baseLength; i += 1) {\n for (let j = 1; j <= targetLength; j += 1) {\n const match = scoreMatrix[i - 1][j - 1] + computeMatchScore(i - 1, j - 1);\n const deleteGap = scoreMatrix[i - 1][j] + GAP_PENALTY;\n const insertGap = scoreMatrix[i][j - 1] + GAP_PENALTY;\n\n const best = Math.max(match, deleteGap, insertGap);\n scoreMatrix[i][j] = best;\n traceMatrix[i][j] =\n best === match ? 'diagonal' : best === deleteGap ? 'up' : 'left';\n }\n }\n\n // traceback\n const result: AlignmentPair[] = [];\n let i = baseLength;\n let j = targetLength;\n while (i > 0 || j > 0) {\n if (i > 0 && j > 0 && traceMatrix[i][j] === 'diagonal') {\n const baseIndex = i - 1;\n const targetIndex = j - 1;\n const similarityScore = computeJaccardSimilarity(\n baseBlocks[baseIndex].anchorText,\n targetBlocks[targetIndex].anchorText,\n 3\n );\n result.unshift({ baseIndex, targetIndex, similarityScore });\n i -= 1;\n j -= 1;\n } else if (i > 0 && (j === 0 || traceMatrix[i][j] === 'up')) {\n result.unshift({\n baseIndex: i - 1,\n targetIndex: null,\n similarityScore: 0,\n });\n i -= 1;\n } else if (j > 0 && (i === 0 || traceMatrix[i][j] === 'left')) {\n // target block has no corresponding base block (deleted)\n result.unshift({\n baseIndex: -1,\n targetIndex: j - 1,\n similarityScore: 0,\n });\n j -= 1;\n }\n }\n return result;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"alignBlocks.mjs","names":[],"sources":["../../../src/docReview/alignBlocks.ts"],"sourcesContent":["import { computeJaccardSimilarity } from './computeSimilarity';\nimport type { AlignmentPair, FingerprintedBlock } from './types';\n\n/** Cost of leaving a block unaligned (an insertion or a deletion). */\nconst GAP_PENALTY = -2;\n\n/**\n * Score of a pair that structural evidence rules out.\n *\n * Strictly below the cost of two gaps (`2 * GAP_PENALTY`) so the aligner always\n * prefers reporting an insertion plus a deletion over such a pair. Every other\n * score is positive, which means that without an explicit veto the aligner would\n * rather pair two unrelated blocks than leave them unaligned — and a bogus pair\n * is planned as `reuse`, which keeps the stale translation verbatim *and*\n * inserts the freshly translated base block next to it, producing the duplicated\n * headings this penalty exists to prevent.\n */\nconst STRUCTURAL_MISMATCH_PENALTY = GAP_PENALTY * 2 - 1;\n\n/** Reward for two blocks opened by a heading of the very same depth. */\nconst HEADING_DEPTH_MATCH_BONUS = 3;\n\n/** Minimum length ratio for the (small) \"comparable size\" reward. */\nconst COMPARABLE_LENGTH_RATIO = 0.75;\n\n/**\n * Body length from which a section is considered to carry content of its own.\n *\n * Kept above a single sentence so a translator's short lead-in paragraph is not\n * mistaken for a whole section body.\n */\nconst SUBSTANTIAL_BODY_LENGTH = 80;\n\n/**\n * Length of the body a block carries below its opening heading.\n *\n * A section that only holds its heading (because subsections carry all of its\n * content) is structurally different from one that holds a full body, and that\n * difference survives translation.\n *\n * @param block - The block to measure.\n * @returns The trimmed length of everything below the opening heading line.\n */\nconst measureBodyLength = (block: FingerprintedBlock): number => {\n if (block.headingDepth === null) return block.content.trim().length;\n\n const [, ...bodyLines] = block.content.split('\\n');\n\n return bodyLines.join('\\n').trim().length;\n};\n\n/**\n * Align the blocks of a base document with the blocks of its translation using a\n * Needleman–Wunsch global alignment over heading depth, anchor similarity and\n * block type.\n *\n * Because prose differs across languages, the score is weighted toward the\n * structural signals — heading depth first, then the anchor (digits and symbols)\n * — rather than the words themselves.\n *\n * @param baseBlocks - Blocks of the base (source) document.\n * @param targetBlocks - Blocks of the target (translated) document.\n * @returns The ordered list of alignment pairs, including insertions and deletions.\n */\nexport const alignBaseAndTargetBlocks = (\n baseBlocks: FingerprintedBlock[],\n targetBlocks: FingerprintedBlock[]\n): AlignmentPair[] => {\n const baseLength = baseBlocks.length;\n const targetLength = targetBlocks.length;\n\n const scoreMatrix: number[][] = Array.from({ length: baseLength + 1 }, () =>\n Array.from({ length: targetLength + 1 }, () => 0)\n );\n const traceMatrix: ('diagonal' | 'up' | 'left')[][] = Array.from(\n { length: baseLength + 1 },\n () => Array.from({ length: targetLength + 1 }, () => 'diagonal')\n );\n\n const computeMatchScore = (\n baseIndex: number,\n targetIndex: number\n ): number => {\n const baseBlock = baseBlocks[baseIndex];\n const targetBlock = targetBlocks[targetIndex];\n\n if (!baseBlock || !targetBlock) return STRUCTURAL_MISMATCH_PENALTY;\n\n // Translating a document never changes its heading depths, so two headings\n // of different depths cannot be counterparts, however similar their content.\n const hasComparableHeadingDepths =\n baseBlock.headingDepth !== null && targetBlock.headingDepth !== null;\n\n if (\n hasComparableHeadingDepths &&\n baseBlock.headingDepth !== targetBlock.headingDepth\n ) {\n return STRUCTURAL_MISMATCH_PENALTY;\n }\n\n // A section reduced to its bare heading (its content living in subsections)\n // is not the translation of a section holding a full body. Pairing them\n // reuses that whole body while the base heading is translated again right\n // next to it — which is exactly how a duplicated heading appears.\n const baseBodyLength = measureBodyLength(baseBlock);\n const targetBodyLength = measureBodyLength(targetBlock);\n const isBodyPresenceMismatched =\n (baseBodyLength === 0 && targetBodyLength >= SUBSTANTIAL_BODY_LENGTH) ||\n (targetBodyLength === 0 && baseBodyLength >= SUBSTANTIAL_BODY_LENGTH);\n\n if (isBodyPresenceMismatched) return STRUCTURAL_MISMATCH_PENALTY;\n\n const lengthRatio =\n Math.min(baseBlock.content.length, targetBlock.content.length) /\n Math.max(baseBlock.content.length, targetBlock.content.length);\n\n const headingDepthBonus = hasComparableHeadingDepths\n ? HEADING_DEPTH_MATCH_BONUS\n : 0;\n const typeBonus = baseBlock.type === targetBlock.type ? 2 : 0;\n const anchorSimilarity = computeJaccardSimilarity(\n baseBlock.anchorText,\n targetBlock.anchorText,\n 3\n );\n const lengthBonus = lengthRatio > COMPARABLE_LENGTH_RATIO ? 1 : 0;\n\n // weighted toward the structural signals (heading depth, then anchor)\n return headingDepthBonus + typeBonus + lengthBonus + anchorSimilarity * 8;\n };\n\n // initialize first row and column\n for (let i = 1; i <= baseLength; i += 1) {\n const row = scoreMatrix[i];\n const prevRow = scoreMatrix[i - 1];\n const traceRow = traceMatrix[i];\n\n if (row && prevRow) {\n row[0] = (prevRow[0] ?? 0) + GAP_PENALTY;\n }\n if (traceRow) {\n traceRow[0] = 'up';\n }\n }\n const firstRow = scoreMatrix[0];\n const firstTraceRow = traceMatrix[0];\n\n for (let j = 1; j <= targetLength; j += 1) {\n if (firstRow) {\n firstRow[j] = (firstRow[j - 1] ?? 0) + GAP_PENALTY;\n }\n if (firstTraceRow) {\n firstTraceRow[j] = 'left';\n }\n }\n\n // fill\n for (let i = 1; i <= baseLength; i += 1) {\n const currentRow = scoreMatrix[i];\n const prevRow = scoreMatrix[i - 1];\n const currentTraceRow = traceMatrix[i];\n\n if (!currentRow || !prevRow || !currentTraceRow) continue;\n\n for (let j = 1; j <= targetLength; j += 1) {\n const match = (prevRow[j - 1] ?? 0) + computeMatchScore(i - 1, j - 1);\n const deleteGap = (prevRow[j] ?? 0) + GAP_PENALTY;\n const insertGap = (currentRow[j - 1] ?? 0) + GAP_PENALTY;\n\n const best = Math.max(match, deleteGap, insertGap);\n currentRow[j] = best;\n currentTraceRow[j] =\n best === match ? 'diagonal' : best === deleteGap ? 'up' : 'left';\n }\n }\n\n // traceback\n const result: AlignmentPair[] = [];\n let i = baseLength;\n let j = targetLength;\n while (i > 0 || j > 0) {\n const direction = traceMatrix[i]?.[j];\n if (i > 0 && j > 0 && direction === 'diagonal') {\n const baseIndex = i - 1;\n const targetIndex = j - 1;\n const baseAnchor = baseBlocks[baseIndex]?.anchorText ?? '';\n const targetAnchor = targetBlocks[targetIndex]?.anchorText ?? '';\n const similarityScore = computeJaccardSimilarity(\n baseAnchor,\n targetAnchor,\n 3\n );\n result.unshift({ baseIndex, targetIndex, similarityScore });\n i -= 1;\n j -= 1;\n } else if (i > 0 && (j === 0 || direction === 'up')) {\n result.unshift({\n baseIndex: i - 1,\n targetIndex: null,\n similarityScore: 0,\n });\n i -= 1;\n } else if (j > 0 && (i === 0 || direction === 'left')) {\n // target block has no corresponding base block (deleted)\n result.unshift({\n baseIndex: -1,\n targetIndex: j - 1,\n similarityScore: 0,\n });\n j -= 1;\n }\n }\n return result;\n};\n"],"mappings":"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"}
|
|
@@ -12,7 +12,8 @@ const identifySegmentsToReview = ({ baseBlocks, targetBlocks, plan }) => {
|
|
|
12
12
|
plan.actions.forEach((action, actionIndex) => {
|
|
13
13
|
if (action.kind === "review") {
|
|
14
14
|
const baseBlock = baseBlocks[action.baseIndex];
|
|
15
|
-
|
|
15
|
+
if (!baseBlock) return;
|
|
16
|
+
const targetBlockText = action.targetIndex !== null ? targetBlocks[action.targetIndex]?.content ?? null : null;
|
|
16
17
|
segmentsToReview.push({
|
|
17
18
|
baseBlock,
|
|
18
19
|
targetBlockText,
|
|
@@ -20,6 +21,7 @@ const identifySegmentsToReview = ({ baseBlocks, targetBlocks, plan }) => {
|
|
|
20
21
|
});
|
|
21
22
|
} else if (action.kind === "insert_new") {
|
|
22
23
|
const baseBlock = baseBlocks[action.baseIndex];
|
|
24
|
+
if (!baseBlock) return;
|
|
23
25
|
segmentsToReview.push({
|
|
24
26
|
baseBlock,
|
|
25
27
|
targetBlockText: null,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rebuildDocument.mjs","names":[],"sources":["../../../src/docReview/rebuildDocument.ts"],"sourcesContent":["import type { AlignmentPlan, FingerprintedBlock } from './types';\n\n/**\n * A block that needs to be translated or re-translated by an external consumer\n * (an AI client, a human, or an agent).\n */\nexport type SegmentToReview = {\n /** The base block to translate. */\n baseBlock: FingerprintedBlock;\n /** Existing target translation, or `null` when the block is new. */\n targetBlockText: string | null;\n /** Index of the originating action within {@link AlignmentPlan.actions}. */\n actionIndex: number;\n};\n\nexport type RebuildInput = {\n baseBlocks: FingerprintedBlock[];\n targetBlocks: FingerprintedBlock[];\n plan: AlignmentPlan;\n};\n\nexport type RebuildResult = {\n segmentsToReview: SegmentToReview[];\n};\n\n/**\n * Analyze the alignment plan and return only the segments that need\n * review/translation. Does not generate output text - that is done by\n * {@link mergeReviewedSegments} once the translations are available.\n *\n * @param input - The base/target blocks and the alignment plan.\n * @returns The list of segments that require translation.\n */\nexport const identifySegmentsToReview = ({\n baseBlocks,\n targetBlocks,\n plan,\n}: RebuildInput): RebuildResult => {\n const segmentsToReview: SegmentToReview[] = [];\n\n plan.actions.forEach((action, actionIndex) => {\n if (action.kind === 'review') {\n const baseBlock = baseBlocks[action.baseIndex];\n const targetBlockText =\n action.targetIndex !== null\n ? targetBlocks[action.targetIndex]
|
|
1
|
+
{"version":3,"file":"rebuildDocument.mjs","names":[],"sources":["../../../src/docReview/rebuildDocument.ts"],"sourcesContent":["import type { AlignmentPlan, FingerprintedBlock } from './types';\n\n/**\n * A block that needs to be translated or re-translated by an external consumer\n * (an AI client, a human, or an agent).\n */\nexport type SegmentToReview = {\n /** The base block to translate. */\n baseBlock: FingerprintedBlock;\n /** Existing target translation, or `null` when the block is new. */\n targetBlockText: string | null;\n /** Index of the originating action within {@link AlignmentPlan.actions}. */\n actionIndex: number;\n};\n\nexport type RebuildInput = {\n baseBlocks: FingerprintedBlock[];\n targetBlocks: FingerprintedBlock[];\n plan: AlignmentPlan;\n};\n\nexport type RebuildResult = {\n segmentsToReview: SegmentToReview[];\n};\n\n/**\n * Analyze the alignment plan and return only the segments that need\n * review/translation. Does not generate output text - that is done by\n * {@link mergeReviewedSegments} once the translations are available.\n *\n * @param input - The base/target blocks and the alignment plan.\n * @returns The list of segments that require translation.\n */\nexport const identifySegmentsToReview = ({\n baseBlocks,\n targetBlocks,\n plan,\n}: RebuildInput): RebuildResult => {\n const segmentsToReview: SegmentToReview[] = [];\n\n plan.actions.forEach((action, actionIndex) => {\n if (action.kind === 'review') {\n const baseBlock = baseBlocks[action.baseIndex];\n\n if (!baseBlock) return;\n\n const targetBlockText =\n action.targetIndex !== null\n ? (targetBlocks[action.targetIndex]?.content ?? null)\n : null;\n\n segmentsToReview.push({ baseBlock, targetBlockText, actionIndex });\n } else if (action.kind === 'insert_new') {\n const baseBlock = baseBlocks[action.baseIndex];\n\n if (!baseBlock) return;\n\n segmentsToReview.push({\n baseBlock,\n targetBlockText: null,\n actionIndex,\n });\n }\n });\n\n return { segmentsToReview };\n};\n\n/** Markdown separates two blocks with a blank line. */\nconst endsWithBlockBoundary = (text: string): boolean =>\n /\\n[ \\t]*\\n$/.test(text);\n\n/**\n * The newlines missing at the end of `text` for it to close a markdown block.\n *\n * @param text - The document built so far.\n * @returns `''`, `'\\n'` or `'\\n\\n'` depending on how the text already ends.\n */\nconst buildBlockSeparator = (text: string): string => {\n if (text.length === 0 || endsWithBlockBoundary(text)) return '';\n\n return text.endsWith('\\n') ? '\\n' : '\\n\\n';\n};\n\n/**\n * Merge reviewed translations back into the final document following the\n * alignment plan, reusing untouched target blocks as-is.\n *\n * @param plan - The alignment plan.\n * @param targetBlocks - Blocks of the existing target document.\n * @param reviewedSegments - Map of action index to its reviewed translation.\n * @returns The rebuilt target document.\n */\nexport const mergeReviewedSegments = (\n plan: AlignmentPlan,\n targetBlocks: FingerprintedBlock[],\n reviewedSegments: Map<number, string>\n): string => {\n let mergedText = '';\n let previousPartWasGenerated = false;\n\n /**\n * Append one part, keeping a blank line around the content this run produced.\n *\n * Blocks own the blank lines that trail them, so two verbatim blocks already\n * separate themselves and are appended untouched — a document with nothing to\n * change merges back byte for byte. Generated content is the exception: the\n * block it lands after may be the one that used to end the document, which\n * carries no trailing blank line, and the two would be glued into a single\n * markdown block (a heading swallowed by the paragraph above it).\n */\n const appendPart = (content: string, isGenerated: boolean): void => {\n if (content.length === 0) return;\n\n if (isGenerated || previousPartWasGenerated) {\n mergedText += buildBlockSeparator(mergedText);\n }\n\n mergedText += content;\n previousPartWasGenerated = isGenerated;\n };\n\n plan.actions.forEach((action, actionIndex) => {\n if (action.kind === 'reuse') {\n appendPart(targetBlocks[action.targetIndex]!.content, false);\n } else if (action.kind === 'review' || action.kind === 'insert_new') {\n const reviewedContent = reviewedSegments.get(actionIndex);\n\n if (reviewedContent !== undefined) {\n appendPart(reviewedContent, true);\n } else {\n // Fallback: if review failed, use existing or blank\n if (action.kind === 'review' && action.targetIndex !== null) {\n appendPart(targetBlocks[action.targetIndex]!.content, false);\n } else {\n appendPart('\\n', false);\n }\n }\n } else if (action.kind === 'delete') {\n const reviewedContent = reviewedSegments.get(actionIndex);\n if (reviewedContent !== undefined) {\n // Caller explicitly resolved this block: empty string = actually delete,\n // non-empty string = replacement content.\n appendPart(reviewedContent, true);\n } else {\n // Default: keep verbatim. A target block with no base counterpart may\n // just be a section the aligner could not follow (reordering, split\n // prose) — keeping it prevents accidental data loss in log/read-only mode.\n appendPart(targetBlocks[action.targetIndex]!.content, false);\n }\n }\n });\n\n return mergedText;\n};\n"],"mappings":";;;;;;;;;AAiCA,MAAa,4BAA4B,EACvC,YACA,cACA,WACiC;CACjC,MAAM,mBAAsC,CAAC;CAE7C,KAAK,QAAQ,SAAS,QAAQ,gBAAgB;EAC5C,IAAI,OAAO,SAAS,UAAU;GAC5B,MAAM,YAAY,WAAW,OAAO;GAEpC,IAAI,CAAC,WAAW;GAEhB,MAAM,kBACJ,OAAO,gBAAgB,OAClB,aAAa,OAAO,YAAY,EAAE,WAAW,OAC9C;GAEN,iBAAiB,KAAK;IAAE;IAAW;IAAiB;GAAY,CAAC;EACnE,OAAO,IAAI,OAAO,SAAS,cAAc;GACvC,MAAM,YAAY,WAAW,OAAO;GAEpC,IAAI,CAAC,WAAW;GAEhB,iBAAiB,KAAK;IACpB;IACA,iBAAiB;IACjB;GACF,CAAC;EACH;CACF,CAAC;CAED,OAAO,EAAE,iBAAiB;AAC5B;;AAGA,MAAM,yBAAyB,SAC7B,cAAc,KAAK,IAAI;;;;;;;AAQzB,MAAM,uBAAuB,SAAyB;CACpD,IAAI,KAAK,WAAW,KAAK,sBAAsB,IAAI,GAAG,OAAO;CAE7D,OAAO,KAAK,SAAS,IAAI,IAAI,OAAO;AACtC;;;;;;;;;;AAWA,MAAa,yBACX,MACA,cACA,qBACW;CACX,IAAI,aAAa;CACjB,IAAI,2BAA2B;;;;;;;;;;;CAY/B,MAAM,cAAc,SAAiB,gBAA+B;EAClE,IAAI,QAAQ,WAAW,GAAG;EAE1B,IAAI,eAAe,0BACjB,cAAc,oBAAoB,UAAU;EAG9C,cAAc;EACd,2BAA2B;CAC7B;CAEA,KAAK,QAAQ,SAAS,QAAQ,gBAAgB;EAC5C,IAAI,OAAO,SAAS,SAClB,WAAW,aAAa,OAAO,YAAY,CAAE,SAAS,KAAK;OACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,cAAc;GACnE,MAAM,kBAAkB,iBAAiB,IAAI,WAAW;GAExD,IAAI,oBAAoB,QACtB,WAAW,iBAAiB,IAAI;QAGhC,IAAI,OAAO,SAAS,YAAY,OAAO,gBAAgB,MACrD,WAAW,aAAa,OAAO,YAAY,CAAE,SAAS,KAAK;QAE3D,WAAW,MAAM,KAAK;EAG5B,OAAO,IAAI,OAAO,SAAS,UAAU;GACnC,MAAM,kBAAkB,iBAAiB,IAAI,WAAW;GACxD,IAAI,oBAAoB,QAGtB,WAAW,iBAAiB,IAAI;QAKhC,WAAW,aAAa,OAAO,YAAY,CAAE,SAAS,KAAK;EAE/D;CACF,CAAC;CAED,OAAO;AACT"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//#region src/docReview/segmentDocument.ts
|
|
2
2
|
const HEADING_PATTERN = /^\s*(#{1,6})\s+/;
|
|
3
|
-
const isBlankLine = (line) => line.trim().length === 0;
|
|
4
|
-
const isFencedCodeDelimiter = (line) => /^\s*```/.test(line);
|
|
3
|
+
const isBlankLine = (line) => !line || line.trim().length === 0;
|
|
4
|
+
const isFencedCodeDelimiter = (line) => Boolean(line && /^\s*```/.test(line));
|
|
5
5
|
/**
|
|
6
6
|
* Read the depth of an ATX markdown heading (`#` → 1, `######` → 6).
|
|
7
7
|
*
|
|
@@ -9,10 +9,11 @@ const isFencedCodeDelimiter = (line) => /^\s*```/.test(line);
|
|
|
9
9
|
* @returns The heading depth, or `null` when the line is not a heading.
|
|
10
10
|
*/
|
|
11
11
|
const parseHeadingDepth = (line) => {
|
|
12
|
+
if (!line) return null;
|
|
12
13
|
return HEADING_PATTERN.exec(line)?.[1]?.length ?? null;
|
|
13
14
|
};
|
|
14
15
|
const isHeading = (line) => parseHeadingDepth(line) !== null;
|
|
15
|
-
const isFrontmatterDelimiter = (line) => /^\s*---\s*$/.test(line);
|
|
16
|
+
const isFrontmatterDelimiter = (line) => Boolean(line && /^\s*---\s*$/.test(line));
|
|
16
17
|
/**
|
|
17
18
|
* Split a markdown document into fine-grained blocks.
|
|
18
19
|
*
|
|
@@ -87,7 +88,8 @@ const segmentDocument = (text) => {
|
|
|
87
88
|
if (units.length === 0) return [];
|
|
88
89
|
return units.map((unit, unitIndex) => {
|
|
89
90
|
const blockStartIndex = unitIndex === 0 ? 0 : unit.startIndex;
|
|
90
|
-
const
|
|
91
|
+
const nextUnit = units[unitIndex + 1];
|
|
92
|
+
const blockEndIndex = unitIndex === units.length - 1 || !nextUnit ? lineCount - 1 : nextUnit.startIndex - 1;
|
|
91
93
|
const blockLines = lines.slice(blockStartIndex, blockEndIndex + 1);
|
|
92
94
|
const content = blockEndIndex < lineCount - 1 ? `${blockLines.join("\n")}\n` : blockLines.join("\n");
|
|
93
95
|
return {
|
|
@@ -123,8 +125,9 @@ const segmentSections = (text) => {
|
|
|
123
125
|
let currentBlocks = [];
|
|
124
126
|
const flushSection = () => {
|
|
125
127
|
if (currentBlocks.length === 0) return;
|
|
126
|
-
const
|
|
128
|
+
const firstBlock = currentBlocks[0];
|
|
127
129
|
const lastBlock = currentBlocks[currentBlocks.length - 1];
|
|
130
|
+
if (!firstBlock || !lastBlock) return;
|
|
128
131
|
sections.push({
|
|
129
132
|
type: firstBlock.type,
|
|
130
133
|
content: currentBlocks.map((block) => block.content).join(""),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segmentDocument.mjs","names":[],"sources":["../../../src/docReview/segmentDocument.ts"],"sourcesContent":["import type { Block, BlockType } from './types';\n\nconst HEADING_PATTERN = /^\\s*(#{1,6})\\s+/;\n\nconst isBlankLine = (line: string): boolean => line.trim().length === 0;\nconst isFencedCodeDelimiter = (line: string): boolean => /^\\s*```/.test(line);\n\n/**\n * Read the depth of an ATX markdown heading (`#` → 1, `######` → 6).\n *\n * @param line - The line to inspect.\n * @returns The heading depth, or `null` when the line is not a heading.\n */\nconst parseHeadingDepth = (line: string): number | null => {\n const match = HEADING_PATTERN.exec(line);\n\n return match?.[1]?.length ?? null;\n};\n\nconst isHeading = (line: string): boolean => parseHeadingDepth(line) !== null;\nconst isFrontmatterDelimiter = (line: string): boolean =>\n /^\\s*---\\s*$/.test(line);\n\n/**\n * A content unit (heading, paragraph, code block or frontmatter) spanning a\n * 0-based, inclusive line range. Blank-line runs are not units of their own;\n * they are folded into the preceding unit as trailing separators (see\n * {@link segmentDocument}) so that the concatenation of every block's content\n * reproduces the document byte-for-byte.\n */\ntype ContentUnit = {\n type: BlockType;\n /** Depth of the ATX heading opening the unit, `null` when it is not a heading. */\n headingDepth: number | null;\n startIndex: number;\n endIndex: number;\n};\n\n/**\n * Split a markdown document into fine-grained blocks.\n *\n * Boundaries are drawn at headings, blank lines (paragraph breaks) and fenced\n * code blocks, while frontmatter and the inside of code fences are kept intact.\n * Each blank-line run is appended to the block that precedes it, so the blocks\n * form an exact partition of the document: concatenating every `content` (in\n * order) yields the original text unchanged. This is what lets the block-aware\n * review re-translate only the paragraphs/snippets that actually changed instead\n * of the whole heading section.\n *\n * @param text - The full markdown document.\n * @returns The ordered list of blocks with their 1-based line ranges.\n */\nexport const segmentDocument = (text: string): Block[] => {\n const lines = text.split('\\n');\n const lineCount = lines.length;\n\n // 1. Tokenize into content units, skipping blank-line runs (folded in below).\n const units: ContentUnit[] = [];\n let index = 0;\n\n while (index < lineCount) {\n const currentLine = lines[index];\n\n if (isBlankLine(currentLine)) {\n index += 1;\n continue;\n }\n\n // Frontmatter: only when it opens the document.\n if (units.length === 0 && isFrontmatterDelimiter(currentLine)) {\n const startIndex = index;\n index += 1;\n while (index < lineCount && !isFrontmatterDelimiter(lines[index])) {\n index += 1;\n }\n // Include the closing delimiter when present.\n if (index < lineCount) index += 1;\n units.push({\n type: 'unknown',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n continue;\n }\n\n // Fenced code block: consumed whole so inner blank lines and `#` lines are\n // never treated as boundaries.\n if (isFencedCodeDelimiter(currentLine)) {\n const startIndex = index;\n index += 1;\n while (index < lineCount && !isFencedCodeDelimiter(lines[index])) {\n index += 1;\n }\n // Include the closing fence when present.\n if (index < lineCount) index += 1;\n units.push({\n type: 'code_block',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n continue;\n }\n\n // Heading: a single self-contained line.\n const headingDepth = parseHeadingDepth(currentLine);\n\n if (headingDepth !== null) {\n units.push({\n type: 'heading',\n headingDepth,\n startIndex: index,\n endIndex: index,\n });\n index += 1;\n continue;\n }\n\n // Paragraph: a run of consecutive lines until a blank line, a heading or a\n // code fence. Tables and tight lists stay together (no blank line between\n // their rows/items).\n const startIndex = index;\n while (\n index < lineCount &&\n !isBlankLine(lines[index]) &&\n !isHeading(lines[index]) &&\n !isFencedCodeDelimiter(lines[index])\n ) {\n index += 1;\n }\n units.push({\n type: 'paragraph',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n }\n\n if (units.length === 0) return [];\n\n // 2. Turn each unit into a block whose line range extends to just before the\n // next unit, so the trailing blank-line run is owned by it. The first block\n // also absorbs any leading blank lines, and the last block runs to EOF.\n return units.map((unit, unitIndex): Block => {\n const blockStartIndex = unitIndex === 0 ? 0 : unit.startIndex;\n const blockEndIndex =\n unitIndex === units.length - 1\n ? lineCount - 1\n : units[unitIndex + 1].startIndex - 1;\n\n const blockLines = lines.slice(blockStartIndex, blockEndIndex + 1);\n // Re-append the boundary newline dropped by `split` for every block but the\n // one ending at EOF, so concatenating all blocks rebuilds the document.\n const content =\n blockEndIndex < lineCount - 1\n ? `${blockLines.join('\\n')}\\n`\n : blockLines.join('\\n');\n\n return {\n type: unit.type,\n content,\n headingDepth: unit.headingDepth,\n lineStart: blockStartIndex + 1,\n lineEnd: blockEndIndex + 1,\n };\n });\n};\n\n/**\n * Split a markdown document into coarse, heading-anchored sections.\n *\n * Built by grouping the fine blocks of {@link segmentDocument}: frontmatter and\n * each heading open a new section, and the following non-heading blocks are\n * folded into it. Because it only concatenates adjacent fine blocks, the result\n * is still an exact partition of the document (sections concatenate back to the\n * source unchanged).\n *\n * Sections are the robust alignment unit between a base document and its\n * translation — both share the same heading structure, so they align almost\n * perfectly and a translation that splits its prose into a different number of\n * paragraphs never causes a section to be dropped. Fine-grained review happens\n * within a section once it is known to have changed.\n *\n * @param text - The full markdown document.\n * @returns The ordered list of sections with their 1-based line ranges.\n */\nexport const segmentSections = (text: string): Block[] => {\n const fineBlocks = segmentDocument(text);\n const sections: Block[] = [];\n let currentBlocks: Block[] = [];\n\n const flushSection = (): void => {\n if (currentBlocks.length === 0) return;\n\n const [firstBlock] = currentBlocks;\n const lastBlock = currentBlocks[currentBlocks.length - 1];\n\n sections.push({\n type: firstBlock.type,\n content: currentBlocks.map((block) => block.content).join(''),\n // A section is identified by the heading that opens it, so it inherits its\n // depth — this is what keeps a `##` section from aligning with a `###` one.\n headingDepth: firstBlock.headingDepth,\n lineStart: firstBlock.lineStart,\n lineEnd: lastBlock.lineEnd,\n });\n currentBlocks = [];\n };\n\n for (const block of fineBlocks) {\n // Frontmatter (a leading `unknown` block) and every heading open a section.\n const opensSection =\n block.type === 'heading' ||\n (block.type === 'unknown' && sections.length === 0);\n\n if (opensSection) flushSection();\n currentBlocks.push(block);\n if (block.type === 'unknown' && sections.length === 0) flushSection();\n }\n\n flushSection();\n\n return sections;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"segmentDocument.mjs","names":[],"sources":["../../../src/docReview/segmentDocument.ts"],"sourcesContent":["import type { Block, BlockType } from './types';\n\nconst HEADING_PATTERN = /^\\s*(#{1,6})\\s+/;\n\nconst isBlankLine = (line?: string): boolean =>\n !line || line.trim().length === 0;\nconst isFencedCodeDelimiter = (line?: string): boolean =>\n Boolean(line && /^\\s*```/.test(line));\n\n/**\n * Read the depth of an ATX markdown heading (`#` → 1, `######` → 6).\n *\n * @param line - The line to inspect.\n * @returns The heading depth, or `null` when the line is not a heading.\n */\nconst parseHeadingDepth = (line?: string): number | null => {\n if (!line) return null;\n const match = HEADING_PATTERN.exec(line);\n\n return match?.[1]?.length ?? null;\n};\n\nconst isHeading = (line?: string): boolean => parseHeadingDepth(line) !== null;\nconst isFrontmatterDelimiter = (line?: string): boolean =>\n Boolean(line && /^\\s*---\\s*$/.test(line));\n\n/**\n * A content unit (heading, paragraph, code block or frontmatter) spanning a\n * 0-based, inclusive line range. Blank-line runs are not units of their own;\n * they are folded into the preceding unit as trailing separators (see\n * {@link segmentDocument}) so that the concatenation of every block's content\n * reproduces the document byte-for-byte.\n */\ntype ContentUnit = {\n type: BlockType;\n /** Depth of the ATX heading opening the unit, `null` when it is not a heading. */\n headingDepth: number | null;\n startIndex: number;\n endIndex: number;\n};\n\n/**\n * Split a markdown document into fine-grained blocks.\n *\n * Boundaries are drawn at headings, blank lines (paragraph breaks) and fenced\n * code blocks, while frontmatter and the inside of code fences are kept intact.\n * Each blank-line run is appended to the block that precedes it, so the blocks\n * form an exact partition of the document: concatenating every `content` (in\n * order) yields the original text unchanged. This is what lets the block-aware\n * review re-translate only the paragraphs/snippets that actually changed instead\n * of the whole heading section.\n *\n * @param text - The full markdown document.\n * @returns The ordered list of blocks with their 1-based line ranges.\n */\nexport const segmentDocument = (text: string): Block[] => {\n const lines = text.split('\\n');\n const lineCount = lines.length;\n\n // 1. Tokenize into content units, skipping blank-line runs (folded in below).\n const units: ContentUnit[] = [];\n let index = 0;\n\n while (index < lineCount) {\n const currentLine = lines[index];\n\n if (isBlankLine(currentLine)) {\n index += 1;\n continue;\n }\n\n // Frontmatter: only when it opens the document.\n if (units.length === 0 && isFrontmatterDelimiter(currentLine)) {\n const startIndex = index;\n index += 1;\n while (index < lineCount && !isFrontmatterDelimiter(lines[index])) {\n index += 1;\n }\n // Include the closing delimiter when present.\n if (index < lineCount) index += 1;\n units.push({\n type: 'unknown',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n continue;\n }\n\n // Fenced code block: consumed whole so inner blank lines and `#` lines are\n // never treated as boundaries.\n if (isFencedCodeDelimiter(currentLine)) {\n const startIndex = index;\n index += 1;\n while (index < lineCount && !isFencedCodeDelimiter(lines[index])) {\n index += 1;\n }\n // Include the closing fence when present.\n if (index < lineCount) index += 1;\n units.push({\n type: 'code_block',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n continue;\n }\n\n // Heading: a single self-contained line.\n const headingDepth = parseHeadingDepth(currentLine);\n\n if (headingDepth !== null) {\n units.push({\n type: 'heading',\n headingDepth,\n startIndex: index,\n endIndex: index,\n });\n index += 1;\n continue;\n }\n\n // Paragraph: a run of consecutive lines until a blank line, a heading or a\n // code fence. Tables and tight lists stay together (no blank line between\n // their rows/items).\n const startIndex = index;\n while (\n index < lineCount &&\n !isBlankLine(lines[index]) &&\n !isHeading(lines[index]) &&\n !isFencedCodeDelimiter(lines[index])\n ) {\n index += 1;\n }\n units.push({\n type: 'paragraph',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n }\n\n if (units.length === 0) return [];\n\n // 2. Turn each unit into a block whose line range extends to just before the\n // next unit, so the trailing blank-line run is owned by it. The first block\n // also absorbs any leading blank lines, and the last block runs to EOF.\n return units.map((unit, unitIndex): Block => {\n const blockStartIndex = unitIndex === 0 ? 0 : unit.startIndex;\n const nextUnit = units[unitIndex + 1];\n const blockEndIndex =\n unitIndex === units.length - 1 || !nextUnit\n ? lineCount - 1\n : nextUnit.startIndex - 1;\n\n const blockLines = lines.slice(blockStartIndex, blockEndIndex + 1);\n // Re-append the boundary newline dropped by `split` for every block but the\n // one ending at EOF, so concatenating all blocks rebuilds the document.\n const content =\n blockEndIndex < lineCount - 1\n ? `${blockLines.join('\\n')}\\n`\n : blockLines.join('\\n');\n\n return {\n type: unit.type,\n content,\n headingDepth: unit.headingDepth,\n lineStart: blockStartIndex + 1,\n lineEnd: blockEndIndex + 1,\n };\n });\n};\n\n/**\n * Split a markdown document into coarse, heading-anchored sections.\n *\n * Built by grouping the fine blocks of {@link segmentDocument}: frontmatter and\n * each heading open a new section, and the following non-heading blocks are\n * folded into it. Because it only concatenates adjacent fine blocks, the result\n * is still an exact partition of the document (sections concatenate back to the\n * source unchanged).\n *\n * Sections are the robust alignment unit between a base document and its\n * translation — both share the same heading structure, so they align almost\n * perfectly and a translation that splits its prose into a different number of\n * paragraphs never causes a section to be dropped. Fine-grained review happens\n * within a section once it is known to have changed.\n *\n * @param text - The full markdown document.\n * @returns The ordered list of sections with their 1-based line ranges.\n */\nexport const segmentSections = (text: string): Block[] => {\n const fineBlocks = segmentDocument(text);\n const sections: Block[] = [];\n let currentBlocks: Block[] = [];\n\n const flushSection = (): void => {\n if (currentBlocks.length === 0) return;\n\n const firstBlock = currentBlocks[0];\n const lastBlock = currentBlocks[currentBlocks.length - 1];\n\n if (!firstBlock || !lastBlock) return;\n\n sections.push({\n type: firstBlock.type,\n content: currentBlocks.map((block) => block.content).join(''),\n // A section is identified by the heading that opens it, so it inherits its\n // depth — this is what keeps a `##` section from aligning with a `###` one.\n headingDepth: firstBlock.headingDepth,\n lineStart: firstBlock.lineStart,\n lineEnd: lastBlock.lineEnd,\n });\n currentBlocks = [];\n };\n\n for (const block of fineBlocks) {\n // Frontmatter (a leading `unknown` block) and every heading open a section.\n const opensSection =\n block.type === 'heading' ||\n (block.type === 'unknown' && sections.length === 0);\n\n if (opensSection) flushSection();\n currentBlocks.push(block);\n if (block.type === 'unknown' && sections.length === 0) flushSection();\n }\n\n flushSection();\n\n return sections;\n};\n"],"mappings":"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"}
|
|
@@ -71,7 +71,10 @@ const setHtmlLang = (ast) => {
|
|
|
71
71
|
const langAttr = node.attributes?.find((attr) => attr.type === "JSXAttribute" && attr.name?.name === "lang");
|
|
72
72
|
const localeExpression = b.jsxExpressionContainer(b.identifier("locale"));
|
|
73
73
|
if (langAttr) langAttr.value = localeExpression;
|
|
74
|
-
else
|
|
74
|
+
else {
|
|
75
|
+
node.attributes = node.attributes ?? [];
|
|
76
|
+
node.attributes.push(b.jsxAttribute(b.jsxIdentifier("lang"), localeExpression));
|
|
77
|
+
}
|
|
75
78
|
return false;
|
|
76
79
|
}
|
|
77
80
|
this.traverse(path);
|