@intlayer/chokidar 8.12.4 → 9.0.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/buildIntlayerDictionary/buildIntlayerDictionary.cjs +21 -4
- package/dist/cjs/buildIntlayerDictionary/buildIntlayerDictionary.cjs.map +1 -1
- package/dist/cjs/buildIntlayerDictionary/writeDynamicDictionary.cjs +94 -0
- package/dist/cjs/buildIntlayerDictionary/writeDynamicDictionary.cjs.map +1 -1
- package/dist/cjs/buildIntlayerDictionary/writeMergedDictionary.cjs +1 -1
- package/dist/cjs/buildIntlayerDictionary/writeMergedDictionary.cjs.map +1 -1
- package/dist/cjs/createType/createType.cjs.map +1 -1
- package/dist/cjs/init/index.cjs +63 -9
- package/dist/cjs/init/index.cjs.map +1 -1
- package/dist/cjs/init/utils/configManipulation.cjs +196 -0
- package/dist/cjs/init/utils/configManipulation.cjs.map +1 -1
- package/dist/cjs/init/utils/fileSystem.cjs +84 -0
- package/dist/cjs/init/utils/fileSystem.cjs.map +1 -1
- package/dist/cjs/init/utils/index.cjs +12 -0
- package/dist/cjs/init/utils/packageManager.cjs +187 -0
- package/dist/cjs/init/utils/packageManager.cjs.map +1 -0
- package/dist/cjs/scan/analyzeBundleContent.cjs +182 -0
- package/dist/cjs/scan/analyzeBundleContent.cjs.map +1 -0
- package/dist/cjs/scan/calculateScore.cjs +65 -0
- package/dist/cjs/scan/calculateScore.cjs.map +1 -0
- package/dist/cjs/scan/checks.cjs +274 -0
- package/dist/cjs/scan/checks.cjs.map +1 -0
- package/dist/cjs/scan/index.cjs +31 -0
- package/dist/cjs/scan/parseHtml.cjs +127 -0
- package/dist/cjs/scan/parseHtml.cjs.map +1 -0
- package/dist/cjs/scan/scanWebsite.cjs +205 -0
- package/dist/cjs/scan/scanWebsite.cjs.map +1 -0
- package/dist/cjs/scan/types.cjs +0 -0
- package/dist/esm/buildIntlayerDictionary/buildIntlayerDictionary.mjs +22 -5
- package/dist/esm/buildIntlayerDictionary/buildIntlayerDictionary.mjs.map +1 -1
- package/dist/esm/buildIntlayerDictionary/writeDynamicDictionary.mjs +93 -1
- package/dist/esm/buildIntlayerDictionary/writeDynamicDictionary.mjs.map +1 -1
- package/dist/esm/buildIntlayerDictionary/writeMergedDictionary.mjs +2 -2
- package/dist/esm/buildIntlayerDictionary/writeMergedDictionary.mjs.map +1 -1
- package/dist/esm/createType/createType.mjs.map +1 -1
- package/dist/esm/init/index.mjs +65 -11
- package/dist/esm/init/index.mjs.map +1 -1
- package/dist/esm/init/utils/configManipulation.mjs +190 -1
- package/dist/esm/init/utils/configManipulation.mjs.map +1 -1
- package/dist/esm/init/utils/fileSystem.mjs +83 -1
- package/dist/esm/init/utils/fileSystem.mjs.map +1 -1
- package/dist/esm/init/utils/index.mjs +4 -3
- package/dist/esm/init/utils/packageManager.mjs +183 -0
- package/dist/esm/init/utils/packageManager.mjs.map +1 -0
- package/dist/esm/scan/analyzeBundleContent.mjs +180 -0
- package/dist/esm/scan/analyzeBundleContent.mjs.map +1 -0
- package/dist/esm/scan/calculateScore.mjs +61 -0
- package/dist/esm/scan/calculateScore.mjs.map +1 -0
- package/dist/esm/scan/checks.mjs +265 -0
- package/dist/esm/scan/checks.mjs.map +1 -0
- package/dist/esm/scan/index.mjs +7 -0
- package/dist/esm/scan/parseHtml.mjs +115 -0
- package/dist/esm/scan/parseHtml.mjs.map +1 -0
- package/dist/esm/scan/scanWebsite.mjs +203 -0
- package/dist/esm/scan/scanWebsite.mjs.map +1 -0
- package/dist/esm/scan/types.mjs +0 -0
- package/dist/types/buildIntlayerDictionary/buildIntlayerDictionary.d.ts.map +1 -1
- package/dist/types/buildIntlayerDictionary/writeDynamicDictionary.d.ts +31 -4
- package/dist/types/buildIntlayerDictionary/writeDynamicDictionary.d.ts.map +1 -1
- package/dist/types/buildIntlayerDictionary/writeMergedDictionary.d.ts +13 -3
- package/dist/types/buildIntlayerDictionary/writeMergedDictionary.d.ts.map +1 -1
- package/dist/types/createType/createType.d.ts +3 -3
- package/dist/types/createType/createType.d.ts.map +1 -1
- package/dist/types/formatDictionary.d.ts +9 -2
- package/dist/types/formatDictionary.d.ts.map +1 -1
- package/dist/types/init/index.d.ts.map +1 -1
- package/dist/types/init/utils/configManipulation.d.ts +42 -1
- package/dist/types/init/utils/configManipulation.d.ts.map +1 -1
- package/dist/types/init/utils/fileSystem.d.ts +31 -1
- package/dist/types/init/utils/fileSystem.d.ts.map +1 -1
- package/dist/types/init/utils/index.d.ts +4 -3
- package/dist/types/init/utils/packageManager.d.ts +59 -0
- package/dist/types/init/utils/packageManager.d.ts.map +1 -0
- package/dist/types/intlayer/dist/types/index.d.ts +4 -0
- package/dist/types/scan/analyzeBundleContent.d.ts +16 -0
- package/dist/types/scan/analyzeBundleContent.d.ts.map +1 -0
- package/dist/types/scan/calculateScore.d.ts +65 -0
- package/dist/types/scan/calculateScore.d.ts.map +1 -0
- package/dist/types/scan/checks.d.ts +38 -0
- package/dist/types/scan/checks.d.ts.map +1 -0
- package/dist/types/scan/index.d.ts +7 -0
- package/dist/types/scan/parseHtml.d.ts +54 -0
- package/dist/types/scan/parseHtml.d.ts.map +1 -0
- package/dist/types/scan/scanWebsite.d.ts +18 -0
- package/dist/types/scan/scanWebsite.d.ts.map +1 -0
- package/dist/types/scan/types.d.ts +76 -0
- package/dist/types/scan/types.d.ts.map +1 -0
- package/package.json +17 -9
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
import { EXCLUDED_PATHS } from "@intlayer/config/defaultValues";
|
|
5
|
+
import { ALL_LOCALES } from "@intlayer/types/allLocales";
|
|
3
6
|
|
|
4
7
|
//#region src/init/utils/fileSystem.ts
|
|
5
8
|
/**
|
|
@@ -29,7 +32,86 @@ const ensureDirectory = async (rootDir, dirPath) => {
|
|
|
29
32
|
await mkdir(join(rootDir, dirPath), { recursive: true });
|
|
30
33
|
} catch {}
|
|
31
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* Set of all known locale string values from the intlayer Locales registry
|
|
37
|
+
* (e.g. `'en'`, `'fr'`, `'zh-TW'`). Keyed by the exact locale string so
|
|
38
|
+
* `.has()` lookups are O(1) and common short directory names (`src`, `lib`,
|
|
39
|
+
* `app`, …) are not mistaken for locales.
|
|
40
|
+
*/
|
|
41
|
+
const ALL_LOCALE_VALUES = new Set(Object.values(ALL_LOCALES));
|
|
42
|
+
/**
|
|
43
|
+
* Returns true when `segment` matches a known BCP-47 locale identifier, e.g.
|
|
44
|
+
* `en`, `fr`, `zh-TW`, `pt-BR`, `en-US`.
|
|
45
|
+
*/
|
|
46
|
+
const isLocaleSegment = (segment) => ALL_LOCALE_VALUES.has(segment);
|
|
47
|
+
/** JSON filenames that are never locale translation files. */
|
|
48
|
+
const KNOWN_CONFIG_FILENAMES = new Set([
|
|
49
|
+
"package.json",
|
|
50
|
+
"tsconfig.json",
|
|
51
|
+
"jsconfig.json",
|
|
52
|
+
"biome.json",
|
|
53
|
+
"turbo.json",
|
|
54
|
+
"lerna.json",
|
|
55
|
+
"vercel.json",
|
|
56
|
+
"netlify.json",
|
|
57
|
+
"babel.config.json",
|
|
58
|
+
"jest.config.json",
|
|
59
|
+
"vitest.config.json",
|
|
60
|
+
".eslintrc.json",
|
|
61
|
+
".prettierrc.json"
|
|
62
|
+
]);
|
|
63
|
+
/**
|
|
64
|
+
* Scans the project for JSON files and determines whether locale files are
|
|
65
|
+
* organised as `{base}/{locale}/{key}.json` (nested) or `{base}/{locale}.json`
|
|
66
|
+
* (flat). Returns the most likely source template, or `null` when no locale
|
|
67
|
+
* JSON files are found.
|
|
68
|
+
*
|
|
69
|
+
* The returned `template` contains `${locale}` and `${key}` as **literal**
|
|
70
|
+
* placeholder strings so it can be embedded inside a JS template literal.
|
|
71
|
+
*/
|
|
72
|
+
const detectJsonLocalePattern = async (rootDir) => {
|
|
73
|
+
const files = await fg("**/*.json", {
|
|
74
|
+
cwd: rootDir,
|
|
75
|
+
ignore: EXCLUDED_PATHS,
|
|
76
|
+
absolute: false,
|
|
77
|
+
onlyFiles: true
|
|
78
|
+
});
|
|
79
|
+
const nestedBasePaths = [];
|
|
80
|
+
const flatBasePaths = [];
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
const parts = file.split("/");
|
|
83
|
+
const filename = parts[parts.length - 1] ?? "";
|
|
84
|
+
if (KNOWN_CONFIG_FILENAMES.has(filename)) continue;
|
|
85
|
+
if (parts.length >= 3) {
|
|
86
|
+
if (isLocaleSegment(parts[parts.length - 2] ?? "")) nestedBasePaths.push(parts.slice(0, -2).join("/") || ".");
|
|
87
|
+
}
|
|
88
|
+
if (parts.length >= 2) {
|
|
89
|
+
if (isLocaleSegment(filename.slice(0, -5))) flatBasePaths.push(parts.slice(0, -1).join("/") || ".");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (nestedBasePaths.length === 0 && flatBasePaths.length === 0) return null;
|
|
93
|
+
/**
|
|
94
|
+
* Returns the path prefix that appears most frequently among the matches,
|
|
95
|
+
* formatted as a relative path suitable for a source template.
|
|
96
|
+
*/
|
|
97
|
+
const mostFrequentPrefix = (paths) => {
|
|
98
|
+
const counts = paths.reduce((accumulator, path) => {
|
|
99
|
+
accumulator[path] = (accumulator[path] ?? 0) + 1;
|
|
100
|
+
return accumulator;
|
|
101
|
+
}, {});
|
|
102
|
+
const basePath = Object.entries(counts).sort(([, a], [, b]) => b - a)[0]?.[0] ?? ".";
|
|
103
|
+
return basePath === "." ? "." : `./${basePath}`;
|
|
104
|
+
};
|
|
105
|
+
if (nestedBasePaths.length >= flatBasePaths.length) return {
|
|
106
|
+
type: "nested",
|
|
107
|
+
template: `${mostFrequentPrefix(nestedBasePaths)}/\${locale}/\${key}.json`
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
type: "flat",
|
|
111
|
+
template: `${mostFrequentPrefix(flatBasePaths)}/\${locale}.json`
|
|
112
|
+
};
|
|
113
|
+
};
|
|
32
114
|
|
|
33
115
|
//#endregion
|
|
34
|
-
export { ensureDirectory, exists, readFileFromRoot, writeFileToRoot };
|
|
116
|
+
export { detectJsonLocalePattern, ensureDirectory, exists, readFileFromRoot, writeFileToRoot };
|
|
35
117
|
//# sourceMappingURL=fileSystem.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileSystem.mjs","names":[],"sources":["../../../../src/init/utils/fileSystem.ts"],"sourcesContent":["import { access, mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\n/**\n * Helper to check if a file exists\n */\nexport const exists = async (rootDir: string, filePath: string) => {\n try {\n await access(join(rootDir, filePath));\n return true;\n } catch {\n return false;\n }\n};\n\n/**\n * Helper to read a file\n */\nexport const readFileFromRoot = async (rootDir: string, filePath: string) =>\n await readFile(join(rootDir, filePath), 'utf8');\n\n/**\n * Helper to write a file\n */\nexport const writeFileToRoot = async (\n rootDir: string,\n filePath: string,\n content: string\n) => await writeFile(join(rootDir, filePath), content, 'utf8');\n\n/**\n * Helper to ensure a directory exists\n */\nexport const ensureDirectory = async (rootDir: string, dirPath: string) => {\n try {\n await mkdir(join(rootDir, dirPath), { recursive: true });\n } catch {\n // Directory already exists or could not be created\n }\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"fileSystem.mjs","names":[],"sources":["../../../../src/init/utils/fileSystem.ts"],"sourcesContent":["import { access, mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { EXCLUDED_PATHS } from '@intlayer/config/defaultValues';\nimport { ALL_LOCALES } from '@intlayer/types/allLocales';\nimport fg from 'fast-glob';\n\n/**\n * Helper to check if a file exists\n */\nexport const exists = async (rootDir: string, filePath: string) => {\n try {\n await access(join(rootDir, filePath));\n return true;\n } catch {\n return false;\n }\n};\n\n/**\n * Helper to read a file\n */\nexport const readFileFromRoot = async (rootDir: string, filePath: string) =>\n await readFile(join(rootDir, filePath), 'utf8');\n\n/**\n * Helper to write a file\n */\nexport const writeFileToRoot = async (\n rootDir: string,\n filePath: string,\n content: string\n) => await writeFile(join(rootDir, filePath), content, 'utf8');\n\n/**\n * Helper to ensure a directory exists\n */\nexport const ensureDirectory = async (rootDir: string, dirPath: string) => {\n try {\n await mkdir(join(rootDir, dirPath), { recursive: true });\n } catch {\n // Directory already exists or could not be created\n }\n};\n\n/**\n * Pattern type for locale JSON file organisation.\n * - 'nested': files are at `{base}/{locale}/{key}.json`\n * - 'flat': files are at `{base}/{locale}.json`\n */\nexport type JsonLocalePatternType = 'nested' | 'flat';\n\n/**\n * Detected locale JSON file pattern and the corresponding source template.\n * `template` uses `${locale}` and `${key}` as literal placeholders (not JS\n * expressions) so it can be embedded directly in a template-literal string.\n */\nexport type JsonLocalePattern = {\n type: JsonLocalePatternType;\n /**\n * Source path template for syncJSON `source` option.\n * Example nested: `./locales/${locale}/${key}.json`\n * Example flat: `./locales/${locale}.json`\n */\n template: string;\n};\n\n/**\n * Set of all known locale string values from the intlayer Locales registry\n * (e.g. `'en'`, `'fr'`, `'zh-TW'`). Keyed by the exact locale string so\n * `.has()` lookups are O(1) and common short directory names (`src`, `lib`,\n * `app`, …) are not mistaken for locales.\n */\nconst ALL_LOCALE_VALUES = new Set<string>(Object.values(ALL_LOCALES));\n\n/**\n * Returns true when `segment` matches a known BCP-47 locale identifier, e.g.\n * `en`, `fr`, `zh-TW`, `pt-BR`, `en-US`.\n */\nconst isLocaleSegment = (segment: string): boolean =>\n ALL_LOCALE_VALUES.has(segment);\n\n/** JSON filenames that are never locale translation files. */\nconst KNOWN_CONFIG_FILENAMES = new Set([\n 'package.json',\n 'tsconfig.json',\n 'jsconfig.json',\n 'biome.json',\n 'turbo.json',\n 'lerna.json',\n 'vercel.json',\n 'netlify.json',\n 'babel.config.json',\n 'jest.config.json',\n 'vitest.config.json',\n '.eslintrc.json',\n '.prettierrc.json',\n]);\n\n/**\n * Scans the project for JSON files and determines whether locale files are\n * organised as `{base}/{locale}/{key}.json` (nested) or `{base}/{locale}.json`\n * (flat). Returns the most likely source template, or `null` when no locale\n * JSON files are found.\n *\n * The returned `template` contains `${locale}` and `${key}` as **literal**\n * placeholder strings so it can be embedded inside a JS template literal.\n */\nexport const detectJsonLocalePattern = async (\n rootDir: string\n): Promise<JsonLocalePattern | null> => {\n const files = await fg('**/*.json', {\n cwd: rootDir,\n ignore: EXCLUDED_PATHS,\n absolute: false,\n onlyFiles: true,\n });\n\n const nestedBasePaths: string[] = [];\n const flatBasePaths: string[] = [];\n\n for (const file of files) {\n const parts = file.split('/');\n const filename = parts[parts.length - 1] ?? '';\n\n if (KNOWN_CONFIG_FILENAMES.has(filename)) continue;\n\n // Nested: …/{locale}/{key}.json — parent directory is a locale code\n if (parts.length >= 3) {\n const localeDir = parts[parts.length - 2] ?? '';\n if (isLocaleSegment(localeDir)) {\n nestedBasePaths.push(parts.slice(0, -2).join('/') || '.');\n }\n }\n\n // Flat: …/{locale}.json — filename (without extension) is a locale code\n if (parts.length >= 2) {\n const baseName = filename.slice(0, -5); // strip \".json\"\n if (isLocaleSegment(baseName)) {\n flatBasePaths.push(parts.slice(0, -1).join('/') || '.');\n }\n }\n }\n\n if (nestedBasePaths.length === 0 && flatBasePaths.length === 0) {\n return null;\n }\n\n /**\n * Returns the path prefix that appears most frequently among the matches,\n * formatted as a relative path suitable for a source template.\n */\n const mostFrequentPrefix = (paths: string[]): string => {\n const counts = paths.reduce<Record<string, number>>((accumulator, path) => {\n accumulator[path] = (accumulator[path] ?? 0) + 1;\n return accumulator;\n }, {});\n const topEntry = Object.entries(counts).sort(([, a], [, b]) => b - a)[0];\n const basePath = topEntry?.[0] ?? '.';\n return basePath === '.' ? '.' : `./${basePath}`;\n };\n\n if (nestedBasePaths.length >= flatBasePaths.length) {\n const prefix = mostFrequentPrefix(nestedBasePaths);\n return {\n type: 'nested',\n // Literal ${locale} and ${key} — not evaluated here, used in template literals\n template: `${prefix}/\\${locale}/\\${key}.json`,\n };\n }\n\n const prefix = mostFrequentPrefix(flatBasePaths);\n return {\n type: 'flat',\n template: `${prefix}/\\${locale}.json`,\n };\n};\n"],"mappings":";;;;;;;;;;AASA,MAAa,SAAS,OAAO,SAAiB,aAAqB;AACjE,KAAI;AACF,QAAM,OAAO,KAAK,SAAS,SAAS,CAAC;AACrC,SAAO;SACD;AACN,SAAO;;;;;;AAOX,MAAa,mBAAmB,OAAO,SAAiB,aACtD,MAAM,SAAS,KAAK,SAAS,SAAS,EAAE,OAAO;;;;AAKjD,MAAa,kBAAkB,OAC7B,SACA,UACA,YACG,MAAM,UAAU,KAAK,SAAS,SAAS,EAAE,SAAS,OAAO;;;;AAK9D,MAAa,kBAAkB,OAAO,SAAiB,YAAoB;AACzE,KAAI;AACF,QAAM,MAAM,KAAK,SAAS,QAAQ,EAAE,EAAE,WAAW,MAAM,CAAC;SAClD;;;;;;;;AAiCV,MAAM,oBAAoB,IAAI,IAAY,OAAO,OAAO,YAAY,CAAC;;;;;AAMrE,MAAM,mBAAmB,YACvB,kBAAkB,IAAI,QAAQ;;AAGhC,MAAM,yBAAyB,IAAI,IAAI;CACrC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;;;AAWF,MAAa,0BAA0B,OACrC,YACsC;CACtC,MAAM,QAAQ,MAAM,GAAG,aAAa;EAClC,KAAK;EACL,QAAQ;EACR,UAAU;EACV,WAAW;EACZ,CAAC;CAEF,MAAM,kBAA4B,EAAE;CACpC,MAAM,gBAA0B,EAAE;AAElC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,QAAQ,KAAK,MAAM,IAAI;EAC7B,MAAM,WAAW,MAAM,MAAM,SAAS,MAAM;AAE5C,MAAI,uBAAuB,IAAI,SAAS,CAAE;AAG1C,MAAI,MAAM,UAAU,GAElB;OAAI,gBADc,MAAM,MAAM,SAAS,MAAM,GACf,CAC5B,iBAAgB,KAAK,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,IAAI,IAAI,IAAI;;AAK7D,MAAI,MAAM,UAAU,GAElB;OAAI,gBADa,SAAS,MAAM,GAAG,GACP,CAAC,CAC3B,eAAc,KAAK,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,IAAI,IAAI,IAAI;;;AAK7D,KAAI,gBAAgB,WAAW,KAAK,cAAc,WAAW,EAC3D,QAAO;;;;;CAOT,MAAM,sBAAsB,UAA4B;EACtD,MAAM,SAAS,MAAM,QAAgC,aAAa,SAAS;AACzE,eAAY,SAAS,YAAY,SAAS,KAAK;AAC/C,UAAO;KACN,EAAE,CAAC;EAEN,MAAM,WADW,OAAO,QAAQ,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,OAAO,IAAI,EAAE,CAAC,KAC1C,MAAM;AAClC,SAAO,aAAa,MAAM,MAAM,KAAK;;AAGvC,KAAI,gBAAgB,UAAU,cAAc,OAE1C,QAAO;EACL,MAAM;EAEN,UAAU,GAJG,mBAAmB,gBAIb,CAAC;EACrB;AAIH,QAAO;EACL,MAAM;EACN,UAAU,GAHG,mBAAmB,cAGb,CAAC;EACrB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { updateAstroConfig, updateNextConfig, updateNuxtConfig, updateViteConfig } from "./configManipulation.mjs";
|
|
2
|
-
import { ensureDirectory, exists, readFileFromRoot, writeFileToRoot } from "./fileSystem.mjs";
|
|
1
|
+
import { updateAstroConfig, updateIntlayerConfigWithSyncPlugin, updateNextConfig, updateNextConfigForNextI18next, updateNextConfigForNextIntl, updateNextConfigForNextTranslate, updateNuxtConfig, updateNuxtConfigForNuxtjsI18n, updateViteConfig, updateViteConfigForCompatPlugin, updateViteConfigForVueI18n } from "./configManipulation.mjs";
|
|
2
|
+
import { detectJsonLocalePattern, ensureDirectory, exists, readFileFromRoot, writeFileToRoot } from "./fileSystem.mjs";
|
|
3
3
|
import { parseJSONWithComments } from "./jsonParser.mjs";
|
|
4
|
+
import { detectMissingIntlayerPackages, detectPackageManager, installPackages } from "./packageManager.mjs";
|
|
4
5
|
import { findTsConfigFiles } from "./tsConfig.mjs";
|
|
5
6
|
|
|
6
|
-
export { ensureDirectory, exists, findTsConfigFiles, parseJSONWithComments, readFileFromRoot, updateAstroConfig, updateNextConfig, updateNuxtConfig, updateViteConfig, writeFileToRoot };
|
|
7
|
+
export { detectJsonLocalePattern, detectMissingIntlayerPackages, detectPackageManager, ensureDirectory, exists, findTsConfigFiles, installPackages, parseJSONWithComments, readFileFromRoot, updateAstroConfig, updateIntlayerConfigWithSyncPlugin, updateNextConfig, updateNextConfigForNextI18next, updateNextConfigForNextIntl, updateNextConfigForNextTranslate, updateNuxtConfig, updateNuxtConfigForNuxtjsI18n, updateViteConfig, updateViteConfigForCompatPlugin, updateViteConfigForVueI18n, writeFileToRoot };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
//#region src/init/utils/packageManager.ts
|
|
6
|
+
/**
|
|
7
|
+
* Detects the package manager in use by checking for lock files in the
|
|
8
|
+
* project root. Falls back to npm when no lock file is found.
|
|
9
|
+
*/
|
|
10
|
+
const detectPackageManager = (rootDir) => {
|
|
11
|
+
if (existsSync(join(rootDir, "bun.lock")) || existsSync(join(rootDir, "bun.lockb"))) return "bun";
|
|
12
|
+
if (existsSync(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
13
|
+
if (existsSync(join(rootDir, "yarn.lock"))) return "yarn";
|
|
14
|
+
return "npm";
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Returns the install command for the given package manager and package list.
|
|
18
|
+
*/
|
|
19
|
+
const buildInstallCommand = (packageManager, packages) => {
|
|
20
|
+
const packageList = packages.join(" ");
|
|
21
|
+
switch (packageManager) {
|
|
22
|
+
case "bun": return `bun add ${packageList}`;
|
|
23
|
+
case "pnpm": return `pnpm add ${packageList}`;
|
|
24
|
+
case "yarn": return `yarn add ${packageList}`;
|
|
25
|
+
case "npm": return `npm install ${packageList}`;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Analyzes existing project dependencies to determine which intlayer packages
|
|
30
|
+
* are missing and what syncJSON configuration to inject when compat i18n
|
|
31
|
+
* libraries are present.
|
|
32
|
+
*/
|
|
33
|
+
const detectMissingIntlayerPackages = (allDependencies) => {
|
|
34
|
+
const packagesToInstall = [];
|
|
35
|
+
let compatSyncConfig;
|
|
36
|
+
let compatVitePluginConfig;
|
|
37
|
+
const isInstalled = (packageName) => Boolean(allDependencies[packageName]);
|
|
38
|
+
const addIfMissing = (packageName) => {
|
|
39
|
+
if (!isInstalled(packageName)) packagesToInstall.push(packageName);
|
|
40
|
+
};
|
|
41
|
+
addIfMissing("intlayer");
|
|
42
|
+
if (isInstalled("next")) addIfMissing("next-intlayer");
|
|
43
|
+
else if (isInstalled("react")) addIfMissing("react-intlayer");
|
|
44
|
+
if (isInstalled("svelte")) addIfMissing("svelte-intlayer");
|
|
45
|
+
if (isInstalled("solid-js")) addIfMissing("solid-intlayer");
|
|
46
|
+
if (isInstalled("@angular/core")) addIfMissing("angular-intlayer");
|
|
47
|
+
if (isInstalled("vue")) addIfMissing("vue-intlayer");
|
|
48
|
+
if (isInstalled("vite")) addIfMissing("vite-intlayer");
|
|
49
|
+
if (isInstalled("next-intl")) {
|
|
50
|
+
addIfMissing("@intlayer/next-intl");
|
|
51
|
+
compatSyncConfig ??= {
|
|
52
|
+
format: "icu",
|
|
53
|
+
sourceTemplate: "./locales/${locale}/${key}.json"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (isInstalled("next-i18next")) {
|
|
57
|
+
addIfMissing("@intlayer/next-i18next");
|
|
58
|
+
compatSyncConfig ??= {
|
|
59
|
+
format: "i18next",
|
|
60
|
+
sourceTemplate: "./src/locales/${locale}/${key}.json"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (isInstalled("next-translate")) {
|
|
64
|
+
addIfMissing("@intlayer/next-translate");
|
|
65
|
+
compatSyncConfig ??= {
|
|
66
|
+
format: "i18next",
|
|
67
|
+
sourceTemplate: "./locales/${locale}/${key}.json"
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (isInstalled("i18next")) {
|
|
71
|
+
addIfMissing("@intlayer/i18next");
|
|
72
|
+
compatSyncConfig ??= {
|
|
73
|
+
format: "i18next",
|
|
74
|
+
sourceTemplate: "./src/locales/${locale}/${key}.json"
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (isInstalled("react-i18next")) {
|
|
78
|
+
addIfMissing("@intlayer/react-i18next");
|
|
79
|
+
compatSyncConfig ??= {
|
|
80
|
+
format: "i18next",
|
|
81
|
+
sourceTemplate: "./src/locales/${locale}/${key}.json"
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (isInstalled("vue-i18n")) {
|
|
85
|
+
addIfMissing("@intlayer/vue-i18n");
|
|
86
|
+
compatSyncConfig ??= {
|
|
87
|
+
format: "vue-i18n",
|
|
88
|
+
sourceTemplate: "./locales/${locale}/${key}.json"
|
|
89
|
+
};
|
|
90
|
+
compatVitePluginConfig ??= {
|
|
91
|
+
pluginFunctionName: "vueI18nVitePlugin",
|
|
92
|
+
pluginPackageSource: "@intlayer/vue-i18n/plugin"
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (isInstalled("react-intl")) {
|
|
96
|
+
addIfMissing("@intlayer/react-intl");
|
|
97
|
+
compatSyncConfig ??= {
|
|
98
|
+
format: "icu",
|
|
99
|
+
sourceTemplate: "./src/i18n/${locale}.json"
|
|
100
|
+
};
|
|
101
|
+
compatVitePluginConfig ??= {
|
|
102
|
+
pluginFunctionName: "reactIntlVitePlugin",
|
|
103
|
+
pluginPackageSource: "@intlayer/react-intl/plugin"
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (isInstalled("@ngneat/transloco")) {
|
|
107
|
+
addIfMissing("@intlayer/transloco");
|
|
108
|
+
compatVitePluginConfig ??= {
|
|
109
|
+
pluginFunctionName: "translocoVitePlugin",
|
|
110
|
+
pluginPackageSource: "@intlayer/transloco/plugin"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (isInstalled("svelte-i18n")) {
|
|
114
|
+
addIfMissing("@intlayer/svelte-i18n");
|
|
115
|
+
compatSyncConfig ??= {
|
|
116
|
+
format: "i18next",
|
|
117
|
+
sourceTemplate: "./src/locales/${locale}.json"
|
|
118
|
+
};
|
|
119
|
+
compatVitePluginConfig ??= {
|
|
120
|
+
pluginFunctionName: "svelteI18nVitePlugin",
|
|
121
|
+
pluginPackageSource: "@intlayer/svelte-i18n/plugin"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (isInstalled("node-polyglot")) {
|
|
125
|
+
addIfMissing("@intlayer/polyglot");
|
|
126
|
+
compatVitePluginConfig ??= {
|
|
127
|
+
pluginFunctionName: "polyglotVitePlugin",
|
|
128
|
+
pluginPackageSource: "@intlayer/polyglot/plugin"
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (isInstalled("@nuxtjs/i18n")) {
|
|
132
|
+
addIfMissing("@intlayer/nuxtjs-i18n");
|
|
133
|
+
compatSyncConfig ??= {
|
|
134
|
+
format: "vue-i18n",
|
|
135
|
+
sourceTemplate: "./locales/${locale}/${key}.json"
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (isInstalled("@ngx-translate/core")) {
|
|
139
|
+
addIfMissing("@intlayer/ngx-translate");
|
|
140
|
+
compatSyncConfig ??= {
|
|
141
|
+
format: "i18next",
|
|
142
|
+
sourceTemplate: "./assets/i18n/${locale}.json"
|
|
143
|
+
};
|
|
144
|
+
compatVitePluginConfig ??= {
|
|
145
|
+
pluginFunctionName: "ngxTranslateVitePlugin",
|
|
146
|
+
pluginPackageSource: "@intlayer/ngx-translate/plugin"
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (isInstalled("@lingui/core") || isInstalled("@lingui/react")) {
|
|
150
|
+
addIfMissing("@intlayer/lingui");
|
|
151
|
+
compatVitePluginConfig ??= {
|
|
152
|
+
pluginFunctionName: "linguiVitePlugin",
|
|
153
|
+
pluginPackageSource: "@intlayer/lingui/plugin"
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (isInstalled("i18n-js")) {
|
|
157
|
+
addIfMissing("@intlayer/i18n-js");
|
|
158
|
+
compatVitePluginConfig ??= {
|
|
159
|
+
pluginFunctionName: "i18nJsVitePlugin",
|
|
160
|
+
pluginPackageSource: "@intlayer/i18n-js/plugin"
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (compatSyncConfig) addIfMissing("@intlayer/sync-json-plugin");
|
|
164
|
+
return {
|
|
165
|
+
packagesToInstall,
|
|
166
|
+
compatSyncConfig,
|
|
167
|
+
compatVitePluginConfig
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Runs the package install command synchronously.
|
|
172
|
+
* Throws if the install process exits with a non-zero code.
|
|
173
|
+
*/
|
|
174
|
+
const installPackages = (rootDir, packages, packageManager) => {
|
|
175
|
+
execSync(buildInstallCommand(packageManager, packages), {
|
|
176
|
+
cwd: rootDir,
|
|
177
|
+
stdio: "inherit"
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
//#endregion
|
|
182
|
+
export { detectMissingIntlayerPackages, detectPackageManager, installPackages };
|
|
183
|
+
//# sourceMappingURL=packageManager.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packageManager.mjs","names":[],"sources":["../../../../src/init/utils/packageManager.ts"],"sourcesContent":["import { execSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** Package managers supported for dependency installation. */\nexport type PackageManager = 'bun' | 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Configuration for the syncJSON plugin injected into intlayer.config\n * when a compat i18n library is detected.\n */\nexport type CompatSyncConfig = {\n /** JSON format matching the compat library's conventions. */\n format: 'icu' | 'i18next' | 'vue-i18n';\n /**\n * Source path template using ${locale} and ${key} placeholders.\n * Rendered as a template literal in the generated config.\n */\n sourceTemplate: string;\n};\n\n/**\n * Configuration for injecting a compat vite plugin into vite.config.\n * The plugin replaces the generic `intlayer` plugin for libraries that\n * require alias injection (e.g. `vue-i18n` → `@intlayer/vue-i18n`).\n */\nexport type CompatVitePluginConfig = {\n /** Exported function name from the plugin package, e.g. `'vueI18nVitePlugin'`. */\n pluginFunctionName: string;\n /** Import path for the plugin package, e.g. `'@intlayer/vue-i18n/plugin'`. */\n pluginPackageSource: string;\n};\n\n/** Result of analyzing project dependencies for intlayer package gaps. */\nexport type IntlayerPackageAnalysis = {\n /** Intlayer packages that are referenced but not yet installed. */\n packagesToInstall: string[];\n /**\n * syncJSON plugin configuration to inject when a compat i18n library is\n * detected. Undefined when no compat library is present or format is not\n * yet implemented.\n */\n compatSyncConfig: CompatSyncConfig | undefined;\n /**\n * Vite config plugin to inject when a vite-based compat library is\n * detected. Undefined for Next.js/Nuxt-only compat libs or when no compat\n * library requires alias injection.\n */\n compatVitePluginConfig: CompatVitePluginConfig | undefined;\n};\n\n/**\n * Detects the package manager in use by checking for lock files in the\n * project root. Falls back to npm when no lock file is found.\n */\nexport const detectPackageManager = (rootDir: string): PackageManager => {\n if (\n existsSync(join(rootDir, 'bun.lock')) ||\n existsSync(join(rootDir, 'bun.lockb'))\n ) {\n return 'bun';\n }\n if (existsSync(join(rootDir, 'pnpm-lock.yaml'))) {\n return 'pnpm';\n }\n if (existsSync(join(rootDir, 'yarn.lock'))) {\n return 'yarn';\n }\n return 'npm';\n};\n\n/**\n * Returns the install command for the given package manager and package list.\n */\nconst buildInstallCommand = (\n packageManager: PackageManager,\n packages: string[]\n): string => {\n const packageList = packages.join(' ');\n switch (packageManager) {\n case 'bun':\n return `bun add ${packageList}`;\n case 'pnpm':\n return `pnpm add ${packageList}`;\n case 'yarn':\n return `yarn add ${packageList}`;\n case 'npm':\n return `npm install ${packageList}`;\n }\n};\n\n/**\n * Analyzes existing project dependencies to determine which intlayer packages\n * are missing and what syncJSON configuration to inject when compat i18n\n * libraries are present.\n */\nexport const detectMissingIntlayerPackages = (\n allDependencies: Record<string, string>\n): IntlayerPackageAnalysis => {\n const packagesToInstall: string[] = [];\n let compatSyncConfig: CompatSyncConfig | undefined;\n let compatVitePluginConfig: CompatVitePluginConfig | undefined;\n\n const isInstalled = (packageName: string): boolean =>\n Boolean(allDependencies[packageName]);\n\n const addIfMissing = (packageName: string): void => {\n if (!isInstalled(packageName)) {\n packagesToInstall.push(packageName);\n }\n };\n\n // Core package — always required\n addIfMissing('intlayer');\n\n // Framework-specific runtime integrations\n if (isInstalled('next')) {\n addIfMissing('next-intlayer');\n } else if (isInstalled('react')) {\n addIfMissing('react-intlayer');\n }\n\n if (isInstalled('svelte')) {\n addIfMissing('svelte-intlayer');\n }\n\n if (isInstalled('solid-js')) {\n addIfMissing('solid-intlayer');\n }\n\n if (isInstalled('@angular/core')) {\n addIfMissing('angular-intlayer');\n }\n\n if (isInstalled('vue')) {\n addIfMissing('vue-intlayer');\n }\n\n if (isInstalled('vite')) {\n addIfMissing('vite-intlayer');\n }\n\n // -------------------------------------------------------------------------\n // Compat adapters for existing i18n libraries.\n //\n // Detection order matters: more specific libraries are checked first so that\n // `compatSyncConfig ??=` and `compatVitePluginConfig ??=` capture the most\n // relevant match. Libraries that only affect the Next.js or Nuxt config do\n // not set `compatVitePluginConfig` (handled separately in init/index.ts).\n // Libraries whose JSON format is not yet supported leave `compatSyncConfig`\n // undefined so no syncJSON plugin is injected.\n // -------------------------------------------------------------------------\n\n // next-intl — next.js only, ICU format\n if (isInstalled('next-intl')) {\n addIfMissing('@intlayer/next-intl');\n compatSyncConfig ??= {\n format: 'icu',\n sourceTemplate: './locales/${locale}/${key}.json',\n };\n // next config handled via updateNextConfigForNextIntl in init/index.ts\n }\n\n // next-i18next — next.js only, i18next JSON format\n if (isInstalled('next-i18next')) {\n addIfMissing('@intlayer/next-i18next');\n compatSyncConfig ??= {\n format: 'i18next',\n sourceTemplate: './src/locales/${locale}/${key}.json',\n };\n // next config handled via updateNextConfigForNextI18next in init/index.ts\n }\n\n // next-translate — next.js only, i18next-style flat-namespace JSON\n if (isInstalled('next-translate')) {\n addIfMissing('@intlayer/next-translate');\n compatSyncConfig ??= {\n format: 'i18next',\n sourceTemplate: './locales/${locale}/${key}.json',\n };\n // next config handled via updateNextConfigForNextTranslate in init/index.ts\n }\n\n // i18next — explicit import from @intlayer/i18next (no alias injection needed)\n if (isInstalled('i18next')) {\n addIfMissing('@intlayer/i18next');\n compatSyncConfig ??= {\n format: 'i18next',\n sourceTemplate: './src/locales/${locale}/${key}.json',\n };\n }\n\n // react-i18next — explicit import from @intlayer/react-i18next (no alias)\n if (isInstalled('react-i18next')) {\n addIfMissing('@intlayer/react-i18next');\n compatSyncConfig ??= {\n format: 'i18next',\n sourceTemplate: './src/locales/${locale}/${key}.json',\n };\n }\n\n // vue-i18n — vite alias injection required\n if (isInstalled('vue-i18n')) {\n addIfMissing('@intlayer/vue-i18n');\n compatSyncConfig ??= {\n format: 'vue-i18n',\n sourceTemplate: './locales/${locale}/${key}.json',\n };\n compatVitePluginConfig ??= {\n pluginFunctionName: 'vueI18nVitePlugin',\n pluginPackageSource: '@intlayer/vue-i18n/plugin',\n };\n }\n\n // react-intl — vite alias injection required, ICU format\n if (isInstalled('react-intl')) {\n addIfMissing('@intlayer/react-intl');\n compatSyncConfig ??= {\n format: 'icu',\n sourceTemplate: './src/i18n/${locale}.json',\n };\n compatVitePluginConfig ??= {\n pluginFunctionName: 'reactIntlVitePlugin',\n pluginPackageSource: '@intlayer/react-intl/plugin',\n };\n }\n\n // @ngneat/transloco — vite alias injection required\n // @todo syncJSON format not yet implemented for transloco\n if (isInstalled('@ngneat/transloco')) {\n addIfMissing('@intlayer/transloco');\n compatVitePluginConfig ??= {\n pluginFunctionName: 'translocoVitePlugin',\n pluginPackageSource: '@intlayer/transloco/plugin',\n };\n }\n\n // svelte-i18n — vite alias injection required, flat JSON (i18next-compatible)\n if (isInstalled('svelte-i18n')) {\n addIfMissing('@intlayer/svelte-i18n');\n compatSyncConfig ??= {\n format: 'i18next',\n sourceTemplate: './src/locales/${locale}.json',\n };\n compatVitePluginConfig ??= {\n pluginFunctionName: 'svelteI18nVitePlugin',\n pluginPackageSource: '@intlayer/svelte-i18n/plugin',\n };\n }\n\n // node-polyglot — vite alias injection required\n // @todo syncJSON format not yet implemented for polyglot\n if (isInstalled('node-polyglot')) {\n addIfMissing('@intlayer/polyglot');\n compatVitePluginConfig ??= {\n pluginFunctionName: 'polyglotVitePlugin',\n pluginPackageSource: '@intlayer/polyglot/plugin',\n };\n }\n\n // @nuxtjs/i18n — nuxt module (no vite plugin), vue-i18n JSON format\n if (isInstalled('@nuxtjs/i18n')) {\n addIfMissing('@intlayer/nuxtjs-i18n');\n compatSyncConfig ??= {\n format: 'vue-i18n',\n sourceTemplate: './locales/${locale}/${key}.json',\n };\n // nuxt config handled via updateNuxtConfigForNuxtjsI18n in init/index.ts\n }\n\n // @ngx-translate/core — vite alias injection required, flat JSON (i18next)\n if (isInstalled('@ngx-translate/core')) {\n addIfMissing('@intlayer/ngx-translate');\n compatSyncConfig ??= {\n format: 'i18next',\n sourceTemplate: './assets/i18n/${locale}.json',\n };\n compatVitePluginConfig ??= {\n pluginFunctionName: 'ngxTranslateVitePlugin',\n pluginPackageSource: '@intlayer/ngx-translate/plugin',\n };\n }\n\n // @lingui/core — vite alias injection required\n // @todo syncJSON format not yet implemented for lingui (uses PO files)\n if (isInstalled('@lingui/core') || isInstalled('@lingui/react')) {\n addIfMissing('@intlayer/lingui');\n compatVitePluginConfig ??= {\n pluginFunctionName: 'linguiVitePlugin',\n pluginPackageSource: '@intlayer/lingui/plugin',\n };\n }\n\n // i18n-js — vite alias injection required\n // @todo syncJSON format not yet implemented for i18n-js\n if (isInstalled('i18n-js')) {\n addIfMissing('@intlayer/i18n-js');\n compatVitePluginConfig ??= {\n pluginFunctionName: 'i18nJsVitePlugin',\n pluginPackageSource: '@intlayer/i18n-js/plugin',\n };\n }\n\n if (compatSyncConfig) {\n addIfMissing('@intlayer/sync-json-plugin');\n }\n\n return { packagesToInstall, compatSyncConfig, compatVitePluginConfig };\n};\n\n/**\n * Runs the package install command synchronously.\n * Throws if the install process exits with a non-zero code.\n */\nexport const installPackages = (\n rootDir: string,\n packages: string[],\n packageManager: PackageManager\n): void => {\n const command = buildInstallCommand(packageManager, packages);\n execSync(command, { cwd: rootDir, stdio: 'inherit' });\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { byteLength, extractVisibleTextStrings } from "./parseHtml.mjs";
|
|
2
|
+
import { ALL_LOCALES } from "@intlayer/types/allLocales";
|
|
3
|
+
|
|
4
|
+
//#region src/scan/analyzeBundleContent.ts
|
|
5
|
+
/**
|
|
6
|
+
* Detect and measure localized (i18n) content embedded in JavaScript bundles.
|
|
7
|
+
*
|
|
8
|
+
* This is a dependency-free port of the hosted backend bundle analyzer: it
|
|
9
|
+
* estimates how many translation strings ship in each chunk and, of those, how
|
|
10
|
+
* many belong to locales other than the one currently rendered (i.e. dead
|
|
11
|
+
* weight that a build-time optimization could strip).
|
|
12
|
+
*/
|
|
13
|
+
const allLocaleValues = new Set(Object.values(ALL_LOCALES));
|
|
14
|
+
const isLocaleCode = (key) => allLocaleValues.has(key) || /^[a-z]{2}(-[A-Z]{2,4})?$/.test(key);
|
|
15
|
+
/** Find the end index of the value following a `locale:` key. */
|
|
16
|
+
const extractValueEnd = (text, valueStart) => {
|
|
17
|
+
let cursor = valueStart;
|
|
18
|
+
while (cursor < text.length && " \n\r".includes(text[cursor])) cursor++;
|
|
19
|
+
if (cursor >= text.length) return valueStart;
|
|
20
|
+
const char = text[cursor];
|
|
21
|
+
if (char === "{" || char === "[") {
|
|
22
|
+
const endChar = char === "{" ? "}" : "]";
|
|
23
|
+
let depth = 1;
|
|
24
|
+
cursor++;
|
|
25
|
+
while (cursor < text.length && depth > 0) {
|
|
26
|
+
if (text[cursor] === char) depth++;
|
|
27
|
+
else if (text[cursor] === endChar) depth--;
|
|
28
|
+
else if (text[cursor] === "\"" || text[cursor] === "`") {
|
|
29
|
+
const quote = text[cursor];
|
|
30
|
+
cursor++;
|
|
31
|
+
while (cursor < text.length) {
|
|
32
|
+
if (text[cursor] === "\\") {
|
|
33
|
+
cursor += 2;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (text[cursor] === quote) break;
|
|
37
|
+
cursor++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
cursor++;
|
|
41
|
+
}
|
|
42
|
+
return cursor;
|
|
43
|
+
}
|
|
44
|
+
if (char === "\"" || char === "`") {
|
|
45
|
+
const quote = char;
|
|
46
|
+
cursor++;
|
|
47
|
+
while (cursor < text.length) {
|
|
48
|
+
if (text[cursor] === "\\") {
|
|
49
|
+
cursor += 2;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (text[cursor] === quote) {
|
|
53
|
+
cursor++;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
cursor++;
|
|
57
|
+
}
|
|
58
|
+
return cursor;
|
|
59
|
+
}
|
|
60
|
+
const endMatch = text.slice(cursor).search(/[,}\]\s]/);
|
|
61
|
+
return endMatch === -1 ? text.length : cursor + endMatch;
|
|
62
|
+
};
|
|
63
|
+
const LOCALE_KEY_PATTERN = /(?:"([a-z]{2}(?:-[A-Z]{2,4})?)"|\b([a-z]{2}(?:-[A-Z]{2,4})?)\b)\s*:\s*(?=\{)/g;
|
|
64
|
+
const LOCALE_CLUSTER_WINDOW = 1e4;
|
|
65
|
+
const looksLikeI18nContent = (valueText) => {
|
|
66
|
+
if (/\\x[0-9a-fA-F]{2}/.test(valueText)) return false;
|
|
67
|
+
if (/\\u00[01][0-9a-fA-F]/.test(valueText)) return false;
|
|
68
|
+
return true;
|
|
69
|
+
};
|
|
70
|
+
const analyzeChunkLocaleContent = (text, baseCurrent) => {
|
|
71
|
+
const candidates = [];
|
|
72
|
+
const regex = new RegExp(LOCALE_KEY_PATTERN.source, "g");
|
|
73
|
+
let match = regex.exec(text);
|
|
74
|
+
while (match !== null) {
|
|
75
|
+
const locale = match[1] ?? match[2];
|
|
76
|
+
if (isLocaleCode(locale)) {
|
|
77
|
+
const valueStart = match.index + match[0].length;
|
|
78
|
+
const valueEnd = extractValueEnd(text, valueStart);
|
|
79
|
+
const valueSize = valueEnd - valueStart;
|
|
80
|
+
const valueText = text.slice(valueStart, valueEnd);
|
|
81
|
+
if (valueSize >= 5 && looksLikeI18nContent(valueText)) candidates.push({
|
|
82
|
+
locale,
|
|
83
|
+
position: match.index,
|
|
84
|
+
valueStart,
|
|
85
|
+
valueEnd,
|
|
86
|
+
valueSize
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
match = regex.exec(text);
|
|
90
|
+
}
|
|
91
|
+
const isI18nMatch = (idx) => {
|
|
92
|
+
const base = candidates[idx].locale.split("-")[0].toLowerCase();
|
|
93
|
+
for (let j = 0; j < candidates.length; j++) {
|
|
94
|
+
if (j === idx) continue;
|
|
95
|
+
if (Math.abs(candidates[j].position - candidates[idx].position) > LOCALE_CLUSTER_WINDOW) continue;
|
|
96
|
+
if (candidates[j].locale.split("-")[0].toLowerCase() !== base) return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
};
|
|
100
|
+
let unusedLocaleSize = 0;
|
|
101
|
+
let usedLocaleSize = 0;
|
|
102
|
+
let dictionariesFound = 0;
|
|
103
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
104
|
+
if (!isI18nMatch(i)) continue;
|
|
105
|
+
const { locale, valueSize } = candidates[i];
|
|
106
|
+
dictionariesFound++;
|
|
107
|
+
if (locale.split("-")[0].toLowerCase() === baseCurrent) usedLocaleSize += valueSize;
|
|
108
|
+
else unusedLocaleSize += valueSize;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
unusedLocaleSize,
|
|
112
|
+
usedLocaleSize,
|
|
113
|
+
dictionariesFound
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Analyze the locale weight of a page's JavaScript bundles.
|
|
118
|
+
*
|
|
119
|
+
* @param chunks - The fetched JavaScript chunks (main + lazy).
|
|
120
|
+
* @param htmlContent - The page HTML, used to estimate rendered content size.
|
|
121
|
+
* @param currentLocale - The locale currently rendered by the page.
|
|
122
|
+
* @param totalPageSize - The total transferred bytes measured for the page.
|
|
123
|
+
* @returns The aggregated {@link BundleContentAnalysis}.
|
|
124
|
+
*/
|
|
125
|
+
const analyzeBundleContent = (chunks, htmlContent, currentLocale, totalPageSize) => {
|
|
126
|
+
const empty = {
|
|
127
|
+
currentLocale,
|
|
128
|
+
totalPageSize,
|
|
129
|
+
renderedContentSize: 0,
|
|
130
|
+
contentSize: 0,
|
|
131
|
+
totalLocaleSize: 0,
|
|
132
|
+
totalUnusedLocaleSize: 0,
|
|
133
|
+
unusedPercentOfLocale: 0,
|
|
134
|
+
mainBundleChunks: [],
|
|
135
|
+
lazyBundleChunks: []
|
|
136
|
+
};
|
|
137
|
+
if (!chunks.length && !htmlContent) return empty;
|
|
138
|
+
const pageStrings = new Set(extractVisibleTextStrings(htmlContent));
|
|
139
|
+
let renderedContentSize = 0;
|
|
140
|
+
pageStrings.forEach((text) => {
|
|
141
|
+
renderedContentSize += byteLength(text);
|
|
142
|
+
});
|
|
143
|
+
const baseCurrent = currentLocale.split("-")[0].toLowerCase();
|
|
144
|
+
const mainBundleChunks = [];
|
|
145
|
+
const lazyBundleChunks = [];
|
|
146
|
+
for (const chunk of chunks) {
|
|
147
|
+
const { unusedLocaleSize, usedLocaleSize, dictionariesFound } = analyzeChunkLocaleContent(chunk.content, baseCurrent);
|
|
148
|
+
const totalLocaleSize = unusedLocaleSize + usedLocaleSize;
|
|
149
|
+
const analysis = {
|
|
150
|
+
url: chunk.url,
|
|
151
|
+
fileSize: byteLength(chunk.content),
|
|
152
|
+
totalLocaleSize,
|
|
153
|
+
unusedLocaleSize,
|
|
154
|
+
usedLocaleSize,
|
|
155
|
+
dictionariesFound,
|
|
156
|
+
unusedPercent: totalLocaleSize > 0 ? Math.round(unusedLocaleSize / totalLocaleSize * 100) : 0
|
|
157
|
+
};
|
|
158
|
+
if (chunk.isMainBundle) mainBundleChunks.push(analysis);
|
|
159
|
+
else if (dictionariesFound > 0) lazyBundleChunks.push(analysis);
|
|
160
|
+
}
|
|
161
|
+
const totalUnusedLocaleSize = mainBundleChunks.reduce((sum, c) => sum + c.unusedLocaleSize, 0) + lazyBundleChunks.reduce((sum, c) => sum + c.unusedLocaleSize, 0);
|
|
162
|
+
const totalLocaleSize = mainBundleChunks.reduce((sum, c) => sum + c.totalLocaleSize, 0) + lazyBundleChunks.reduce((sum, c) => sum + c.totalLocaleSize, 0);
|
|
163
|
+
const contentSize = renderedContentSize + totalLocaleSize;
|
|
164
|
+
const unusedPercentOfLocale = totalLocaleSize > 0 ? Math.round(totalUnusedLocaleSize / totalLocaleSize * 100) : 0;
|
|
165
|
+
return {
|
|
166
|
+
currentLocale,
|
|
167
|
+
totalPageSize,
|
|
168
|
+
renderedContentSize,
|
|
169
|
+
contentSize,
|
|
170
|
+
totalLocaleSize,
|
|
171
|
+
totalUnusedLocaleSize,
|
|
172
|
+
unusedPercentOfLocale,
|
|
173
|
+
mainBundleChunks,
|
|
174
|
+
lazyBundleChunks
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
//#endregion
|
|
179
|
+
export { analyzeBundleContent };
|
|
180
|
+
//# sourceMappingURL=analyzeBundleContent.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzeBundleContent.mjs","names":[],"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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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/scan/calculateScore.ts
|
|
2
|
+
/**
|
|
3
|
+
* Resolve how many points an event contributes based on its status.
|
|
4
|
+
* - `warning` → half of the weight
|
|
5
|
+
* - `error` → none
|
|
6
|
+
* - anything else (`success`, `started`, `done`, …) → full weight
|
|
7
|
+
*/
|
|
8
|
+
const scoreCheck = (score, event) => {
|
|
9
|
+
if (event.status === "warning") return score / 2;
|
|
10
|
+
if (event.status === "error") return 0;
|
|
11
|
+
return score;
|
|
12
|
+
};
|
|
13
|
+
/** Weight (in points) of every scorable check. */
|
|
14
|
+
const scoreRecord = {
|
|
15
|
+
robots_robotsPresent: 10,
|
|
16
|
+
robots_noLocalizedUrlsForgotten: 8,
|
|
17
|
+
sitemap_sitemapPresent: 10,
|
|
18
|
+
sitemap_noLocalizedUrlsForgotten: 9,
|
|
19
|
+
sitemap_hasAlternates: 8,
|
|
20
|
+
sitemap_hasXDefault: 7,
|
|
21
|
+
url_htmlLang: 9,
|
|
22
|
+
url_htmlDir: 3,
|
|
23
|
+
url_hasCanonical: 10,
|
|
24
|
+
url_hreflang: 9,
|
|
25
|
+
url_hasLocalizedLinks: 8,
|
|
26
|
+
url_hasXDefault: 7,
|
|
27
|
+
url_allAnchorsLocalized: 6,
|
|
28
|
+
url_currentLocale: 3,
|
|
29
|
+
url_unusedBundleContent: 8
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Apply a single event to the running score, returning a new {@link Score}.
|
|
33
|
+
* Unknown check types are ignored so the function is safe to call on every
|
|
34
|
+
* emitted event.
|
|
35
|
+
*
|
|
36
|
+
* @param score - The current accumulated score.
|
|
37
|
+
* @param event - The event to fold into the score.
|
|
38
|
+
* @returns A new score with the event applied.
|
|
39
|
+
*/
|
|
40
|
+
const mutateScore = (score, event) => {
|
|
41
|
+
const newScore = { ...score };
|
|
42
|
+
const typeWithoutUrl = event.type?.split("\\")[0];
|
|
43
|
+
if (!typeWithoutUrl) return newScore;
|
|
44
|
+
const scoreValue = scoreRecord[typeWithoutUrl];
|
|
45
|
+
if (typeof scoreValue === "number") {
|
|
46
|
+
newScore.score += scoreCheck(scoreValue, event);
|
|
47
|
+
newScore.totalScore += scoreValue;
|
|
48
|
+
}
|
|
49
|
+
return newScore;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Convert a raw {@link Score} into a 0–100 percentage.
|
|
53
|
+
*
|
|
54
|
+
* @param score - The accumulated score.
|
|
55
|
+
* @returns The rounded percentage, or 0 when no check ran.
|
|
56
|
+
*/
|
|
57
|
+
const toScorePercent = (score) => Math.round(score.totalScore > 0 ? score.score / score.totalScore * 100 : 0);
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
60
|
+
export { mutateScore, scoreRecord, toScorePercent };
|
|
61
|
+
//# sourceMappingURL=calculateScore.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calculateScore.mjs","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;AAClE,KAAI,MAAM,WAAW,UAAW,QAAO,QAAQ;AAC/C,KAAI,MAAM,WAAW,QAAS,QAAO;AACrC,QAAO;;;AAIT,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;CAC1B;;;;;;;;;;AAcD,MAAa,eAAe,OAAc,UAAgC;CACxE,MAAM,WAAkB,EAAE,GAAG,OAAO;CAEpC,MAAM,iBAAiB,MAAM,MAAM,MAAM,KAAK,CAAC;AAE/C,KAAI,CAAC,eACH,QAAO;CAGT,MAAM,aAAa,YAAY;AAE/B,KAAI,OAAO,eAAe,UAAU;AAClC,WAAS,SAAS,WAAW,YAAY,MAAM;AAC/C,WAAS,cAAc;;AAGzB,QAAO;;;;;;;;AAST,MAAa,kBAAkB,UAC7B,KAAK,MAAM,MAAM,aAAa,IAAK,MAAM,QAAQ,MAAM,aAAc,MAAM,EAAE"}
|