@intlayer/engine 9.5.10 → 9.5.11

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.
Files changed (188) hide show
  1. package/dist/cjs/docReview/alignBlocks.cjs +53 -36
  2. package/dist/cjs/docReview/alignBlocks.cjs.map +1 -1
  3. package/dist/cjs/docReview/rebuildDocument.cjs +3 -1
  4. package/dist/cjs/docReview/rebuildDocument.cjs.map +1 -1
  5. package/dist/cjs/docReview/segmentDocument.cjs +8 -5
  6. package/dist/cjs/docReview/segmentDocument.cjs.map +1 -1
  7. package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs +4 -1
  8. package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs.map +1 -1
  9. package/dist/cjs/installMCP/installMCP.cjs +1 -1
  10. package/dist/cjs/installMCP/installMCP.cjs.map +1 -1
  11. package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs +3 -1
  12. package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs.map +1 -1
  13. package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs +1 -1
  14. package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs.map +1 -1
  15. package/dist/cjs/loadDictionaries/log.cjs +1 -1
  16. package/dist/cjs/loadDictionaries/log.cjs.map +1 -1
  17. package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs +19 -12
  18. package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs.map +1 -1
  19. package/dist/cjs/scan/analyzeBundleContent.cjs +26 -12
  20. package/dist/cjs/scan/analyzeBundleContent.cjs.map +1 -1
  21. package/dist/cjs/scan/calculateScore.cjs +4 -1
  22. package/dist/cjs/scan/calculateScore.cjs.map +1 -1
  23. package/dist/cjs/scan/checks.cjs +394 -221
  24. package/dist/cjs/scan/checks.cjs.map +1 -1
  25. package/dist/cjs/scan/detection/checkDetails.cjs +56 -0
  26. package/dist/cjs/scan/detection/checkDetails.cjs.map +1 -0
  27. package/dist/cjs/scan/detection/classifyInternalLinks.cjs +47 -0
  28. package/dist/cjs/scan/detection/classifyInternalLinks.cjs.map +1 -0
  29. package/dist/cjs/scan/detection/detectRoutingStrategy.cjs +143 -0
  30. package/dist/cjs/scan/detection/detectRoutingStrategy.cjs.map +1 -0
  31. package/dist/cjs/scan/detection/detectTechnologies.cjs +89 -0
  32. package/dist/cjs/scan/detection/detectTechnologies.cjs.map +1 -0
  33. package/dist/cjs/scan/detection/index.cjs +36 -0
  34. package/dist/cjs/scan/detection/localeCode.cjs +118 -0
  35. package/dist/cjs/scan/detection/localeCode.cjs.map +1 -0
  36. package/dist/cjs/scan/detection/localizedPages.cjs +48 -0
  37. package/dist/cjs/scan/detection/localizedPages.cjs.map +1 -0
  38. package/dist/cjs/scan/detection/technologySignatures.cjs +564 -0
  39. package/dist/cjs/scan/detection/technologySignatures.cjs.map +1 -0
  40. package/dist/cjs/scan/detection/url.cjs +53 -0
  41. package/dist/cjs/scan/detection/url.cjs.map +1 -0
  42. package/dist/cjs/scan/fetchText.cjs +56 -0
  43. package/dist/cjs/scan/fetchText.cjs.map +1 -0
  44. package/dist/cjs/scan/index.cjs +59 -3
  45. package/dist/cjs/scan/parseHtml.cjs +55 -10
  46. package/dist/cjs/scan/parseHtml.cjs.map +1 -1
  47. package/dist/cjs/scan/robots.cjs +65 -0
  48. package/dist/cjs/scan/robots.cjs.map +1 -0
  49. package/dist/cjs/scan/runScanChecks.cjs +89 -0
  50. package/dist/cjs/scan/runScanChecks.cjs.map +1 -0
  51. package/dist/cjs/scan/scanWebsite.cjs +58 -52
  52. package/dist/cjs/scan/scanWebsite.cjs.map +1 -1
  53. package/dist/cjs/scan/sitemap.cjs +109 -0
  54. package/dist/cjs/scan/sitemap.cjs.map +1 -0
  55. package/dist/cjs/utils/chunkJSON.cjs +24 -9
  56. package/dist/cjs/utils/chunkJSON.cjs.map +1 -1
  57. package/dist/cjs/utils/getChunk.cjs +2 -2
  58. package/dist/cjs/utils/getChunk.cjs.map +1 -1
  59. package/dist/cjs/utils/runParallel/index.cjs +1 -1
  60. package/dist/cjs/utils/runParallel/index.cjs.map +1 -1
  61. package/dist/cjs/utils/runParallel/pidTree.cjs +23 -11
  62. package/dist/cjs/utils/runParallel/pidTree.cjs.map +1 -1
  63. package/dist/cjs/utils/runParallel/ps.cjs +8 -3
  64. package/dist/cjs/utils/runParallel/ps.cjs.map +1 -1
  65. package/dist/cjs/utils/runParallel/runTask.cjs +2 -1
  66. package/dist/cjs/utils/runParallel/runTask.cjs.map +1 -1
  67. package/dist/cjs/utils/runParallel/wmic.cjs +8 -3
  68. package/dist/cjs/utils/runParallel/wmic.cjs.map +1 -1
  69. package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs +7 -4
  70. package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs.map +1 -1
  71. package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs +2 -2
  72. package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs.map +1 -1
  73. package/dist/esm/docReview/alignBlocks.mjs +53 -36
  74. package/dist/esm/docReview/alignBlocks.mjs.map +1 -1
  75. package/dist/esm/docReview/rebuildDocument.mjs +3 -1
  76. package/dist/esm/docReview/rebuildDocument.mjs.map +1 -1
  77. package/dist/esm/docReview/segmentDocument.mjs +8 -5
  78. package/dist/esm/docReview/segmentDocument.mjs.map +1 -1
  79. package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs +4 -1
  80. package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs.map +1 -1
  81. package/dist/esm/installMCP/installMCP.mjs +1 -1
  82. package/dist/esm/installMCP/installMCP.mjs.map +1 -1
  83. package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs +3 -1
  84. package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs.map +1 -1
  85. package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs +1 -1
  86. package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs.map +1 -1
  87. package/dist/esm/loadDictionaries/log.mjs +1 -1
  88. package/dist/esm/loadDictionaries/log.mjs.map +1 -1
  89. package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs +19 -12
  90. package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs.map +1 -1
  91. package/dist/esm/scan/analyzeBundleContent.mjs +26 -12
  92. package/dist/esm/scan/analyzeBundleContent.mjs.map +1 -1
  93. package/dist/esm/scan/calculateScore.mjs +4 -1
  94. package/dist/esm/scan/calculateScore.mjs.map +1 -1
  95. package/dist/esm/scan/checks.mjs +387 -220
  96. package/dist/esm/scan/checks.mjs.map +1 -1
  97. package/dist/esm/scan/detection/checkDetails.mjs +54 -0
  98. package/dist/esm/scan/detection/checkDetails.mjs.map +1 -0
  99. package/dist/esm/scan/detection/classifyInternalLinks.mjs +46 -0
  100. package/dist/esm/scan/detection/classifyInternalLinks.mjs.map +1 -0
  101. package/dist/esm/scan/detection/detectRoutingStrategy.mjs +141 -0
  102. package/dist/esm/scan/detection/detectRoutingStrategy.mjs.map +1 -0
  103. package/dist/esm/scan/detection/detectTechnologies.mjs +87 -0
  104. package/dist/esm/scan/detection/detectTechnologies.mjs.map +1 -0
  105. package/dist/esm/scan/detection/index.mjs +10 -0
  106. package/dist/esm/scan/detection/localeCode.mjs +108 -0
  107. package/dist/esm/scan/detection/localeCode.mjs.map +1 -0
  108. package/dist/esm/scan/detection/localizedPages.mjs +46 -0
  109. package/dist/esm/scan/detection/localizedPages.mjs.map +1 -0
  110. package/dist/esm/scan/detection/technologySignatures.mjs +563 -0
  111. package/dist/esm/scan/detection/technologySignatures.mjs.map +1 -0
  112. package/dist/esm/scan/detection/url.mjs +47 -0
  113. package/dist/esm/scan/detection/url.mjs.map +1 -0
  114. package/dist/esm/scan/fetchText.mjs +55 -0
  115. package/dist/esm/scan/fetchText.mjs.map +1 -0
  116. package/dist/esm/scan/index.mjs +15 -3
  117. package/dist/esm/scan/parseHtml.mjs +52 -11
  118. package/dist/esm/scan/parseHtml.mjs.map +1 -1
  119. package/dist/esm/scan/robots.mjs +63 -0
  120. package/dist/esm/scan/robots.mjs.map +1 -0
  121. package/dist/esm/scan/runScanChecks.mjs +88 -0
  122. package/dist/esm/scan/runScanChecks.mjs.map +1 -0
  123. package/dist/esm/scan/scanWebsite.mjs +59 -53
  124. package/dist/esm/scan/scanWebsite.mjs.map +1 -1
  125. package/dist/esm/scan/sitemap.mjs +105 -0
  126. package/dist/esm/scan/sitemap.mjs.map +1 -0
  127. package/dist/esm/utils/chunkJSON.mjs +24 -9
  128. package/dist/esm/utils/chunkJSON.mjs.map +1 -1
  129. package/dist/esm/utils/getChunk.mjs +2 -2
  130. package/dist/esm/utils/getChunk.mjs.map +1 -1
  131. package/dist/esm/utils/runParallel/index.mjs +1 -1
  132. package/dist/esm/utils/runParallel/index.mjs.map +1 -1
  133. package/dist/esm/utils/runParallel/pidTree.mjs +23 -11
  134. package/dist/esm/utils/runParallel/pidTree.mjs.map +1 -1
  135. package/dist/esm/utils/runParallel/ps.mjs +8 -3
  136. package/dist/esm/utils/runParallel/ps.mjs.map +1 -1
  137. package/dist/esm/utils/runParallel/runTask.mjs +2 -1
  138. package/dist/esm/utils/runParallel/runTask.mjs.map +1 -1
  139. package/dist/esm/utils/runParallel/wmic.mjs +8 -3
  140. package/dist/esm/utils/runParallel/wmic.mjs.map +1 -1
  141. package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs +7 -4
  142. package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs.map +1 -1
  143. package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs +2 -2
  144. package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs.map +1 -1
  145. package/dist/types/docReview/rebuildDocument.d.ts.map +1 -1
  146. package/dist/types/docReview/segmentDocument.d.ts.map +1 -1
  147. package/dist/types/init/frameworkSetup/nextAppRouter/transforms.d.ts.map +1 -1
  148. package/dist/types/loadDictionaries/logTypeScriptErrors.d.ts.map +1 -1
  149. package/dist/types/scan/analyzeBundleContent.d.ts.map +1 -1
  150. package/dist/types/scan/calculateScore.d.ts +4 -1
  151. package/dist/types/scan/calculateScore.d.ts.map +1 -1
  152. package/dist/types/scan/checks.d.ts +114 -21
  153. package/dist/types/scan/checks.d.ts.map +1 -1
  154. package/dist/types/scan/detection/checkDetails.d.ts +29 -0
  155. package/dist/types/scan/detection/checkDetails.d.ts.map +1 -0
  156. package/dist/types/scan/detection/classifyInternalLinks.d.ts +44 -0
  157. package/dist/types/scan/detection/classifyInternalLinks.d.ts.map +1 -0
  158. package/dist/types/scan/detection/detectRoutingStrategy.d.ts +60 -0
  159. package/dist/types/scan/detection/detectRoutingStrategy.d.ts.map +1 -0
  160. package/dist/types/scan/detection/detectTechnologies.d.ts +60 -0
  161. package/dist/types/scan/detection/detectTechnologies.d.ts.map +1 -0
  162. package/dist/types/scan/detection/index.d.ts +9 -0
  163. package/dist/types/scan/detection/localeCode.d.ts +49 -0
  164. package/dist/types/scan/detection/localeCode.d.ts.map +1 -0
  165. package/dist/types/scan/detection/localizedPages.d.ts +40 -0
  166. package/dist/types/scan/detection/localizedPages.d.ts.map +1 -0
  167. package/dist/types/scan/detection/technologySignatures.d.ts +48 -0
  168. package/dist/types/scan/detection/technologySignatures.d.ts.map +1 -0
  169. package/dist/types/scan/detection/url.d.ts +27 -0
  170. package/dist/types/scan/detection/url.d.ts.map +1 -0
  171. package/dist/types/scan/fetchText.d.ts +30 -0
  172. package/dist/types/scan/fetchText.d.ts.map +1 -0
  173. package/dist/types/scan/index.d.ts +16 -4
  174. package/dist/types/scan/parseHtml.d.ts +17 -0
  175. package/dist/types/scan/parseHtml.d.ts.map +1 -1
  176. package/dist/types/scan/robots.d.ts +23 -0
  177. package/dist/types/scan/robots.d.ts.map +1 -0
  178. package/dist/types/scan/runScanChecks.d.ts +43 -0
  179. package/dist/types/scan/runScanChecks.d.ts.map +1 -0
  180. package/dist/types/scan/scanWebsite.d.ts.map +1 -1
  181. package/dist/types/scan/sitemap.d.ts +49 -0
  182. package/dist/types/scan/sitemap.d.ts.map +1 -0
  183. package/dist/types/scan/types.d.ts +15 -0
  184. package/dist/types/scan/types.d.ts.map +1 -1
  185. package/dist/types/utils/chunkJSON.d.ts.map +1 -1
  186. package/dist/types/utils/runParallel/pidTree.d.ts.map +1 -1
  187. package/dist/types/writeContentDeclaration/writeMarkdownFile.d.ts.map +1 -1
  188. package/package.json +15 -7
