@intlayer/cli 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.
- package/dist/cjs/scan.cjs +47 -11
- package/dist/cjs/scan.cjs.map +1 -1
- package/dist/esm/scan.mjs +48 -12
- package/dist/esm/scan.mjs.map +1 -1
- package/dist/types/scan.d.ts.map +1 -1
- package/package.json +10 -10
package/dist/cjs/scan.cjs
CHANGED
|
@@ -10,21 +10,44 @@ let _intlayer_engine_scan = require("@intlayer/engine/scan");
|
|
|
10
10
|
/** Human-readable labels for each scorable check type. */
|
|
11
11
|
const checkLabels = {
|
|
12
12
|
robots_robotsPresent: "robots.txt present",
|
|
13
|
-
robots_noLocalizedUrlsForgotten: "robots.txt keeps
|
|
14
|
-
sitemap_sitemapPresent: "sitemap
|
|
13
|
+
robots_noLocalizedUrlsForgotten: "robots.txt keeps localized URLs crawlable",
|
|
14
|
+
sitemap_sitemapPresent: "sitemap present",
|
|
15
15
|
sitemap_noLocalizedUrlsForgotten: "sitemap lists every locale",
|
|
16
16
|
sitemap_hasAlternates: "sitemap has alternate links",
|
|
17
17
|
sitemap_hasXDefault: "sitemap has x-default",
|
|
18
18
|
url_htmlLang: "html lang attribute",
|
|
19
19
|
url_htmlDir: "html dir attribute",
|
|
20
|
+
url_currentLocale: "locale signals consistent (lang, URL, hreflang)",
|
|
21
|
+
url_ogLocale: "og:locale meta tag",
|
|
20
22
|
url_hasCanonical: "canonical link",
|
|
21
23
|
url_hreflang: "hreflang tags",
|
|
22
|
-
|
|
24
|
+
url_hreflangReciprocal: "hreflang alternates link back",
|
|
23
25
|
url_hasXDefault: "x-default hreflang",
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
url_hasLocalizedLinks: "localized internal links",
|
|
27
|
+
url_allAnchorsLocalized: "all internal links keep the locale",
|
|
28
|
+
url_hasLangSelector: "crawlable language switcher",
|
|
26
29
|
url_unusedBundleContent: "unused bundle locale content"
|
|
27
30
|
};
|
|
31
|
+
/** Human-readable labels of the routing strategies. */
|
|
32
|
+
const routingLabels = {
|
|
33
|
+
"prefix-all": "locale prefix on every URL (/en/…, /fr/…)",
|
|
34
|
+
"prefix-no-default": "locale prefix except for the default locale",
|
|
35
|
+
"search-params": "locale in a query parameter",
|
|
36
|
+
subdomain: "locale subdomain (fr.example.com)",
|
|
37
|
+
domain: "one domain per locale",
|
|
38
|
+
"no-prefix": "no locale in the URL (cookie / header based)",
|
|
39
|
+
unknown: "unknown"
|
|
40
|
+
};
|
|
41
|
+
/** Human-readable labels of the technology categories. */
|
|
42
|
+
const categoryLabels = {
|
|
43
|
+
framework: "Framework",
|
|
44
|
+
"i18n-library": "i18n library",
|
|
45
|
+
tms: "TMS",
|
|
46
|
+
"translation-proxy": "Translation proxy",
|
|
47
|
+
cms: "CMS"
|
|
48
|
+
};
|
|
49
|
+
/** Maximum number of detail lines printed under a failing check. */
|
|
50
|
+
const MAX_DETAIL_LINES = 3;
|
|
28
51
|
const statusIcon = (status) => {
|
|
29
52
|
if (status === "success") return (0, _intlayer_config_logger.colorize)("✓", _intlayer_config_colors.GREEN);
|
|
30
53
|
if (status === "warning") return (0, _intlayer_config_logger.colorize)("⚠", _intlayer_config_colors.YELLOW);
|
|
@@ -35,6 +58,18 @@ const scoreColor = (score) => {
|
|
|
35
58
|
if (score >= 50) return _intlayer_config_colors.YELLOW;
|
|
36
59
|
return _intlayer_config_colors.RED;
|
|
37
60
|
};
|
|
61
|
+
/** Print a check with, when it fails, a short explanation. */
|
|
62
|
+
const logCheck = (appLogger, event) => {
|
|
63
|
+
const type = event.type.split("\\")[0];
|
|
64
|
+
if (!type) return;
|
|
65
|
+
appLogger(` ${statusIcon(event.status)} ${checkLabels[type] ?? type}`);
|
|
66
|
+
if (event.status === "success") return;
|
|
67
|
+
const detailLines = (0, _intlayer_engine_scan.getCheckDetailLines)(event.details?.[event.status]);
|
|
68
|
+
detailLines.slice(0, MAX_DETAIL_LINES).forEach((line, index) => {
|
|
69
|
+
appLogger((0, _intlayer_config_logger.colorize)(`${index === 0 ? " " : " "}${line}`, _intlayer_config_colors.GREY));
|
|
70
|
+
});
|
|
71
|
+
if (detailLines.length > MAX_DETAIL_LINES) appLogger((0, _intlayer_config_logger.colorize)(` … ${detailLines.length - MAX_DETAIL_LINES} more (use --json)`, _intlayer_config_colors.GREY));
|
|
72
|
+
};
|
|
38
73
|
/**
|
|
39
74
|
* Scan a website for i18n/SEO health and bundle weight, printing a formatted
|
|
40
75
|
* report (or JSON with `--json`).
|
|
@@ -56,13 +91,14 @@ const scan = async (url, options = {}) => {
|
|
|
56
91
|
appLogger(`Score: ${(0, _intlayer_config_logger.colorize)(`${result.score}/100`, scoreColor(result.score))}`);
|
|
57
92
|
appLogger(`Page size: ${(0, _intlayer_config_logger.colorize)((0, _intlayer_engine_scan.formatSize)(result.totalPageSize), _intlayer_config_colors.BLUE)} ${(0, _intlayer_config_logger.colorize)(`(HTML ${(0, _intlayer_engine_scan.formatSize)(result.htmlSize)})`, _intlayer_config_colors.GREY)}`);
|
|
58
93
|
if (result.locales.length > 0) appLogger(`Locales: ${(0, _intlayer_config_logger.colorize)(result.locales.join(", "), _intlayer_config_colors.GREEN)}`);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
appLogger(` ${statusIcon(event.status)} ${label}`);
|
|
94
|
+
const { routing } = result;
|
|
95
|
+
appLogger(`Routing: ${(0, _intlayer_config_logger.colorize)(routingLabels[routing.strategy], _intlayer_config_colors.BLUE)} ${(0, _intlayer_config_logger.colorize)(`(${routing.evidence}${routing.defaultLocale ? `, default locale: ${routing.defaultLocale}` : ""})`, _intlayer_config_colors.GREY)}`);
|
|
96
|
+
if (result.technologies.length > 0) {
|
|
97
|
+
appLogger("\nStack:");
|
|
98
|
+
for (const technology of result.technologies) appLogger(` ${(0, _intlayer_config_logger.colorize)(categoryLabels[technology.category], _intlayer_config_colors.GREY)} ${technology.name}${technology.version ? ` ${technology.version}` : ""} ${(0, _intlayer_config_logger.colorize)(`(${technology.evidence})`, _intlayer_config_colors.GREY)}`);
|
|
65
99
|
}
|
|
100
|
+
appLogger("\nChecks:");
|
|
101
|
+
for (const event of result.events) logCheck(appLogger, event);
|
|
66
102
|
if (result.bundle && result.bundle.totalLocaleSize > 0) {
|
|
67
103
|
const { bundle } = result;
|
|
68
104
|
appLogger("\nBundle locale weight:");
|
package/dist/cjs/scan.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.cjs","names":["colorize","ANSIColors","getConfiguration","getAppLogger","scanWebsite","formatSize"],"sources":["../../src/scan.ts"],"sourcesContent":["import * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { formatSize
|
|
1
|
+
{"version":3,"file":"scan.cjs","names":["colorize","ANSIColors","getCheckDetailLines","getConfiguration","getAppLogger","scanWebsite","formatSize"],"sources":["../../src/scan.ts"],"sourcesContent":["import * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport {\n type DetectedTechnology,\n formatSize,\n getCheckDetailLines,\n type ScanEvent,\n type ScanResult,\n scanWebsite,\n} from '@intlayer/engine/scan';\nimport type { ConfigurationOptions } from './cli';\n\n/** Options accepted by the {@link scan} command. */\nexport type ScanCommandOptions = {\n /** Disable the deeper puppeteer-based render scan. */\n deep?: boolean;\n /** Output the raw {@link ScanResult} as JSON instead of formatted text. */\n json?: boolean;\n configOptions?: ConfigurationOptions;\n};\n\n/** Human-readable labels for each scorable check type. */\nconst checkLabels: Record<string, string> = {\n robots_robotsPresent: 'robots.txt present',\n robots_noLocalizedUrlsForgotten: 'robots.txt keeps localized URLs crawlable',\n sitemap_sitemapPresent: 'sitemap present',\n sitemap_noLocalizedUrlsForgotten: 'sitemap lists every locale',\n sitemap_hasAlternates: 'sitemap has alternate links',\n sitemap_hasXDefault: 'sitemap has x-default',\n url_htmlLang: 'html lang attribute',\n url_htmlDir: 'html dir attribute',\n url_currentLocale: 'locale signals consistent (lang, URL, hreflang)',\n url_ogLocale: 'og:locale meta tag',\n url_hasCanonical: 'canonical link',\n url_hreflang: 'hreflang tags',\n url_hreflangReciprocal: 'hreflang alternates link back',\n url_hasXDefault: 'x-default hreflang',\n url_hasLocalizedLinks: 'localized internal links',\n url_allAnchorsLocalized: 'all internal links keep the locale',\n url_hasLangSelector: 'crawlable language switcher',\n url_unusedBundleContent: 'unused bundle locale content',\n};\n\n/** Human-readable labels of the routing strategies. */\nconst routingLabels: Record<ScanResult['routing']['strategy'], string> = {\n 'prefix-all': 'locale prefix on every URL (/en/…, /fr/…)',\n 'prefix-no-default': 'locale prefix except for the default locale',\n 'search-params': 'locale in a query parameter',\n subdomain: 'locale subdomain (fr.example.com)',\n domain: 'one domain per locale',\n 'no-prefix': 'no locale in the URL (cookie / header based)',\n unknown: 'unknown',\n};\n\n/** Human-readable labels of the technology categories. */\nconst categoryLabels: Record<DetectedTechnology['category'], string> = {\n framework: 'Framework',\n 'i18n-library': 'i18n library',\n tms: 'TMS',\n 'translation-proxy': 'Translation proxy',\n cms: 'CMS',\n};\n\n/** Maximum number of detail lines printed under a failing check. */\nconst MAX_DETAIL_LINES = 3;\n\nconst statusIcon = (status: ScanEvent['status']): string => {\n if (status === 'success') return colorize('✓', ANSIColors.GREEN);\n if (status === 'warning') return colorize('⚠', ANSIColors.YELLOW);\n return colorize('✗', ANSIColors.RED);\n};\n\nconst scoreColor = (score: number) => {\n if (score >= 80) return ANSIColors.GREEN;\n if (score >= 50) return ANSIColors.YELLOW;\n return ANSIColors.RED;\n};\n\n/** Print a check with, when it fails, a short explanation. */\nconst logCheck = (\n appLogger: ReturnType<typeof getAppLogger>,\n event: ScanEvent\n): void => {\n const type = event.type.split('\\\\')[0];\n if (!type) return;\n\n appLogger(` ${statusIcon(event.status)} ${checkLabels[type] ?? type}`);\n\n if (event.status === 'success') return;\n\n const detailLines = getCheckDetailLines(event.details?.[event.status]);\n detailLines.slice(0, MAX_DETAIL_LINES).forEach((line, index) => {\n appLogger(\n colorize(`${index === 0 ? ' ' : ' '}${line}`, ANSIColors.GREY)\n );\n });\n if (detailLines.length > MAX_DETAIL_LINES) {\n appLogger(\n colorize(\n ` … ${detailLines.length - MAX_DETAIL_LINES} more (use --json)`,\n ANSIColors.GREY\n )\n );\n }\n};\n\n/**\n * Scan a website for i18n/SEO health and bundle weight, printing a formatted\n * report (or JSON with `--json`).\n *\n * @example\n * ```sh\n * npx intlayer scan https://intlayer.org\n * ```\n */\nexport const scan = async (\n url: string,\n options: ScanCommandOptions = {}\n): Promise<void> => {\n const configuration = getConfiguration(options.configOptions);\n const appLogger = getAppLogger(configuration);\n\n const result = await scanWebsite(url, { deep: options.deep });\n\n if (options.json) {\n appLogger(JSON.stringify(result, null, 2));\n return;\n }\n\n appLogger(\n `\\n🔍 Scanned ${colorize(result.url, ANSIColors.GREY_LIGHT)} ${colorize(\n `(${result.mode} mode)`,\n ANSIColors.GREY\n )}\\n`\n );\n\n appLogger(\n `Score: ${colorize(`${result.score}/100`, scoreColor(result.score))}`\n );\n appLogger(\n `Page size: ${colorize(formatSize(result.totalPageSize), ANSIColors.BLUE)} ${colorize(\n `(HTML ${formatSize(result.htmlSize)})`,\n ANSIColors.GREY\n )}`\n );\n if (result.locales.length > 0) {\n appLogger(\n `Locales: ${colorize(result.locales.join(', '), ANSIColors.GREEN)}`\n );\n }\n\n const { routing } = result;\n appLogger(\n `Routing: ${colorize(routingLabels[routing.strategy], ANSIColors.BLUE)} ${colorize(\n `(${routing.evidence}${routing.defaultLocale ? `, default locale: ${routing.defaultLocale}` : ''})`,\n ANSIColors.GREY\n )}`\n );\n\n if (result.technologies.length > 0) {\n appLogger('\\nStack:');\n for (const technology of result.technologies) {\n appLogger(\n ` ${colorize(categoryLabels[technology.category], ANSIColors.GREY)} ${technology.name}${\n technology.version ? ` ${technology.version}` : ''\n } ${colorize(`(${technology.evidence})`, ANSIColors.GREY)}`\n );\n }\n }\n\n appLogger('\\nChecks:');\n for (const event of result.events) {\n logCheck(appLogger, event);\n }\n\n if (result.bundle && result.bundle.totalLocaleSize > 0) {\n const { bundle } = result;\n appLogger('\\nBundle locale weight:');\n appLogger(\n ` Translations shipped: ${colorize(formatSize(bundle.totalLocaleSize), ANSIColors.BLUE)}`\n );\n appLogger(\n ` Unused (other locales): ${colorize(\n `${formatSize(bundle.totalUnusedLocaleSize)} (${bundle.unusedPercentOfLocale}%)`,\n bundle.unusedPercentOfLocale > 30\n ? ANSIColors.RED\n : bundle.unusedPercentOfLocale > 0\n ? ANSIColors.YELLOW\n : ANSIColors.GREEN\n )}`\n );\n }\n};\n"],"mappings":";;;;;;;;;;AAuBA,MAAM,cAAsC;CAC1C,sBAAsB;CACtB,iCAAiC;CACjC,wBAAwB;CACxB,kCAAkC;CAClC,uBAAuB;CACvB,qBAAqB;CACrB,cAAc;CACd,aAAa;CACb,mBAAmB;CACnB,cAAc;CACd,kBAAkB;CAClB,cAAc;CACd,wBAAwB;CACxB,iBAAiB;CACjB,uBAAuB;CACvB,yBAAyB;CACzB,qBAAqB;CACrB,yBAAyB;AAC3B;;AAGA,MAAM,gBAAmE;CACvE,cAAc;CACd,qBAAqB;CACrB,iBAAiB;CACjB,WAAW;CACX,QAAQ;CACR,aAAa;CACb,SAAS;AACX;;AAGA,MAAM,iBAAiE;CACrE,WAAW;CACX,gBAAgB;CAChB,KAAK;CACL,qBAAqB;CACrB,KAAK;AACP;;AAGA,MAAM,mBAAmB;AAEzB,MAAM,cAAc,WAAwC;CAC1D,IAAI,WAAW,WAAW,WAAOA,kCAAS,KAAKC,wBAAW,KAAK;CAC/D,IAAI,WAAW,WAAW,WAAOD,kCAAS,KAAKC,wBAAW,MAAM;CAChE,WAAOD,kCAAS,KAAKC,wBAAW,GAAG;AACrC;AAEA,MAAM,cAAc,UAAkB;CACpC,IAAI,SAAS,IAAI,OAAOA,wBAAW;CACnC,IAAI,SAAS,IAAI,OAAOA,wBAAW;CACnC,OAAOA,wBAAW;AACpB;;AAGA,MAAM,YACJ,WACA,UACS;CACT,MAAM,OAAO,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC;CACpC,IAAI,CAAC,MAAM;CAEX,UAAU,KAAK,WAAW,MAAM,MAAM,EAAE,GAAG,YAAY,SAAS,MAAM;CAEtE,IAAI,MAAM,WAAW,WAAW;CAEhC,MAAM,kBAAcC,2CAAoB,MAAM,UAAU,MAAM,OAAO;CACrE,YAAY,MAAM,GAAG,gBAAgB,CAAC,CAAC,SAAS,MAAM,UAAU;EAC9D,cACEF,kCAAS,GAAG,UAAU,IAAI,WAAW,aAAa,QAAQC,wBAAW,IAAI,CAC3E;CACF,CAAC;CACD,IAAI,YAAY,SAAS,kBACvB,cACED,kCACE,WAAW,YAAY,SAAS,iBAAiB,qBACjDC,wBAAW,IACb,CACF;AAEJ;;;;;;;;;;AAWA,MAAa,OAAO,OAClB,KACA,UAA8B,CAAC,MACb;CAClB,MAAM,oBAAgBE,wCAAiB,QAAQ,aAAa;CAC5D,MAAM,gBAAYC,sCAAa,aAAa;CAE5C,MAAM,SAAS,UAAMC,mCAAY,KAAK,EAAE,MAAM,QAAQ,KAAK,CAAC;CAE5D,IAAI,QAAQ,MAAM;EAChB,UAAU,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;EACzC;CACF;CAEA,UACE,oBAAgBL,kCAAS,OAAO,KAAKC,wBAAW,UAAU,EAAE,OAAGD,kCAC7D,IAAI,OAAO,KAAK,SAChBC,wBAAW,IACb,EAAE,GACJ;CAEA,UACE,cAAUD,kCAAS,GAAG,OAAO,MAAM,OAAO,WAAW,OAAO,KAAK,CAAC,GACpE;CACA,UACE,kBAAcA,sCAASM,kCAAW,OAAO,aAAa,GAAGL,wBAAW,IAAI,EAAE,OAAGD,kCAC3E,aAASM,kCAAW,OAAO,QAAQ,EAAE,IACrCL,wBAAW,IACb,GACF;CACA,IAAI,OAAO,QAAQ,SAAS,GAC1B,UACE,gBAAYD,kCAAS,OAAO,QAAQ,KAAK,IAAI,GAAGC,wBAAW,KAAK,GAClE;CAGF,MAAM,EAAE,YAAY;CACpB,UACE,gBAAYD,kCAAS,cAAc,QAAQ,WAAWC,wBAAW,IAAI,EAAE,OAAGD,kCACxE,IAAI,QAAQ,WAAW,QAAQ,gBAAgB,qBAAqB,QAAQ,kBAAkB,GAAG,IACjGC,wBAAW,IACb,GACF;CAEA,IAAI,OAAO,aAAa,SAAS,GAAG;EAClC,UAAU,UAAU;EACpB,KAAK,MAAM,cAAc,OAAO,cAC9B,UACE,SAAKD,kCAAS,eAAe,WAAW,WAAWC,wBAAW,IAAI,EAAE,GAAG,WAAW,OAChF,WAAW,UAAU,IAAI,WAAW,YAAY,GACjD,OAAGD,kCAAS,IAAI,WAAW,SAAS,IAAIC,wBAAW,IAAI,GAC1D;CAEJ;CAEA,UAAU,WAAW;CACrB,KAAK,MAAM,SAAS,OAAO,QACzB,SAAS,WAAW,KAAK;CAG3B,IAAI,OAAO,UAAU,OAAO,OAAO,kBAAkB,GAAG;EACtD,MAAM,EAAE,WAAW;EACnB,UAAU,yBAAyB;EACnC,UACE,+BAA2BD,sCAASM,kCAAW,OAAO,eAAe,GAAGL,wBAAW,IAAI,GACzF;EACA,UACE,iCAA6BD,kCAC3B,OAAGM,kCAAW,OAAO,qBAAqB,EAAE,IAAI,OAAO,sBAAsB,KAC7E,OAAO,wBAAwB,KAC3BL,wBAAW,MACX,OAAO,wBAAwB,IAC7BA,wBAAW,SACXA,wBAAW,KACnB,GACF;CACF;AACF"}
|
package/dist/esm/scan.mjs
CHANGED
|
@@ -1,27 +1,50 @@
|
|
|
1
1
|
import * as ANSIColors from "@intlayer/config/colors";
|
|
2
2
|
import { colorize, getAppLogger } from "@intlayer/config/logger";
|
|
3
3
|
import { getConfiguration } from "@intlayer/config/node";
|
|
4
|
-
import { formatSize, scanWebsite } from "@intlayer/engine/scan";
|
|
4
|
+
import { formatSize, getCheckDetailLines, scanWebsite } from "@intlayer/engine/scan";
|
|
5
5
|
|
|
6
6
|
//#region src/scan.ts
|
|
7
7
|
/** Human-readable labels for each scorable check type. */
|
|
8
8
|
const checkLabels = {
|
|
9
9
|
robots_robotsPresent: "robots.txt present",
|
|
10
|
-
robots_noLocalizedUrlsForgotten: "robots.txt keeps
|
|
11
|
-
sitemap_sitemapPresent: "sitemap
|
|
10
|
+
robots_noLocalizedUrlsForgotten: "robots.txt keeps localized URLs crawlable",
|
|
11
|
+
sitemap_sitemapPresent: "sitemap present",
|
|
12
12
|
sitemap_noLocalizedUrlsForgotten: "sitemap lists every locale",
|
|
13
13
|
sitemap_hasAlternates: "sitemap has alternate links",
|
|
14
14
|
sitemap_hasXDefault: "sitemap has x-default",
|
|
15
15
|
url_htmlLang: "html lang attribute",
|
|
16
16
|
url_htmlDir: "html dir attribute",
|
|
17
|
+
url_currentLocale: "locale signals consistent (lang, URL, hreflang)",
|
|
18
|
+
url_ogLocale: "og:locale meta tag",
|
|
17
19
|
url_hasCanonical: "canonical link",
|
|
18
20
|
url_hreflang: "hreflang tags",
|
|
19
|
-
|
|
21
|
+
url_hreflangReciprocal: "hreflang alternates link back",
|
|
20
22
|
url_hasXDefault: "x-default hreflang",
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
url_hasLocalizedLinks: "localized internal links",
|
|
24
|
+
url_allAnchorsLocalized: "all internal links keep the locale",
|
|
25
|
+
url_hasLangSelector: "crawlable language switcher",
|
|
23
26
|
url_unusedBundleContent: "unused bundle locale content"
|
|
24
27
|
};
|
|
28
|
+
/** Human-readable labels of the routing strategies. */
|
|
29
|
+
const routingLabels = {
|
|
30
|
+
"prefix-all": "locale prefix on every URL (/en/…, /fr/…)",
|
|
31
|
+
"prefix-no-default": "locale prefix except for the default locale",
|
|
32
|
+
"search-params": "locale in a query parameter",
|
|
33
|
+
subdomain: "locale subdomain (fr.example.com)",
|
|
34
|
+
domain: "one domain per locale",
|
|
35
|
+
"no-prefix": "no locale in the URL (cookie / header based)",
|
|
36
|
+
unknown: "unknown"
|
|
37
|
+
};
|
|
38
|
+
/** Human-readable labels of the technology categories. */
|
|
39
|
+
const categoryLabels = {
|
|
40
|
+
framework: "Framework",
|
|
41
|
+
"i18n-library": "i18n library",
|
|
42
|
+
tms: "TMS",
|
|
43
|
+
"translation-proxy": "Translation proxy",
|
|
44
|
+
cms: "CMS"
|
|
45
|
+
};
|
|
46
|
+
/** Maximum number of detail lines printed under a failing check. */
|
|
47
|
+
const MAX_DETAIL_LINES = 3;
|
|
25
48
|
const statusIcon = (status) => {
|
|
26
49
|
if (status === "success") return colorize("✓", ANSIColors.GREEN);
|
|
27
50
|
if (status === "warning") return colorize("⚠", ANSIColors.YELLOW);
|
|
@@ -32,6 +55,18 @@ const scoreColor = (score) => {
|
|
|
32
55
|
if (score >= 50) return ANSIColors.YELLOW;
|
|
33
56
|
return ANSIColors.RED;
|
|
34
57
|
};
|
|
58
|
+
/** Print a check with, when it fails, a short explanation. */
|
|
59
|
+
const logCheck = (appLogger, event) => {
|
|
60
|
+
const type = event.type.split("\\")[0];
|
|
61
|
+
if (!type) return;
|
|
62
|
+
appLogger(` ${statusIcon(event.status)} ${checkLabels[type] ?? type}`);
|
|
63
|
+
if (event.status === "success") return;
|
|
64
|
+
const detailLines = getCheckDetailLines(event.details?.[event.status]);
|
|
65
|
+
detailLines.slice(0, MAX_DETAIL_LINES).forEach((line, index) => {
|
|
66
|
+
appLogger(colorize(`${index === 0 ? " " : " "}${line}`, ANSIColors.GREY));
|
|
67
|
+
});
|
|
68
|
+
if (detailLines.length > MAX_DETAIL_LINES) appLogger(colorize(` … ${detailLines.length - MAX_DETAIL_LINES} more (use --json)`, ANSIColors.GREY));
|
|
69
|
+
};
|
|
35
70
|
/**
|
|
36
71
|
* Scan a website for i18n/SEO health and bundle weight, printing a formatted
|
|
37
72
|
* report (or JSON with `--json`).
|
|
@@ -53,13 +88,14 @@ const scan = async (url, options = {}) => {
|
|
|
53
88
|
appLogger(`Score: ${colorize(`${result.score}/100`, scoreColor(result.score))}`);
|
|
54
89
|
appLogger(`Page size: ${colorize(formatSize(result.totalPageSize), ANSIColors.BLUE)} ${colorize(`(HTML ${formatSize(result.htmlSize)})`, ANSIColors.GREY)}`);
|
|
55
90
|
if (result.locales.length > 0) appLogger(`Locales: ${colorize(result.locales.join(", "), ANSIColors.GREEN)}`);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
appLogger(` ${statusIcon(event.status)} ${label}`);
|
|
91
|
+
const { routing } = result;
|
|
92
|
+
appLogger(`Routing: ${colorize(routingLabels[routing.strategy], ANSIColors.BLUE)} ${colorize(`(${routing.evidence}${routing.defaultLocale ? `, default locale: ${routing.defaultLocale}` : ""})`, ANSIColors.GREY)}`);
|
|
93
|
+
if (result.technologies.length > 0) {
|
|
94
|
+
appLogger("\nStack:");
|
|
95
|
+
for (const technology of result.technologies) appLogger(` ${colorize(categoryLabels[technology.category], ANSIColors.GREY)} ${technology.name}${technology.version ? ` ${technology.version}` : ""} ${colorize(`(${technology.evidence})`, ANSIColors.GREY)}`);
|
|
62
96
|
}
|
|
97
|
+
appLogger("\nChecks:");
|
|
98
|
+
for (const event of result.events) logCheck(appLogger, event);
|
|
63
99
|
if (result.bundle && result.bundle.totalLocaleSize > 0) {
|
|
64
100
|
const { bundle } = result;
|
|
65
101
|
appLogger("\nBundle locale weight:");
|
package/dist/esm/scan.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.mjs","names":[],"sources":["../../src/scan.ts"],"sourcesContent":["import * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { formatSize
|
|
1
|
+
{"version":3,"file":"scan.mjs","names":[],"sources":["../../src/scan.ts"],"sourcesContent":["import * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport {\n type DetectedTechnology,\n formatSize,\n getCheckDetailLines,\n type ScanEvent,\n type ScanResult,\n scanWebsite,\n} from '@intlayer/engine/scan';\nimport type { ConfigurationOptions } from './cli';\n\n/** Options accepted by the {@link scan} command. */\nexport type ScanCommandOptions = {\n /** Disable the deeper puppeteer-based render scan. */\n deep?: boolean;\n /** Output the raw {@link ScanResult} as JSON instead of formatted text. */\n json?: boolean;\n configOptions?: ConfigurationOptions;\n};\n\n/** Human-readable labels for each scorable check type. */\nconst checkLabels: Record<string, string> = {\n robots_robotsPresent: 'robots.txt present',\n robots_noLocalizedUrlsForgotten: 'robots.txt keeps localized URLs crawlable',\n sitemap_sitemapPresent: 'sitemap present',\n sitemap_noLocalizedUrlsForgotten: 'sitemap lists every locale',\n sitemap_hasAlternates: 'sitemap has alternate links',\n sitemap_hasXDefault: 'sitemap has x-default',\n url_htmlLang: 'html lang attribute',\n url_htmlDir: 'html dir attribute',\n url_currentLocale: 'locale signals consistent (lang, URL, hreflang)',\n url_ogLocale: 'og:locale meta tag',\n url_hasCanonical: 'canonical link',\n url_hreflang: 'hreflang tags',\n url_hreflangReciprocal: 'hreflang alternates link back',\n url_hasXDefault: 'x-default hreflang',\n url_hasLocalizedLinks: 'localized internal links',\n url_allAnchorsLocalized: 'all internal links keep the locale',\n url_hasLangSelector: 'crawlable language switcher',\n url_unusedBundleContent: 'unused bundle locale content',\n};\n\n/** Human-readable labels of the routing strategies. */\nconst routingLabels: Record<ScanResult['routing']['strategy'], string> = {\n 'prefix-all': 'locale prefix on every URL (/en/…, /fr/…)',\n 'prefix-no-default': 'locale prefix except for the default locale',\n 'search-params': 'locale in a query parameter',\n subdomain: 'locale subdomain (fr.example.com)',\n domain: 'one domain per locale',\n 'no-prefix': 'no locale in the URL (cookie / header based)',\n unknown: 'unknown',\n};\n\n/** Human-readable labels of the technology categories. */\nconst categoryLabels: Record<DetectedTechnology['category'], string> = {\n framework: 'Framework',\n 'i18n-library': 'i18n library',\n tms: 'TMS',\n 'translation-proxy': 'Translation proxy',\n cms: 'CMS',\n};\n\n/** Maximum number of detail lines printed under a failing check. */\nconst MAX_DETAIL_LINES = 3;\n\nconst statusIcon = (status: ScanEvent['status']): string => {\n if (status === 'success') return colorize('✓', ANSIColors.GREEN);\n if (status === 'warning') return colorize('⚠', ANSIColors.YELLOW);\n return colorize('✗', ANSIColors.RED);\n};\n\nconst scoreColor = (score: number) => {\n if (score >= 80) return ANSIColors.GREEN;\n if (score >= 50) return ANSIColors.YELLOW;\n return ANSIColors.RED;\n};\n\n/** Print a check with, when it fails, a short explanation. */\nconst logCheck = (\n appLogger: ReturnType<typeof getAppLogger>,\n event: ScanEvent\n): void => {\n const type = event.type.split('\\\\')[0];\n if (!type) return;\n\n appLogger(` ${statusIcon(event.status)} ${checkLabels[type] ?? type}`);\n\n if (event.status === 'success') return;\n\n const detailLines = getCheckDetailLines(event.details?.[event.status]);\n detailLines.slice(0, MAX_DETAIL_LINES).forEach((line, index) => {\n appLogger(\n colorize(`${index === 0 ? ' ' : ' '}${line}`, ANSIColors.GREY)\n );\n });\n if (detailLines.length > MAX_DETAIL_LINES) {\n appLogger(\n colorize(\n ` … ${detailLines.length - MAX_DETAIL_LINES} more (use --json)`,\n ANSIColors.GREY\n )\n );\n }\n};\n\n/**\n * Scan a website for i18n/SEO health and bundle weight, printing a formatted\n * report (or JSON with `--json`).\n *\n * @example\n * ```sh\n * npx intlayer scan https://intlayer.org\n * ```\n */\nexport const scan = async (\n url: string,\n options: ScanCommandOptions = {}\n): Promise<void> => {\n const configuration = getConfiguration(options.configOptions);\n const appLogger = getAppLogger(configuration);\n\n const result = await scanWebsite(url, { deep: options.deep });\n\n if (options.json) {\n appLogger(JSON.stringify(result, null, 2));\n return;\n }\n\n appLogger(\n `\\n🔍 Scanned ${colorize(result.url, ANSIColors.GREY_LIGHT)} ${colorize(\n `(${result.mode} mode)`,\n ANSIColors.GREY\n )}\\n`\n );\n\n appLogger(\n `Score: ${colorize(`${result.score}/100`, scoreColor(result.score))}`\n );\n appLogger(\n `Page size: ${colorize(formatSize(result.totalPageSize), ANSIColors.BLUE)} ${colorize(\n `(HTML ${formatSize(result.htmlSize)})`,\n ANSIColors.GREY\n )}`\n );\n if (result.locales.length > 0) {\n appLogger(\n `Locales: ${colorize(result.locales.join(', '), ANSIColors.GREEN)}`\n );\n }\n\n const { routing } = result;\n appLogger(\n `Routing: ${colorize(routingLabels[routing.strategy], ANSIColors.BLUE)} ${colorize(\n `(${routing.evidence}${routing.defaultLocale ? `, default locale: ${routing.defaultLocale}` : ''})`,\n ANSIColors.GREY\n )}`\n );\n\n if (result.technologies.length > 0) {\n appLogger('\\nStack:');\n for (const technology of result.technologies) {\n appLogger(\n ` ${colorize(categoryLabels[technology.category], ANSIColors.GREY)} ${technology.name}${\n technology.version ? ` ${technology.version}` : ''\n } ${colorize(`(${technology.evidence})`, ANSIColors.GREY)}`\n );\n }\n }\n\n appLogger('\\nChecks:');\n for (const event of result.events) {\n logCheck(appLogger, event);\n }\n\n if (result.bundle && result.bundle.totalLocaleSize > 0) {\n const { bundle } = result;\n appLogger('\\nBundle locale weight:');\n appLogger(\n ` Translations shipped: ${colorize(formatSize(bundle.totalLocaleSize), ANSIColors.BLUE)}`\n );\n appLogger(\n ` Unused (other locales): ${colorize(\n `${formatSize(bundle.totalUnusedLocaleSize)} (${bundle.unusedPercentOfLocale}%)`,\n bundle.unusedPercentOfLocale > 30\n ? ANSIColors.RED\n : bundle.unusedPercentOfLocale > 0\n ? ANSIColors.YELLOW\n : ANSIColors.GREEN\n )}`\n );\n }\n};\n"],"mappings":"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"}
|
package/dist/types/scan.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.d.ts","names":[],"sources":["../../src/scan.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"scan.d.ts","names":[],"sources":["../../src/scan.ts"],"mappings":";;;YAcY;;EAEV;;EAEA;EACA,gBAAgB;;;;;;;;;;;qBAiGL,OAAI,aACJ,UACF,uBACR"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intlayer/cli",
|
|
3
|
-
"version": "9.5.
|
|
3
|
+
"version": "9.5.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Provides uniform command-line interface scripts for Intlayer, used in packages like intlayer-cli and intlayer.",
|
|
6
6
|
"keywords": [
|
|
@@ -68,20 +68,20 @@
|
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@clack/prompts": "1.7.0",
|
|
71
|
-
"@intlayer/api": "9.5.
|
|
72
|
-
"@intlayer/babel": "9.5.
|
|
73
|
-
"@intlayer/config": "9.5.
|
|
74
|
-
"@intlayer/core": "9.5.
|
|
75
|
-
"@intlayer/dictionaries-entry": "9.5.
|
|
76
|
-
"@intlayer/engine": "9.5.
|
|
77
|
-
"@intlayer/types": "9.5.
|
|
71
|
+
"@intlayer/api": "9.5.11",
|
|
72
|
+
"@intlayer/babel": "9.5.11",
|
|
73
|
+
"@intlayer/config": "9.5.11",
|
|
74
|
+
"@intlayer/core": "9.5.11",
|
|
75
|
+
"@intlayer/dictionaries-entry": "9.5.11",
|
|
76
|
+
"@intlayer/engine": "9.5.11",
|
|
77
|
+
"@intlayer/types": "9.5.11",
|
|
78
78
|
"commander": "15.0.0",
|
|
79
79
|
"enquirer": "2.4.1",
|
|
80
80
|
"eventsource": "5.1.1",
|
|
81
81
|
"fast-glob": "3.3.3"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@intlayer/ai": "9.5.
|
|
84
|
+
"@intlayer/ai": "9.5.11",
|
|
85
85
|
"@types/node": "^22",
|
|
86
86
|
"@utils/ts-config": "1.0.4",
|
|
87
87
|
"@utils/ts-config-types": "1.0.4",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"vitest": "5.0.2"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
|
-
"@intlayer/ai": "9.5.
|
|
95
|
+
"@intlayer/ai": "9.5.11"
|
|
96
96
|
},
|
|
97
97
|
"peerDependenciesMeta": {
|
|
98
98
|
"@intlayer/ai": {
|