@@ -58,16 +58,19 @@ const flattenNativeDiagnosticMessage = (diagnostic, depth = 0) => [`${" ".repea
58
58
  * export only exposes the version. It spawns the bundled `tsgo` binary, which
59
59
  * resolves the closest tsconfig of each file.
60
60
  */
61
- const collectNativeTypeScriptErrors = (NativeAPI, filePaths, baseDir) => {
61
+ const collectNativeTypeScriptErrors = async (NativeAPI, filePaths, baseDir) => {
62
62
  const api = new NativeAPI({ cwd: baseDir });
63
63
  try {
64
- const snapshot = api.updateSnapshot({ openFiles: filePaths });
64
+ const snapshot = await api.updateSnapshot({ openFiles: filePaths });
65
65
  try {
66
- return filePaths.flatMap((filePath) => {
67
- const program = snapshot.getDefaultProjectForFile(filePath)?.program;
68
- const sourceFile = program?.getSourceFile(filePath);
69
- if (!program || !sourceFile) return [];
70
- return [...program.getSyntacticDiagnostics(filePath), ...program.getSemanticDiagnostics(filePath)].map((diagnostic) => {
66
+ return (await Promise.all(filePaths.map(async (filePath) => {
67
+ const program = (await snapshot.getDefaultProjectForFile(filePath))?.program;
68
+ if (!program) return [];
69
+ const sourceFile = await program.getSourceFile(filePath);
70
+ if (!sourceFile) return [];
71
+ const syntacticDiagnostics = await program.getSyntacticDiagnostics(filePath);
72
+ const semanticDiagnostics = await program.getSemanticDiagnostics(filePath);
73
+ return [...syntacticDiagnostics, ...semanticDiagnostics].map((diagnostic) => {
71
74
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(diagnostic.pos);
72
75
  return {
73
76
  fileName: diagnostic.fileName ?? filePath,
@@ -76,12 +79,12 @@ const collectNativeTypeScriptErrors = (NativeAPI, filePaths, baseDir) => {
76
79
  message: flattenNativeDiagnosticMessage(diagnostic)
77
80
  };
78
81
  });
79
- });
82
+ }))).flat();
80
83
  } finally {
81
- snapshot.dispose();
84
+ await snapshot.dispose();
82
85
  }
83
86
  } finally {
84
- api.close();
87
+ await api.close();
85
88
  }
86
89
  };
87
90
  /**
@@ -99,9 +102,13 @@ const collectTypeScriptErrors = async (filePaths, baseDir) => {
99
102
  if (classicTypeScript?.createProgram) return collectClassicTypeScriptErrors(classicTypeScript, filePaths, baseDir);
100
103
  let nativeModule;
101
104
  try {
102
- nativeModule = await import("typescript/unstable/sync");
105
+ nativeModule = await import("typescript/unstable/async");
103
106
  } catch {
104
- return;
107
+ try {
108
+ nativeModule = await import("typescript/unstable/sync");
109
+ } catch {
110
+ return;
111
+ }
105
112
  }
106
113
  return collectNativeTypeScriptErrors(nativeModule.API, filePaths, baseDir);
107
114
  };
@@ -1 +1 @@
1
- {"version":3,"file":"logTypeScriptErrors.cjs","names":["dirname","getAppLogger","extname","resolve"],"sources":["../../../src/loadDictionaries/logTypeScriptErrors.ts"],"sourcesContent":["import { dirname, extname, resolve } from 'node:path';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type {\n API as NativeTypeScriptAPI,\n Diagnostic as NativeTypeScriptDiagnostic,\n} from 'typescript/unstable/sync';\n\n/** Diagnostic of the classic (TS <= 6) compiler API. */\ninterface ClassicTypeScriptDiagnostic {\n file?: {\n fileName: string;\n getLineAndCharacterOfPosition: (position: number) => {\n line: number;\n character: number;\n };\n };\n start?: number;\n messageText: string | { messageText: string };\n}\n\n/** Subset of the classic (TS <= 6) compiler API used here. */\ninterface ClassicTypeScriptCompiler {\n findConfigFile: (\n searchPath: string,\n fileExists: (path: string) => boolean,\n configName?: string\n ) => string | undefined;\n sys: {\n fileExists: (path: string) => boolean;\n readFile: (path: string, encoding?: string) => string | undefined;\n };\n parseConfigFileTextToJson: (\n fileName: string,\n jsonText: string\n ) => { config?: Record<string, unknown>; error?: unknown };\n parseJsonConfigFileContent: (\n json: unknown,\n host: unknown,\n basePath: string\n ) => {\n options: Record<string, unknown>;\n };\n createProgram: (\n rootNames: readonly string[],\n options: Record<string, unknown>\n ) => {\n getSourceFile: (fileName: string) => unknown;\n };\n getPreEmitDiagnostics: (\n program: unknown,\n sourceFile?: unknown\n ) => readonly ClassicTypeScriptDiagnostic[];\n flattenDiagnosticMessageText: (\n diagnostic: string | { messageText: string },\n newLine: string\n ) => string;\n}\n\n/** Root module of `typescript`, possibly wrapped in a CJS `default`. */\ntype ClassicTypeScriptModule = Partial<ClassicTypeScriptCompiler> & {\n default?: Partial<ClassicTypeScriptCompiler>;\n};\n\n/** Engine-agnostic diagnostic, with 0-based line and character. */\ninterface TypeScriptError {\n fileName: string;\n line: number;\n character: number;\n message: string;\n}\n\nconst CHECKABLE_EXTENSIONS = [\n '.ts',\n '.tsx',\n '.js',\n '.jsx',\n '.cjs',\n '.mjs',\n '.json',\n];\n\n/**\n * Checks files with the classic compiler API (`typescript` <= 6), which is\n * exposed from the package root.\n */\nconst collectClassicTypeScriptErrors = (\n typescript: ClassicTypeScriptCompiler,\n filePaths: string[],\n baseDir: string\n): TypeScriptError[] => {\n const configFileName = typescript.findConfigFile(\n baseDir,\n typescript.sys.fileExists,\n 'tsconfig.json'\n );\n\n let compilerOptions: Record<string, unknown> = {\n noEmit: true,\n allowJs: true,\n resolveJsonModule: true,\n };\n\n const configFileText = configFileName\n ? typescript.sys.readFile(configFileName)\n : undefined;\n\n if (configFileName && configFileText) {\n const configJson = typescript.parseConfigFileTextToJson(\n configFileName,\n configFileText\n );\n\n if (!configJson.error) {\n const parsedConfig = typescript.parseJsonConfigFileContent(\n configJson.config,\n typescript.sys,\n dirname(configFileName)\n );\n\n const { incremental, tsBuildInfoFile, ...restOptions } =\n parsedConfig.options;\n\n compilerOptions = { ...compilerOptions, ...restOptions, noEmit: true };\n }\n }\n\n const program = typescript.createProgram(filePaths, compilerOptions);\n\n return filePaths.flatMap((filePath) => {\n const sourceFile = program.getSourceFile(filePath);\n\n if (!sourceFile) return [];\n\n return typescript\n .getPreEmitDiagnostics(program, sourceFile)\n .flatMap((diagnostic) => {\n if (!diagnostic.file || diagnostic.start === undefined) return [];\n\n const { line, character } =\n diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);\n\n return {\n fileName: diagnostic.file.fileName,\n line,\n character,\n message: typescript.flattenDiagnosticMessageText(\n diagnostic.messageText,\n '\\n'\n ),\n };\n });\n });\n};\n\n/** Flattens a native diagnostic message chain, indenting nested messages. */\nconst flattenNativeDiagnosticMessage = (\n diagnostic: NativeTypeScriptDiagnostic,\n depth = 0\n): string =>\n [\n `${' '.repeat(depth)}${diagnostic.text}`,\n ...(diagnostic.messageChain ?? []).map((chainedDiagnostic) =>\n flattenNativeDiagnosticMessage(chainedDiagnostic, depth + 1)\n ),\n ].join('\\n');\n\n/**\n * Checks files with the native API (`typescript` >= 7, Go port), whose root\n * export only exposes the version. It spawns the bundled `tsgo` binary, which\n * resolves the closest tsconfig of each file.\n */\nconst collectNativeTypeScriptErrors = (\n NativeAPI: typeof NativeTypeScriptAPI,\n filePaths: string[],\n baseDir: string\n): TypeScriptError[] => {\n const api = new NativeAPI({ cwd: baseDir });\n\n try {\n const snapshot = api.updateSnapshot({ openFiles: filePaths });\n\n try {\n return filePaths.flatMap((filePath) => {\n const program = snapshot.getDefaultProjectForFile(filePath)?.program;\n const sourceFile = program?.getSourceFile(filePath);\n\n if (!program || !sourceFile) return [];\n\n return [\n ...program.getSyntacticDiagnostics(filePath),\n ...program.getSemanticDiagnostics(filePath),\n ].map((diagnostic) => {\n const { line, character } = sourceFile.getLineAndCharacterOfPosition(\n diagnostic.pos\n );\n\n return {\n fileName: diagnostic.fileName ?? filePath,\n line,\n character,\n message: flattenNativeDiagnosticMessage(diagnostic),\n };\n });\n });\n } finally {\n snapshot.dispose();\n }\n } finally {\n api.close();\n }\n};\n\n/**\n * Resolves the installed TypeScript and returns its errors for the given\n * files, or `undefined` if TypeScript is not installed.\n */\nconst collectTypeScriptErrors = async (\n filePaths: string[],\n baseDir: string\n): Promise<TypeScriptError[] | undefined> => {\n let typescriptModule: ClassicTypeScriptModule;\n\n try {\n // Typed against the installed version, whose root may be version-only (>= 7)\n typescriptModule = (await import(\n 'typescript'\n )) as unknown as ClassicTypeScriptModule;\n } catch {\n return undefined;\n }\n\n const classicTypeScript = typescriptModule.createProgram\n ? typescriptModule\n : typescriptModule.default;\n\n if (classicTypeScript?.createProgram) {\n return collectClassicTypeScriptErrors(\n classicTypeScript as ClassicTypeScriptCompiler,\n filePaths,\n baseDir\n );\n }\n\n let nativeModule: { API: typeof NativeTypeScriptAPI };\n\n try {\n nativeModule = await import('typescript/unstable/sync');\n } catch {\n // Neither the classic nor the native API is available\n return undefined;\n }\n\n return collectNativeTypeScriptErrors(nativeModule.API, filePaths, baseDir);\n};\n\n/**\n * Type-checks content declaration files and logs errors as warnings.\n * Supports both the classic (`typescript` <= 6) and native (>= 7) compilers.\n */\nexport const logTypeScriptErrors = async (\n filePaths: string[],\n configuration: IntlayerConfig\n): Promise<void> => {\n const appLogger = getAppLogger(configuration);\n const baseDir = configuration.system?.baseDir ?? process.cwd();\n\n const filesToCheck = filePaths\n .filter((filePath) => CHECKABLE_EXTENSIONS.includes(extname(filePath)))\n .map((filePath) => resolve(baseDir, filePath));\n\n if (filesToCheck.length === 0) return;\n\n const typeScriptErrors = await collectTypeScriptErrors(filesToCheck, baseDir);\n\n typeScriptErrors?.forEach(({ fileName, line, character, message }) => {\n appLogger(\n `TS Error in ${fileName} (${line + 1},${character + 1}): ${message}`,\n { level: 'warn' }\n );\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"logTypeScriptErrors.cjs","names":["dirname","getAppLogger","extname","resolve"],"sources":["../../../src/loadDictionaries/logTypeScriptErrors.ts"],"sourcesContent":["import { dirname, extname, resolve } from 'node:path';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type {\n API as NativeTypeScriptAPI,\n Diagnostic as NativeTypeScriptDiagnostic,\n} from 'typescript/unstable/async';\n\n/** Diagnostic of the classic (TS <= 6) compiler API. */\ninterface ClassicTypeScriptDiagnostic {\n file?: {\n fileName: string;\n getLineAndCharacterOfPosition: (position: number) => {\n line: number;\n character: number;\n };\n };\n start?: number;\n messageText: string | { messageText: string };\n}\n\n/** Subset of the classic (TS <= 6) compiler API used here. */\ninterface ClassicTypeScriptCompiler {\n findConfigFile: (\n searchPath: string,\n fileExists: (path: string) => boolean,\n configName?: string\n ) => string | undefined;\n sys: {\n fileExists: (path: string) => boolean;\n readFile: (path: string, encoding?: string) => string | undefined;\n };\n parseConfigFileTextToJson: (\n fileName: string,\n jsonText: string\n ) => { config?: Record<string, unknown>; error?: unknown };\n parseJsonConfigFileContent: (\n json: unknown,\n host: unknown,\n basePath: string\n ) => {\n options: Record<string, unknown>;\n };\n createProgram: (\n rootNames: readonly string[],\n options: Record<string, unknown>\n ) => {\n getSourceFile: (fileName: string) => unknown;\n };\n getPreEmitDiagnostics: (\n program: unknown,\n sourceFile?: unknown\n ) => readonly ClassicTypeScriptDiagnostic[];\n flattenDiagnosticMessageText: (\n diagnostic: string | { messageText: string },\n newLine: string\n ) => string;\n}\n\n/** Root module of `typescript`, possibly wrapped in a CJS `default`. */\ntype ClassicTypeScriptModule = Partial<ClassicTypeScriptCompiler> & {\n default?: Partial<ClassicTypeScriptCompiler>;\n};\n\n/** Engine-agnostic diagnostic, with 0-based line and character. */\ninterface TypeScriptError {\n fileName: string;\n line: number;\n character: number;\n message: string;\n}\n\nconst CHECKABLE_EXTENSIONS = [\n '.ts',\n '.tsx',\n '.js',\n '.jsx',\n '.cjs',\n '.mjs',\n '.json',\n];\n\n/**\n * Checks files with the classic compiler API (`typescript` <= 6), which is\n * exposed from the package root.\n */\nconst collectClassicTypeScriptErrors = (\n typescript: ClassicTypeScriptCompiler,\n filePaths: string[],\n baseDir: string\n): TypeScriptError[] => {\n const configFileName = typescript.findConfigFile(\n baseDir,\n typescript.sys.fileExists,\n 'tsconfig.json'\n );\n\n let compilerOptions: Record<string, unknown> = {\n noEmit: true,\n allowJs: true,\n resolveJsonModule: true,\n };\n\n const configFileText = configFileName\n ? typescript.sys.readFile(configFileName)\n : undefined;\n\n if (configFileName && configFileText) {\n const configJson = typescript.parseConfigFileTextToJson(\n configFileName,\n configFileText\n );\n\n if (!configJson.error) {\n const parsedConfig = typescript.parseJsonConfigFileContent(\n configJson.config,\n typescript.sys,\n dirname(configFileName)\n );\n\n const { incremental, tsBuildInfoFile, ...restOptions } =\n parsedConfig.options;\n\n compilerOptions = { ...compilerOptions, ...restOptions, noEmit: true };\n }\n }\n\n const program = typescript.createProgram(filePaths, compilerOptions);\n\n return filePaths.flatMap((filePath) => {\n const sourceFile = program.getSourceFile(filePath);\n\n if (!sourceFile) return [];\n\n return typescript\n .getPreEmitDiagnostics(program, sourceFile)\n .flatMap((diagnostic) => {\n if (!diagnostic.file || diagnostic.start === undefined) return [];\n\n const { line, character } =\n diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);\n\n return {\n fileName: diagnostic.file.fileName,\n line,\n character,\n message: typescript.flattenDiagnosticMessageText(\n diagnostic.messageText,\n '\\n'\n ),\n };\n });\n });\n};\n\n/** Flattens a native diagnostic message chain, indenting nested messages. */\nconst flattenNativeDiagnosticMessage = (\n diagnostic: NativeTypeScriptDiagnostic,\n depth = 0\n): string =>\n [\n `${' '.repeat(depth)}${diagnostic.text}`,\n ...(diagnostic.messageChain ?? []).map((chainedDiagnostic) =>\n flattenNativeDiagnosticMessage(chainedDiagnostic, depth + 1)\n ),\n ].join('\\n');\n\n/**\n * Checks files with the native API (`typescript` >= 7, Go port), whose root\n * export only exposes the version. It spawns the bundled `tsgo` binary, which\n * resolves the closest tsconfig of each file.\n */\nconst collectNativeTypeScriptErrors = async (\n NativeAPI: typeof NativeTypeScriptAPI,\n filePaths: string[],\n baseDir: string\n): Promise<TypeScriptError[]> => {\n const api = new NativeAPI({ cwd: baseDir });\n\n try {\n const snapshot = await api.updateSnapshot({ openFiles: filePaths });\n\n try {\n const results = await Promise.all(\n filePaths.map(async (filePath) => {\n const defaultProject =\n await snapshot.getDefaultProjectForFile(filePath);\n const program = defaultProject?.program;\n\n if (!program) return [];\n\n const sourceFile = await program.getSourceFile(filePath);\n\n if (!sourceFile) return [];\n\n const syntacticDiagnostics =\n await program.getSyntacticDiagnostics(filePath);\n const semanticDiagnostics =\n await program.getSemanticDiagnostics(filePath);\n\n return [...syntacticDiagnostics, ...semanticDiagnostics].map(\n (diagnostic) => {\n const { line, character } =\n sourceFile.getLineAndCharacterOfPosition(diagnostic.pos);\n\n return {\n fileName: diagnostic.fileName ?? filePath,\n line,\n character,\n message: flattenNativeDiagnosticMessage(diagnostic),\n };\n }\n );\n })\n );\n\n return results.flat();\n } finally {\n await snapshot.dispose();\n }\n } finally {\n await api.close();\n }\n};\n\n/**\n * Resolves the installed TypeScript and returns its errors for the given\n * files, or `undefined` if TypeScript is not installed.\n */\nconst collectTypeScriptErrors = async (\n filePaths: string[],\n baseDir: string\n): Promise<TypeScriptError[] | undefined> => {\n let typescriptModule: ClassicTypeScriptModule;\n\n try {\n // Typed against the installed version, whose root may be version-only (>= 7)\n typescriptModule = (await import(\n 'typescript'\n )) as unknown as ClassicTypeScriptModule;\n } catch {\n return undefined;\n }\n\n const classicTypeScript = typescriptModule.createProgram\n ? typescriptModule\n : typescriptModule.default;\n\n if (classicTypeScript?.createProgram) {\n return collectClassicTypeScriptErrors(\n classicTypeScript as ClassicTypeScriptCompiler,\n filePaths,\n baseDir\n );\n }\n\n let nativeModule: { API: typeof NativeTypeScriptAPI };\n\n try {\n nativeModule = await import('typescript/unstable/async');\n } catch {\n try {\n nativeModule = (await import('typescript/unstable/sync')) as unknown as {\n API: typeof NativeTypeScriptAPI;\n };\n } catch {\n // Neither the classic nor the native API is available\n return undefined;\n }\n }\n\n return collectNativeTypeScriptErrors(nativeModule.API, filePaths, baseDir);\n};\n\n/**\n * Type-checks content declaration files and logs errors as warnings.\n * Supports both the classic (`typescript` <= 6) and native (>= 7) compilers.\n */\nexport const logTypeScriptErrors = async (\n filePaths: string[],\n configuration: IntlayerConfig\n): Promise<void> => {\n const appLogger = getAppLogger(configuration);\n const baseDir = configuration.system?.baseDir ?? process.cwd();\n\n const filesToCheck = filePaths\n .filter((filePath) => CHECKABLE_EXTENSIONS.includes(extname(filePath)))\n .map((filePath) => resolve(baseDir, filePath));\n\n if (filesToCheck.length === 0) return;\n\n const typeScriptErrors = await collectTypeScriptErrors(filesToCheck, baseDir);\n\n typeScriptErrors?.forEach(({ fileName, line, character, message }) => {\n appLogger(\n `TS Error in ${fileName} (${line + 1},${character + 1}): ${message}`,\n { level: 'warn' }\n );\n });\n};\n"],"mappings":"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"}
@@ -16,18 +16,20 @@ const isLocaleCode = (key) => allLocaleValues.has(key) || /^[a-z]{2}(-[A-Z]{2,4}
16
16
  /** Find the end index of the value following a `locale:` key. */
17
17
  const extractValueEnd = (text, valueStart) => {
18
18
  let cursor = valueStart;
19
- while (cursor < text.length && " \n\r".includes(text[cursor])) cursor++;
19
+ while (cursor < text.length && " \n\r".includes(text[cursor] ?? "")) cursor++;
20
20
  if (cursor >= text.length) return valueStart;
21
21
  const char = text[cursor];
22
+ if (!char) return valueStart;
22
23
  if (char === "{" || char === "[") {
23
24
  const endChar = char === "{" ? "}" : "]";
24
25
  let depth = 1;
25
26
  cursor++;
26
27
  while (cursor < text.length && depth > 0) {
27
- if (text[cursor] === char) depth++;
28
- else if (text[cursor] === endChar) depth--;
29
- else if (text[cursor] === "\"" || text[cursor] === "`") {
30
- const quote = text[cursor];
28
+ const c = text[cursor];
29
+ if (c === char) depth++;
30
+ else if (c === endChar) depth--;
31
+ else if (c === "\"" || c === "`") {
32
+ const quote = c;
31
33
  cursor++;
32
34
  while (cursor < text.length) {
33
35
  if (text[cursor] === "\\") {
@@ -74,7 +76,7 @@ const analyzeChunkLocaleContent = (text, baseCurrent) => {
74
76
  let match = regex.exec(text);
75
77
  while (match !== null) {
76
78
  const locale = match[1] ?? match[2];
77
- if (isLocaleCode(locale)) {
79
+ if (locale && isLocaleCode(locale)) {
78
80
  const valueStart = match.index + match[0].length;
79
81
  const valueEnd = extractValueEnd(text, valueStart);
80
82
  const valueSize = valueEnd - valueStart;
@@ -89,12 +91,22 @@ const analyzeChunkLocaleContent = (text, baseCurrent) => {
89
91
  }
90
92
  match = regex.exec(text);
91
93
  }
94
+ const languageOf = (candidate) => (candidate.locale.split("-")[0] ?? "").toLowerCase();
92
95
  const isI18nMatch = (idx) => {
93
- const base = candidates[idx].locale.split("-")[0].toLowerCase();
96
+ const currentCandidate = candidates[idx];
97
+ if (!currentCandidate) return false;
98
+ const base = languageOf(currentCandidate);
99
+ let hasOtherLanguage = false;
100
+ let hasCurrentLanguage = base === baseCurrent;
94
101
  for (let j = 0; j < candidates.length; j++) {
95
102
  if (j === idx) continue;
96
- if (Math.abs(candidates[j].position - candidates[idx].position) > LOCALE_CLUSTER_WINDOW) continue;
97
- if (candidates[j].locale.split("-")[0].toLowerCase() !== base) return true;
103
+ const neighbor = candidates[j];
104
+ if (!neighbor) continue;
105
+ if (Math.abs(neighbor.position - currentCandidate.position) > LOCALE_CLUSTER_WINDOW) continue;
106
+ const neighborLanguage = languageOf(neighbor);
107
+ if (neighborLanguage !== base) hasOtherLanguage = true;
108
+ if (neighborLanguage === baseCurrent) hasCurrentLanguage = true;
109
+ if (hasOtherLanguage && hasCurrentLanguage) return true;
98
110
  }
99
111
  return false;
100
112
  };
@@ -103,9 +115,11 @@ const analyzeChunkLocaleContent = (text, baseCurrent) => {
103
115
  let dictionariesFound = 0;
104
116
  for (let i = 0; i < candidates.length; i++) {
105
117
  if (!isI18nMatch(i)) continue;
106
- const { locale, valueSize } = candidates[i];
118
+ const candidate = candidates[i];
119
+ if (!candidate) continue;
120
+ const { locale, valueSize } = candidate;
107
121
  dictionariesFound++;
108
- if (locale.split("-")[0].toLowerCase() === baseCurrent) usedLocaleSize += valueSize;
122
+ if ((locale.split("-")[0] ?? "").toLowerCase() === baseCurrent) usedLocaleSize += valueSize;
109
123
  else unusedLocaleSize += valueSize;
110
124
  }
111
125
  return {
@@ -141,7 +155,7 @@ const analyzeBundleContent = (chunks, htmlContent, currentLocale, totalPageSize)
141
155
  pageStrings.forEach((text) => {
142
156
  renderedContentSize += require_scan_parseHtml.byteLength(text);
143
157
  });
144
- const baseCurrent = currentLocale.split("-")[0].toLowerCase();
158
+ const baseCurrent = (currentLocale.split("-")[0] ?? "").toLowerCase();
145
159
  const mainBundleChunks = [];
146
160
  const lazyBundleChunks = [];
147
161
  for (const chunk of chunks) {
@@ -1 +1 @@
1
- {"version":3,"file":"analyzeBundleContent.cjs","names":["ALL_LOCALES","extractVisibleTextStrings","byteLength"],"sources":["../../../src/scan/analyzeBundleContent.ts"],"sourcesContent":["import { ALL_LOCALES } from '@intlayer/types/allLocales';\nimport { byteLength, extractVisibleTextStrings } from './parseHtml';\nimport type {\n BundleChunkInput,\n BundleContentAnalysis,\n ChunkAnalysis,\n} from './types';\n\n/**\n * Detect and measure localized (i18n) content embedded in JavaScript bundles.\n *\n * This is a dependency-free port of the hosted backend bundle analyzer: it\n * estimates how many translation strings ship in each chunk and, of those, how\n * many belong to locales other than the one currently rendered (i.e. dead\n * weight that a build-time optimization could strip).\n */\n\nconst allLocaleValues = new Set(Object.values(ALL_LOCALES) as string[]);\n\nconst isLocaleCode = (key: string): boolean =>\n allLocaleValues.has(key) || /^[a-z]{2}(-[A-Z]{2,4})?$/.test(key);\n\n/** Find the end index of the value following a `locale:` key. */\nconst extractValueEnd = (text: string, valueStart: number): number => {\n let cursor = valueStart;\n while (cursor < text.length && ' \\t\\n\\r'.includes(text[cursor])) cursor++;\n if (cursor >= text.length) return valueStart;\n\n const char = text[cursor];\n if (char === '{' || char === '[') {\n const endChar = char === '{' ? '}' : ']';\n let depth = 1;\n cursor++;\n while (cursor < text.length && depth > 0) {\n if (text[cursor] === char) depth++;\n else if (text[cursor] === endChar) depth--;\n else if (text[cursor] === '\"' || text[cursor] === '`') {\n const quote = text[cursor];\n cursor++;\n while (cursor < text.length) {\n if (text[cursor] === '\\\\') {\n cursor += 2;\n continue;\n }\n if (text[cursor] === quote) break;\n cursor++;\n }\n }\n cursor++;\n }\n return cursor;\n }\n if (char === '\"' || char === '`') {\n const quote = char;\n cursor++;\n while (cursor < text.length) {\n if (text[cursor] === '\\\\') {\n cursor += 2;\n continue;\n }\n if (text[cursor] === quote) {\n cursor++;\n break;\n }\n cursor++;\n }\n return cursor;\n }\n const endMatch = text.slice(cursor).search(/[,}\\]\\s]/);\n return endMatch === -1 ? text.length : cursor + endMatch;\n};\n\n// Matches both quoted (\"en\":) and unquoted (en:) locale keys followed by {\n// Works for any i18n solution — intlayer, i18next, vue-i18n, FormatJS, etc.\nconst LOCALE_KEY_PATTERN =\n /(?:\"([a-z]{2}(?:-[A-Z]{2,4})?)\"|\\b([a-z]{2}(?:-[A-Z]{2,4})?)\\b)\\s*:\\s*(?=\\{)/g;\n\n// Maximum character gap between two locale key positions to consider them\n// part of the same i18n object. Large enough for big translation objects.\nconst LOCALE_CLUSTER_WINDOW = 10_000;\n\n// Returns false when a candidate value is clearly not i18n text:\n// - contains hex/control escape sequences (ANSI codes, binary data)\nconst looksLikeI18nContent = (valueText: string): boolean => {\n if (/\\\\x[0-9a-fA-F]{2}/.test(valueText)) return false;\n if (/\\\\u00[01][0-9a-fA-F]/.test(valueText)) return false;\n return true;\n};\n\ntype LocaleMatch = {\n locale: string;\n position: number;\n valueStart: number;\n valueEnd: number;\n valueSize: number;\n};\n\nconst analyzeChunkLocaleContent = (\n text: string,\n baseCurrent: string\n): {\n unusedLocaleSize: number;\n usedLocaleSize: number;\n dictionariesFound: number;\n} => {\n // Step 1: collect all candidate locale key matches\n const candidates: LocaleMatch[] = [];\n const regex = new RegExp(LOCALE_KEY_PATTERN.source, 'g');\n let match = regex.exec(text);\n\n while (match !== null) {\n const locale = match[1] ?? match[2];\n if (isLocaleCode(locale)) {\n const valueStart = match.index + match[0].length;\n const valueEnd = extractValueEnd(text, valueStart);\n const valueSize = valueEnd - valueStart;\n const valueText = text.slice(valueStart, valueEnd);\n if (valueSize >= 5 && looksLikeI18nContent(valueText)) {\n candidates.push({\n locale,\n position: match.index,\n valueStart,\n valueEnd,\n valueSize,\n });\n }\n }\n match = regex.exec(text);\n }\n\n // Step 2: a locale key is i18n content only if another locale key with a\n // DIFFERENT locale code exists within LOCALE_CLUSTER_WINDOW chars.\n const isI18nMatch = (idx: number): boolean => {\n const base = candidates[idx].locale.split('-')[0].toLowerCase();\n for (let j = 0; j < candidates.length; j++) {\n if (j === idx) continue;\n const dist = Math.abs(candidates[j].position - candidates[idx].position);\n if (dist > LOCALE_CLUSTER_WINDOW) continue;\n if (candidates[j].locale.split('-')[0].toLowerCase() !== base)\n return true;\n }\n return false;\n };\n\n let unusedLocaleSize = 0;\n let usedLocaleSize = 0;\n let dictionariesFound = 0;\n\n for (let i = 0; i < candidates.length; i++) {\n if (!isI18nMatch(i)) continue;\n\n const { locale, valueSize } = candidates[i];\n dictionariesFound++;\n\n if (locale.split('-')[0].toLowerCase() === baseCurrent) {\n usedLocaleSize += valueSize;\n } else {\n unusedLocaleSize += valueSize;\n }\n }\n\n return { unusedLocaleSize, usedLocaleSize, dictionariesFound };\n};\n\n/**\n * Analyze the locale weight of a page's JavaScript bundles.\n *\n * @param chunks - The fetched JavaScript chunks (main + lazy).\n * @param htmlContent - The page HTML, used to estimate rendered content size.\n * @param currentLocale - The locale currently rendered by the page.\n * @param totalPageSize - The total transferred bytes measured for the page.\n * @returns The aggregated {@link BundleContentAnalysis}.\n */\nexport const analyzeBundleContent = (\n chunks: BundleChunkInput[],\n htmlContent: string,\n currentLocale: string,\n totalPageSize: number\n): BundleContentAnalysis => {\n const empty: BundleContentAnalysis = {\n currentLocale,\n totalPageSize,\n renderedContentSize: 0,\n contentSize: 0,\n totalLocaleSize: 0,\n totalUnusedLocaleSize: 0,\n unusedPercentOfLocale: 0,\n mainBundleChunks: [],\n lazyBundleChunks: [],\n };\n\n if (!chunks.length && !htmlContent) return empty;\n\n // Rendered content size from HTML visible text (deduplicated).\n const pageStrings = new Set(extractVisibleTextStrings(htmlContent));\n\n let renderedContentSize = 0;\n pageStrings.forEach((text) => {\n renderedContentSize += byteLength(text);\n });\n\n const baseCurrent = currentLocale.split('-')[0].toLowerCase();\n\n const mainBundleChunks: ChunkAnalysis[] = [];\n const lazyBundleChunks: ChunkAnalysis[] = [];\n\n for (const chunk of chunks) {\n const { unusedLocaleSize, usedLocaleSize, dictionariesFound } =\n analyzeChunkLocaleContent(chunk.content, baseCurrent);\n\n const totalLocaleSize = unusedLocaleSize + usedLocaleSize;\n const analysis: ChunkAnalysis = {\n url: chunk.url,\n fileSize: byteLength(chunk.content),\n totalLocaleSize,\n unusedLocaleSize,\n usedLocaleSize,\n dictionariesFound,\n unusedPercent:\n totalLocaleSize > 0\n ? Math.round((unusedLocaleSize / totalLocaleSize) * 100)\n : 0,\n };\n\n if (chunk.isMainBundle) {\n mainBundleChunks.push(analysis);\n } else if (dictionariesFound > 0) {\n lazyBundleChunks.push(analysis);\n }\n }\n\n const totalUnusedLocaleSize =\n mainBundleChunks.reduce((sum, c) => sum + c.unusedLocaleSize, 0) +\n lazyBundleChunks.reduce((sum, c) => sum + c.unusedLocaleSize, 0);\n const totalLocaleSize =\n mainBundleChunks.reduce((sum, c) => sum + c.totalLocaleSize, 0) +\n lazyBundleChunks.reduce((sum, c) => sum + c.totalLocaleSize, 0);\n const contentSize = renderedContentSize + totalLocaleSize;\n\n const unusedPercentOfLocale =\n totalLocaleSize > 0\n ? Math.round((totalUnusedLocaleSize / totalLocaleSize) * 100)\n : 0;\n\n return {\n currentLocale,\n totalPageSize,\n renderedContentSize,\n contentSize,\n totalLocaleSize,\n totalUnusedLocaleSize,\n unusedPercentOfLocale,\n mainBundleChunks,\n lazyBundleChunks,\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"analyzeBundleContent.cjs","names":["ALL_LOCALES","extractVisibleTextStrings","byteLength"],"sources":["../../../src/scan/analyzeBundleContent.ts"],"sourcesContent":["import { ALL_LOCALES } from '@intlayer/types/allLocales';\nimport { byteLength, extractVisibleTextStrings } from './parseHtml';\nimport type {\n BundleChunkInput,\n BundleContentAnalysis,\n ChunkAnalysis,\n} from './types';\n\n/**\n * Detect and measure localized (i18n) content embedded in JavaScript bundles.\n *\n * This is a dependency-free port of the hosted backend bundle analyzer: it\n * estimates how many translation strings ship in each chunk and, of those, how\n * many belong to locales other than the one currently rendered (i.e. dead\n * weight that a build-time optimization could strip).\n */\n\nconst allLocaleValues = new Set(Object.values(ALL_LOCALES) as string[]);\n\nconst isLocaleCode = (key: string): boolean =>\n allLocaleValues.has(key) || /^[a-z]{2}(-[A-Z]{2,4})?$/.test(key);\n\n/** Find the end index of the value following a `locale:` key. */\nconst extractValueEnd = (text: string, valueStart: number): number => {\n let cursor = valueStart;\n\n while (cursor < text.length && ' \\t\\n\\r'.includes(text[cursor] ?? ''))\n cursor++;\n\n if (cursor >= text.length) return valueStart;\n\n const char = text[cursor];\n\n if (!char) return valueStart;\n\n if (char === '{' || char === '[') {\n const endChar = char === '{' ? '}' : ']';\n let depth = 1;\n cursor++;\n while (cursor < text.length && depth > 0) {\n const c = text[cursor];\n\n if (c === char) depth++;\n else if (c === endChar) depth--;\n else if (c === '\"' || c === '`') {\n const quote = c;\n cursor++;\n while (cursor < text.length) {\n if (text[cursor] === '\\\\') {\n cursor += 2;\n continue;\n }\n\n if (text[cursor] === quote) break;\n cursor++;\n }\n }\n cursor++;\n }\n return cursor;\n }\n\n if (char === '\"' || char === '`') {\n const quote = char;\n cursor++;\n while (cursor < text.length) {\n if (text[cursor] === '\\\\') {\n cursor += 2;\n continue;\n }\n\n if (text[cursor] === quote) {\n cursor++;\n break;\n }\n cursor++;\n }\n return cursor;\n }\n const endMatch = text.slice(cursor).search(/[,}\\]\\s]/);\n return endMatch === -1 ? text.length : cursor + endMatch;\n};\n\n// Matches both quoted (\"en\":) and unquoted (en:) locale keys followed by {\n// Works for any i18n solution — intlayer, i18next, vue-i18n, FormatJS, etc.\nconst LOCALE_KEY_PATTERN =\n /(?:\"([a-z]{2}(?:-[A-Z]{2,4})?)\"|\\b([a-z]{2}(?:-[A-Z]{2,4})?)\\b)\\s*:\\s*(?=\\{)/g;\n\n// Maximum character gap between two locale key positions to consider them\n// part of the same i18n object. Large enough for big translation objects.\nconst LOCALE_CLUSTER_WINDOW = 10_000;\n\n// Returns false when a candidate value is clearly not i18n text:\n// - contains hex/control escape sequences (ANSI codes, binary data)\nconst looksLikeI18nContent = (valueText: string): boolean => {\n if (/\\\\x[0-9a-fA-F]{2}/.test(valueText)) return false;\n\n if (/\\\\u00[01][0-9a-fA-F]/.test(valueText)) return false;\n return true;\n};\n\ntype LocaleMatch = {\n locale: string;\n position: number;\n valueStart: number;\n valueEnd: number;\n valueSize: number;\n};\n\nconst analyzeChunkLocaleContent = (\n text: string,\n baseCurrent: string\n): {\n unusedLocaleSize: number;\n usedLocaleSize: number;\n dictionariesFound: number;\n} => {\n // Step 1: collect all candidate locale key matches\n const candidates: LocaleMatch[] = [];\n const regex = new RegExp(LOCALE_KEY_PATTERN.source, 'g');\n let match = regex.exec(text);\n\n while (match !== null) {\n const locale = match[1] ?? match[2];\n\n if (locale && isLocaleCode(locale)) {\n const valueStart = match.index + match[0].length;\n const valueEnd = extractValueEnd(text, valueStart);\n const valueSize = valueEnd - valueStart;\n const valueText = text.slice(valueStart, valueEnd);\n\n if (valueSize >= 5 && looksLikeI18nContent(valueText)) {\n candidates.push({\n locale,\n position: match.index,\n valueStart,\n valueEnd,\n valueSize,\n });\n }\n }\n match = regex.exec(text);\n }\n\n // Step 2: a locale key is i18n content only if, within\n // LOCALE_CLUSTER_WINDOW chars, another locale key with a DIFFERENT language\n // exists AND the rendered locale is part of the cluster. The second rule\n // discards minified object keys that happen to be locale codes (`id:{`,\n // `as:{`, `to:{`…), which never come with the page locale next to them.\n const languageOf = (candidate: LocaleMatch): string =>\n (candidate.locale.split('-')[0] ?? '').toLowerCase();\n\n const isI18nMatch = (idx: number): boolean => {\n const currentCandidate = candidates[idx];\n\n if (!currentCandidate) return false;\n const base = languageOf(currentCandidate);\n let hasOtherLanguage = false;\n let hasCurrentLanguage = base === baseCurrent;\n for (let j = 0; j < candidates.length; j++) {\n if (j === idx) continue;\n const neighbor = candidates[j];\n\n if (!neighbor) continue;\n const dist = Math.abs(neighbor.position - currentCandidate.position);\n\n if (dist > LOCALE_CLUSTER_WINDOW) continue;\n const neighborLanguage = languageOf(neighbor);\n\n if (neighborLanguage !== base) hasOtherLanguage = true;\n\n if (neighborLanguage === baseCurrent) hasCurrentLanguage = true;\n\n if (hasOtherLanguage && hasCurrentLanguage) return true;\n }\n return false;\n };\n\n let unusedLocaleSize = 0;\n let usedLocaleSize = 0;\n let dictionariesFound = 0;\n\n for (let i = 0; i < candidates.length; i++) {\n if (!isI18nMatch(i)) continue;\n\n const candidate = candidates[i];\n\n if (!candidate) continue;\n const { locale, valueSize } = candidate;\n dictionariesFound++;\n\n if ((locale.split('-')[0] ?? '').toLowerCase() === baseCurrent) {\n usedLocaleSize += valueSize;\n } else {\n unusedLocaleSize += valueSize;\n }\n }\n\n return { unusedLocaleSize, usedLocaleSize, dictionariesFound };\n};\n\n/**\n * Analyze the locale weight of a page's JavaScript bundles.\n *\n * @param chunks - The fetched JavaScript chunks (main + lazy).\n * @param htmlContent - The page HTML, used to estimate rendered content size.\n * @param currentLocale - The locale currently rendered by the page.\n * @param totalPageSize - The total transferred bytes measured for the page.\n * @returns The aggregated {@link BundleContentAnalysis}.\n */\nexport const analyzeBundleContent = (\n chunks: BundleChunkInput[],\n htmlContent: string,\n currentLocale: string,\n totalPageSize: number\n): BundleContentAnalysis => {\n const empty: BundleContentAnalysis = {\n currentLocale,\n totalPageSize,\n renderedContentSize: 0,\n contentSize: 0,\n totalLocaleSize: 0,\n totalUnusedLocaleSize: 0,\n unusedPercentOfLocale: 0,\n mainBundleChunks: [],\n lazyBundleChunks: [],\n };\n\n if (!chunks.length && !htmlContent) return empty;\n\n // Rendered content size from HTML visible text (deduplicated).\n const pageStrings = new Set(extractVisibleTextStrings(htmlContent));\n\n let renderedContentSize = 0;\n pageStrings.forEach((text) => {\n renderedContentSize += byteLength(text);\n });\n\n const baseCurrent = (currentLocale.split('-')[0] ?? '').toLowerCase();\n\n const mainBundleChunks: ChunkAnalysis[] = [];\n const lazyBundleChunks: ChunkAnalysis[] = [];\n\n for (const chunk of chunks) {\n const { unusedLocaleSize, usedLocaleSize, dictionariesFound } =\n analyzeChunkLocaleContent(chunk.content, baseCurrent);\n\n const totalLocaleSize = unusedLocaleSize + usedLocaleSize;\n const analysis: ChunkAnalysis = {\n url: chunk.url,\n fileSize: byteLength(chunk.content),\n totalLocaleSize,\n unusedLocaleSize,\n usedLocaleSize,\n dictionariesFound,\n unusedPercent:\n totalLocaleSize > 0\n ? Math.round((unusedLocaleSize / totalLocaleSize) * 100)\n : 0,\n };\n\n if (chunk.isMainBundle) {\n mainBundleChunks.push(analysis);\n } else if (dictionariesFound > 0) {\n lazyBundleChunks.push(analysis);\n }\n }\n\n const totalUnusedLocaleSize =\n mainBundleChunks.reduce((sum, c) => sum + c.unusedLocaleSize, 0) +\n lazyBundleChunks.reduce((sum, c) => sum + c.unusedLocaleSize, 0);\n const totalLocaleSize =\n mainBundleChunks.reduce((sum, c) => sum + c.totalLocaleSize, 0) +\n lazyBundleChunks.reduce((sum, c) => sum + c.totalLocaleSize, 0);\n const contentSize = renderedContentSize + totalLocaleSize;\n\n const unusedPercentOfLocale =\n totalLocaleSize > 0\n ? Math.round((totalUnusedLocaleSize / totalLocaleSize) * 100)\n : 0;\n\n return {\n currentLocale,\n totalPageSize,\n renderedContentSize,\n contentSize,\n totalLocaleSize,\n totalUnusedLocaleSize,\n unusedPercentOfLocale,\n mainBundleChunks,\n lazyBundleChunks,\n };\n};\n"],"mappings":"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"}
@@ -23,10 +23,13 @@ const scoreRecord = {
23
23
  url_htmlDir: 3,
24
24
  url_hasCanonical: 10,
25
25
  url_hreflang: 9,
26
+ url_hreflangReciprocal: 8,
26
27
  url_hasLocalizedLinks: 8,
27
28
  url_hasXDefault: 7,
28
29
  url_allAnchorsLocalized: 6,
29
- url_currentLocale: 3,
30
+ url_hasLangSelector: 6,
31
+ url_currentLocale: 5,
32
+ url_ogLocale: 3,
30
33
  url_unusedBundleContent: 8
31
34
  };
32
35
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"calculateScore.cjs","names":[],"sources":["../../../src/scan/calculateScore.ts"],"sourcesContent":["/**\n * Mutualized SEO/i18n scoring logic.\n *\n * This module is the single source of truth for turning audit/scan events into\n * a weighted score. It is consumed both by the hosted backend SEO audit\n * (`apps/backend`) and by the `intlayer scan` CLI command, so it must stay free\n * of any server-only dependency (no Fastify, no Cheerio, no logger coupling).\n */\n\n/** Accumulated score across all checks that ran. */\nexport type Score = {\n /** Sum of points obtained (success = full, warning = half, error = none). */\n score: number;\n /** Maximum achievable points for the checks that ran. */\n totalScore: number;\n};\n\n/**\n * Minimal shape of an event needed to contribute to the score.\n *\n * The `type` is the check identifier. URL-scoped checks are suffixed with the\n * URL using a backslash separator (e.g. `url_htmlLang\\https://example.com`), so\n * only the part before the first backslash is used to look up the weight.\n */\nexport type ScorableEvent = {\n type?: string;\n status?: string;\n};\n\n/**\n * Resolve how many points an event contributes based on its status.\n * - `warning` → half of the weight\n * - `error` → none\n * - anything else (`success`, `started`, `done`, …) → full weight\n */\nconst scoreCheck = (score: number, event: ScorableEvent): number => {\n if (event.status === 'warning') return score / 2;\n if (event.status === 'error') return 0;\n return score;\n};\n\n/** Weight (in points) of every scorable check. */\nexport const scoreRecord = {\n robots_robotsPresent: 10,\n robots_noLocalizedUrlsForgotten: 8,\n sitemap_sitemapPresent: 10,\n sitemap_noLocalizedUrlsForgotten: 9,\n sitemap_hasAlternates: 8,\n sitemap_hasXDefault: 7,\n url_htmlLang: 9,\n url_htmlDir: 3,\n url_hasCanonical: 10,\n url_hreflang: 9,\n url_hasLocalizedLinks: 8,\n url_hasXDefault: 7,\n url_allAnchorsLocalized: 6,\n url_currentLocale: 3,\n url_unusedBundleContent: 8,\n} as const;\n\n/** Identifier of a scorable check (without the URL suffix). */\nexport type ScoreCheckType = keyof typeof scoreRecord;\n\n/**\n * Apply a single event to the running score, returning a new {@link Score}.\n * Unknown check types are ignored so the function is safe to call on every\n * emitted event.\n *\n * @param score - The current accumulated score.\n * @param event - The event to fold into the score.\n * @returns A new score with the event applied.\n */\nexport const mutateScore = (score: Score, event: ScorableEvent): Score => {\n const newScore: Score = { ...score };\n\n const typeWithoutUrl = event.type?.split('\\\\')[0];\n\n if (!typeWithoutUrl) {\n return newScore;\n }\n\n const scoreValue = scoreRecord[typeWithoutUrl as ScoreCheckType];\n\n if (typeof scoreValue === 'number') {\n newScore.score += scoreCheck(scoreValue, event);\n newScore.totalScore += scoreValue;\n }\n\n return newScore;\n};\n\n/**\n * Convert a raw {@link Score} into a 0–100 percentage.\n *\n * @param score - The accumulated score.\n * @returns The rounded percentage, or 0 when no check ran.\n */\nexport const toScorePercent = (score: Score): number =>\n Math.round(score.totalScore > 0 ? (score.score / score.totalScore) * 100 : 0);\n"],"mappings":";;;;;;;;AAmCA,MAAM,cAAc,OAAe,UAAiC;CAClE,IAAI,MAAM,WAAW,WAAW,OAAO,QAAQ;CAC/C,IAAI,MAAM,WAAW,SAAS,OAAO;CACrC,OAAO;AACT;;AAGA,MAAa,cAAc;CACzB,sBAAsB;CACtB,iCAAiC;CACjC,wBAAwB;CACxB,kCAAkC;CAClC,uBAAuB;CACvB,qBAAqB;CACrB,cAAc;CACd,aAAa;CACb,kBAAkB;CAClB,cAAc;CACd,uBAAuB;CACvB,iBAAiB;CACjB,yBAAyB;CACzB,mBAAmB;CACnB,yBAAyB;AAC3B;;;;;;;;;;AAcA,MAAa,eAAe,OAAc,UAAgC;CACxE,MAAM,WAAkB,EAAE,GAAG,MAAM;CAEnC,MAAM,iBAAiB,MAAM,MAAM,MAAM,IAAI,CAAC,CAAC;CAE/C,IAAI,CAAC,gBACH,OAAO;CAGT,MAAM,aAAa,YAAY;CAE/B,IAAI,OAAO,eAAe,UAAU;EAClC,SAAS,SAAS,WAAW,YAAY,KAAK;EAC9C,SAAS,cAAc;CACzB;CAEA,OAAO;AACT;;;;;;;AAQA,MAAa,kBAAkB,UAC7B,KAAK,MAAM,MAAM,aAAa,IAAK,MAAM,QAAQ,MAAM,aAAc,MAAM,CAAC"}
1
+ {"version":3,"file":"calculateScore.cjs","names":[],"sources":["../../../src/scan/calculateScore.ts"],"sourcesContent":["/**\n * Mutualized SEO/i18n scoring logic.\n *\n * This module is the single source of truth for turning audit/scan events into\n * a weighted score. It is consumed both by the hosted backend SEO audit\n * (`apps/backend`) and by the `intlayer scan` CLI command, so it must stay free\n * of any server-only dependency (no Fastify, no Cheerio, no logger coupling).\n */\n\n/** Accumulated score across all checks that ran. */\nexport type Score = {\n /** Sum of points obtained (success = full, warning = half, error = none). */\n score: number;\n /** Maximum achievable points for the checks that ran. */\n totalScore: number;\n};\n\n/**\n * Minimal shape of an event needed to contribute to the score.\n *\n * The `type` is the check identifier. URL-scoped checks are suffixed with the\n * URL using a backslash separator (e.g. `url_htmlLang\\https://example.com`), so\n * only the part before the first backslash is used to look up the weight.\n */\nexport type ScorableEvent = {\n type?: string;\n status?: string;\n};\n\n/**\n * Resolve how many points an event contributes based on its status.\n * - `warning` → half of the weight\n * - `error` → none\n * - anything else (`success`, `started`, `done`, …) → full weight\n */\nconst scoreCheck = (score: number, event: ScorableEvent): number => {\n if (event.status === 'warning') return score / 2;\n if (event.status === 'error') return 0;\n return score;\n};\n\n/** Weight (in points) of every scorable check. */\nexport const scoreRecord = {\n robots_robotsPresent: 10,\n robots_noLocalizedUrlsForgotten: 8,\n sitemap_sitemapPresent: 10,\n sitemap_noLocalizedUrlsForgotten: 9,\n sitemap_hasAlternates: 8,\n sitemap_hasXDefault: 7,\n url_htmlLang: 9,\n url_htmlDir: 3,\n url_hasCanonical: 10,\n url_hreflang: 9,\n url_hreflangReciprocal: 8,\n url_hasLocalizedLinks: 8,\n url_hasXDefault: 7,\n url_allAnchorsLocalized: 6,\n url_hasLangSelector: 6,\n url_currentLocale: 5,\n url_ogLocale: 3,\n url_unusedBundleContent: 8,\n} as const;\n\n/** Identifier of a scorable check (without the URL suffix). */\nexport type ScoreCheckType = keyof typeof scoreRecord;\n\n/**\n * Apply a single event to the running score, returning a new {@link Score}.\n * Unknown check types are ignored so the function is safe to call on every\n * emitted event.\n *\n * @param score - The current accumulated score.\n * @param event - The event to fold into the score.\n * @returns A new score with the event applied.\n */\nexport const mutateScore = (score: Score, event: ScorableEvent): Score => {\n const newScore: Score = { ...score };\n\n const typeWithoutUrl = event.type?.split('\\\\')[0];\n\n if (!typeWithoutUrl) {\n return newScore;\n }\n\n const scoreValue = scoreRecord[typeWithoutUrl as ScoreCheckType];\n\n if (typeof scoreValue === 'number') {\n newScore.score += scoreCheck(scoreValue, event);\n newScore.totalScore += scoreValue;\n }\n\n return newScore;\n};\n\n/**\n * Convert a raw {@link Score} into a 0–100 percentage.\n *\n * @param score - The accumulated score.\n * @returns The rounded percentage, or 0 when no check ran.\n */\nexport const toScorePercent = (score: Score): number =>\n Math.round(score.totalScore > 0 ? (score.score / score.totalScore) * 100 : 0);\n"],"mappings":";;;;;;;;AAmCA,MAAM,cAAc,OAAe,UAAiC;CAClE,IAAI,MAAM,WAAW,WAAW,OAAO,QAAQ;CAC/C,IAAI,MAAM,WAAW,SAAS,OAAO;CACrC,OAAO;AACT;;AAGA,MAAa,cAAc;CACzB,sBAAsB;CACtB,iCAAiC;CACjC,wBAAwB;CACxB,kCAAkC;CAClC,uBAAuB;CACvB,qBAAqB;CACrB,cAAc;CACd,aAAa;CACb,kBAAkB;CAClB,cAAc;CACd,wBAAwB;CACxB,uBAAuB;CACvB,iBAAiB;CACjB,yBAAyB;CACzB,qBAAqB;CACrB,mBAAmB;CACnB,cAAc;CACd,yBAAyB;AAC3B;;;;;;;;;;AAcA,MAAa,eAAe,OAAc,UAAgC;CACxE,MAAM,WAAkB,EAAE,GAAG,MAAM;CAEnC,MAAM,iBAAiB,MAAM,MAAM,MAAM,IAAI,CAAC,CAAC;CAE/C,IAAI,CAAC,gBACH,OAAO;CAGT,MAAM,aAAa,YAAY;CAE/B,IAAI,OAAO,eAAe,UAAU;EAClC,SAAS,SAAS,WAAW,YAAY,KAAK;EAC9C,SAAS,cAAc;CACzB;CAEA,OAAO;AACT;;;;;;;AAQA,MAAa,kBAAkB,UAC7B,KAAK,MAAM,MAAM,aAAa,IAAK,MAAM,QAAQ,MAAM,aAAc,MAAM,CAAC"}