@intlayer/cli 9.5.10 → 9.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- let _intlayer_engine_cli = require("@intlayer/engine/cli");
3
2
  let _intlayer_config_node = require("@intlayer/config/node");
4
3
  let _intlayer_engine_utils = require("@intlayer/engine/utils");
5
4
  let _intlayer_engine_watcher = require("@intlayer/engine/watcher");
5
+ let _intlayer_engine_logConfigDetails = require("@intlayer/engine/logConfigDetails");
6
6
 
7
7
  //#region src/build.ts
8
8
  /**
@@ -11,7 +11,7 @@ let _intlayer_engine_watcher = require("@intlayer/engine/watcher");
11
11
  */
12
12
  const build = async (options) => {
13
13
  const config = (0, _intlayer_config_node.getConfiguration)(options?.configOptions);
14
- (0, _intlayer_engine_cli.logConfigDetails)(options?.configOptions);
14
+ (0, _intlayer_engine_logConfigDetails.logConfigDetails)(options?.configOptions);
15
15
  let parallelProcess = null;
16
16
  if (options?.watch) {
17
17
  const bundlerAlreadyWatching = await (0, _intlayer_engine_utils.claimCliContentWatcher)(config, "intlayer build --watch");
@@ -1 +1 @@
1
- {"version":3,"file":"build.cjs","names":["getConfiguration","claimCliContentWatcher","runParallel","buildAndWatchIntlayer"],"sources":["../../src/build.ts"],"sourcesContent":["import {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport { logConfigDetails } from '@intlayer/engine/cli';\nimport {\n claimCliContentWatcher,\n type ParallelHandle,\n reportRedundantContentWatcher,\n runParallel,\n} from '@intlayer/engine/utils';\nimport { buildAndWatchIntlayer } from '@intlayer/engine/watcher';\n\ntype BuildOptions = {\n watch?: boolean;\n skipPrepare?: boolean;\n with?: string | string[];\n configOptions?: GetConfigurationOptions;\n};\n\n/**\n * Get locales dictionaries .content.{json|ts|tsx|js|jsx|mjs|cjs} and build the JSON dictionaries in the .intlayer directory.\n * Watch mode available to get the change in the .content.{json|ts|tsx|js|jsx|mjs|cjs}\n */\nexport const build = async (options?: BuildOptions) => {\n const config = getConfiguration(options?.configOptions);\n logConfigDetails(options?.configOptions);\n\n let parallelProcess: ParallelHandle | null = null;\n\n // Only watch mode keeps a watcher alive; a one-off build has nothing to\n // deduplicate. Claimed before the child is spawned so the bundler plugin\n // inside it stands down instead of racing this command.\n if (options?.watch) {\n const bundlerAlreadyWatching = await claimCliContentWatcher(\n config,\n 'intlayer build --watch'\n );\n\n if (bundlerAlreadyWatching) {\n reportRedundantContentWatcher(config, {\n cliLabel: 'intlayer build --watch',\n bundlerLabel: bundlerAlreadyWatching.label,\n });\n }\n }\n\n if (options?.with) {\n parallelProcess = runParallel(options.with);\n // Handle the promise to avoid unhandled rejection\n parallelProcess.result.catch(() => {\n // Parallel process failed or was terminated\n });\n }\n\n await buildAndWatchIntlayer({\n persistent: options?.watch ?? false,\n skipPrepare: options?.skipPrepare ?? false,\n configuration: config,\n });\n\n if (!options?.watch && parallelProcess) {\n parallelProcess.kill();\n }\n};\n"],"mappings":";;;;;;;;;;;AAwBA,MAAa,QAAQ,OAAO,YAA2B;CACrD,MAAM,aAASA,wCAAiB,SAAS,aAAa;CACtD,2CAAiB,SAAS,aAAa;CAEvC,IAAI,kBAAyC;CAK7C,IAAI,SAAS,OAAO;EAClB,MAAM,yBAAyB,UAAMC,+CACnC,QACA,wBACF;EAEA,IAAI,wBACF,0DAA8B,QAAQ;GACpC,UAAU;GACV,cAAc,uBAAuB;EACvC,CAAC;CAEL;CAEA,IAAI,SAAS,MAAM;EACjB,sBAAkBC,oCAAY,QAAQ,IAAI;EAE1C,gBAAgB,OAAO,YAAY,CAEnC,CAAC;CACH;CAEA,UAAMC,gDAAsB;EAC1B,YAAY,SAAS,SAAS;EAC9B,aAAa,SAAS,eAAe;EACrC,eAAe;CACjB,CAAC;CAED,IAAI,CAAC,SAAS,SAAS,iBACrB,gBAAgB,KAAK;AAEzB"}
1
+ {"version":3,"file":"build.cjs","names":["getConfiguration","claimCliContentWatcher","runParallel","buildAndWatchIntlayer"],"sources":["../../src/build.ts"],"sourcesContent":["import {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport { logConfigDetails } from '@intlayer/engine/logConfigDetails';\nimport {\n claimCliContentWatcher,\n type ParallelHandle,\n reportRedundantContentWatcher,\n runParallel,\n} from '@intlayer/engine/utils';\nimport { buildAndWatchIntlayer } from '@intlayer/engine/watcher';\n\ntype BuildOptions = {\n watch?: boolean;\n skipPrepare?: boolean;\n with?: string | string[];\n configOptions?: GetConfigurationOptions;\n};\n\n/**\n * Get locales dictionaries .content.{json|ts|tsx|js|jsx|mjs|cjs} and build the JSON dictionaries in the .intlayer directory.\n * Watch mode available to get the change in the .content.{json|ts|tsx|js|jsx|mjs|cjs}\n */\nexport const build = async (options?: BuildOptions) => {\n const config = getConfiguration(options?.configOptions);\n logConfigDetails(options?.configOptions);\n\n let parallelProcess: ParallelHandle | null = null;\n\n // Only watch mode keeps a watcher alive; a one-off build has nothing to\n // deduplicate. Claimed before the child is spawned so the bundler plugin\n // inside it stands down instead of racing this command.\n if (options?.watch) {\n const bundlerAlreadyWatching = await claimCliContentWatcher(\n config,\n 'intlayer build --watch'\n );\n\n if (bundlerAlreadyWatching) {\n reportRedundantContentWatcher(config, {\n cliLabel: 'intlayer build --watch',\n bundlerLabel: bundlerAlreadyWatching.label,\n });\n }\n }\n\n if (options?.with) {\n parallelProcess = runParallel(options.with);\n // Handle the promise to avoid unhandled rejection\n parallelProcess.result.catch(() => {\n // Parallel process failed or was terminated\n });\n }\n\n await buildAndWatchIntlayer({\n persistent: options?.watch ?? false,\n skipPrepare: options?.skipPrepare ?? false,\n configuration: config,\n });\n\n if (!options?.watch && parallelProcess) {\n parallelProcess.kill();\n }\n};\n"],"mappings":";;;;;;;;;;;AAwBA,MAAa,QAAQ,OAAO,YAA2B;CACrD,MAAM,aAASA,wCAAiB,SAAS,aAAa;CACtD,wDAAiB,SAAS,aAAa;CAEvC,IAAI,kBAAyC;CAK7C,IAAI,SAAS,OAAO;EAClB,MAAM,yBAAyB,UAAMC,+CACnC,QACA,wBACF;EAEA,IAAI,wBACF,0DAA8B,QAAQ;GACpC,UAAU;GACV,cAAc,uBAAuB;EACvC,CAAC;CAEL;CAEA,IAAI,SAAS,MAAM;EACjB,sBAAkBC,oCAAY,QAAQ,IAAI;EAE1C,gBAAgB,OAAO,YAAY,CAEnC,CAAC;CACH;CAEA,UAAMC,gDAAsB;EAC1B,YAAY,SAAS,SAAS;EAC9B,aAAa,SAAS,eAAe;EACrC,eAAe;CACjB,CAAC;CAED,IAAI,CAAC,SAAS,SAAS,iBACrB,gBAAgB,KAAK;AAEzB"}
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 locale paths crawlable",
14
- sitemap_sitemapPresent: "sitemap.xml present",
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
- url_hasLocalizedLinks: "localized internal links",
24
+ url_hreflangReciprocal: "hreflang alternates link back",
23
25
  url_hasXDefault: "x-default hreflang",
24
- url_allAnchorsLocalized: "all internal links localized",
25
- url_currentLocale: "current locale detected",
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
- appLogger("\nChecks:");
60
- for (const event of result.events) {
61
- const type = event.type.split("\\")[0];
62
- if (!type) continue;
63
- const label = checkLabels[type] ?? type;
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}` : ""}`);
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:");
@@ -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, type ScanEvent, scanWebsite } 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 locale paths crawlable',\n sitemap_sitemapPresent: 'sitemap.xml 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_hasCanonical: 'canonical link',\n url_hreflang: 'hreflang tags',\n url_hasLocalizedLinks: 'localized internal links',\n url_hasXDefault: 'x-default hreflang',\n url_allAnchorsLocalized: 'all internal links localized',\n url_currentLocale: 'current locale detected',\n url_unusedBundleContent: 'unused bundle locale content',\n};\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/**\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 appLogger('\\nChecks:');\n for (const event of result.events) {\n const type = event.type.split('\\\\')[0];\n\n if (!type) continue;\n\n const label = checkLabels[type] ?? type;\n appLogger(` ${statusIcon(event.status)} ${label}`);\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":";;;;;;;;;;AAgBA,MAAM,cAAsC;CAC1C,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;AAEA,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;;;;;;;;;;AAWA,MAAa,OAAO,OAClB,KACA,UAA8B,CAAC,MACb;CAClB,MAAM,oBAAgBC,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,oBAAgBJ,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,sCAASK,kCAAW,OAAO,aAAa,GAAGJ,wBAAW,IAAI,EAAE,OAAGD,kCAC3E,aAASK,kCAAW,OAAO,QAAQ,EAAE,IACrCJ,wBAAW,IACb,GACF;CACA,IAAI,OAAO,QAAQ,SAAS,GAC1B,UACE,gBAAYD,kCAAS,OAAO,QAAQ,KAAK,IAAI,GAAGC,wBAAW,KAAK,GAClE;CAGF,UAAU,WAAW;CACrB,KAAK,MAAM,SAAS,OAAO,QAAQ;EACjC,MAAM,OAAO,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC;EAEpC,IAAI,CAAC,MAAM;EAEX,MAAM,QAAQ,YAAY,SAAS;EACnC,UAAU,KAAK,WAAW,MAAM,MAAM,EAAE,GAAG,OAAO;CACpD;CAEA,IAAI,OAAO,UAAU,OAAO,OAAO,kBAAkB,GAAG;EACtD,MAAM,EAAE,WAAW;EACnB,UAAU,yBAAyB;EACnC,UACE,+BAA2BD,sCAASK,kCAAW,OAAO,eAAe,GAAGJ,wBAAW,IAAI,GACzF;EACA,UACE,iCAA6BD,kCAC3B,OAAGK,kCAAW,OAAO,qBAAqB,EAAE,IAAI,OAAO,sBAAsB,KAC7E,OAAO,wBAAwB,KAC3BJ,wBAAW,MACX,OAAO,wBAAwB,IAC7BA,wBAAW,SACXA,wBAAW,KACnB,GACF;CACF;AACF"}
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 }`\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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"}
@@ -1,7 +1,7 @@
1
- import { logConfigDetails } from "@intlayer/engine/cli";
2
1
  import { getConfiguration } from "@intlayer/config/node";
3
2
  import { claimCliContentWatcher, reportRedundantContentWatcher, runParallel } from "@intlayer/engine/utils";
4
3
  import { buildAndWatchIntlayer } from "@intlayer/engine/watcher";
4
+ import { logConfigDetails } from "@intlayer/engine/logConfigDetails";
5
5
 
6
6
  //#region src/build.ts
7
7
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"build.mjs","names":[],"sources":["../../src/build.ts"],"sourcesContent":["import {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport { logConfigDetails } from '@intlayer/engine/cli';\nimport {\n claimCliContentWatcher,\n type ParallelHandle,\n reportRedundantContentWatcher,\n runParallel,\n} from '@intlayer/engine/utils';\nimport { buildAndWatchIntlayer } from '@intlayer/engine/watcher';\n\ntype BuildOptions = {\n watch?: boolean;\n skipPrepare?: boolean;\n with?: string | string[];\n configOptions?: GetConfigurationOptions;\n};\n\n/**\n * Get locales dictionaries .content.{json|ts|tsx|js|jsx|mjs|cjs} and build the JSON dictionaries in the .intlayer directory.\n * Watch mode available to get the change in the .content.{json|ts|tsx|js|jsx|mjs|cjs}\n */\nexport const build = async (options?: BuildOptions) => {\n const config = getConfiguration(options?.configOptions);\n logConfigDetails(options?.configOptions);\n\n let parallelProcess: ParallelHandle | null = null;\n\n // Only watch mode keeps a watcher alive; a one-off build has nothing to\n // deduplicate. Claimed before the child is spawned so the bundler plugin\n // inside it stands down instead of racing this command.\n if (options?.watch) {\n const bundlerAlreadyWatching = await claimCliContentWatcher(\n config,\n 'intlayer build --watch'\n );\n\n if (bundlerAlreadyWatching) {\n reportRedundantContentWatcher(config, {\n cliLabel: 'intlayer build --watch',\n bundlerLabel: bundlerAlreadyWatching.label,\n });\n }\n }\n\n if (options?.with) {\n parallelProcess = runParallel(options.with);\n // Handle the promise to avoid unhandled rejection\n parallelProcess.result.catch(() => {\n // Parallel process failed or was terminated\n });\n }\n\n await buildAndWatchIntlayer({\n persistent: options?.watch ?? false,\n skipPrepare: options?.skipPrepare ?? false,\n configuration: config,\n });\n\n if (!options?.watch && parallelProcess) {\n parallelProcess.kill();\n }\n};\n"],"mappings":";;;;;;;;;;AAwBA,MAAa,QAAQ,OAAO,YAA2B;CACrD,MAAM,SAAS,iBAAiB,SAAS,aAAa;CACtD,iBAAiB,SAAS,aAAa;CAEvC,IAAI,kBAAyC;CAK7C,IAAI,SAAS,OAAO;EAClB,MAAM,yBAAyB,MAAM,uBACnC,QACA,wBACF;EAEA,IAAI,wBACF,8BAA8B,QAAQ;GACpC,UAAU;GACV,cAAc,uBAAuB;EACvC,CAAC;CAEL;CAEA,IAAI,SAAS,MAAM;EACjB,kBAAkB,YAAY,QAAQ,IAAI;EAE1C,gBAAgB,OAAO,YAAY,CAEnC,CAAC;CACH;CAEA,MAAM,sBAAsB;EAC1B,YAAY,SAAS,SAAS;EAC9B,aAAa,SAAS,eAAe;EACrC,eAAe;CACjB,CAAC;CAED,IAAI,CAAC,SAAS,SAAS,iBACrB,gBAAgB,KAAK;AAEzB"}
1
+ {"version":3,"file":"build.mjs","names":[],"sources":["../../src/build.ts"],"sourcesContent":["import {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport { logConfigDetails } from '@intlayer/engine/logConfigDetails';\nimport {\n claimCliContentWatcher,\n type ParallelHandle,\n reportRedundantContentWatcher,\n runParallel,\n} from '@intlayer/engine/utils';\nimport { buildAndWatchIntlayer } from '@intlayer/engine/watcher';\n\ntype BuildOptions = {\n watch?: boolean;\n skipPrepare?: boolean;\n with?: string | string[];\n configOptions?: GetConfigurationOptions;\n};\n\n/**\n * Get locales dictionaries .content.{json|ts|tsx|js|jsx|mjs|cjs} and build the JSON dictionaries in the .intlayer directory.\n * Watch mode available to get the change in the .content.{json|ts|tsx|js|jsx|mjs|cjs}\n */\nexport const build = async (options?: BuildOptions) => {\n const config = getConfiguration(options?.configOptions);\n logConfigDetails(options?.configOptions);\n\n let parallelProcess: ParallelHandle | null = null;\n\n // Only watch mode keeps a watcher alive; a one-off build has nothing to\n // deduplicate. Claimed before the child is spawned so the bundler plugin\n // inside it stands down instead of racing this command.\n if (options?.watch) {\n const bundlerAlreadyWatching = await claimCliContentWatcher(\n config,\n 'intlayer build --watch'\n );\n\n if (bundlerAlreadyWatching) {\n reportRedundantContentWatcher(config, {\n cliLabel: 'intlayer build --watch',\n bundlerLabel: bundlerAlreadyWatching.label,\n });\n }\n }\n\n if (options?.with) {\n parallelProcess = runParallel(options.with);\n // Handle the promise to avoid unhandled rejection\n parallelProcess.result.catch(() => {\n // Parallel process failed or was terminated\n });\n }\n\n await buildAndWatchIntlayer({\n persistent: options?.watch ?? false,\n skipPrepare: options?.skipPrepare ?? false,\n configuration: config,\n });\n\n if (!options?.watch && parallelProcess) {\n parallelProcess.kill();\n }\n};\n"],"mappings":";;;;;;;;;;AAwBA,MAAa,QAAQ,OAAO,YAA2B;CACrD,MAAM,SAAS,iBAAiB,SAAS,aAAa;CACtD,iBAAiB,SAAS,aAAa;CAEvC,IAAI,kBAAyC;CAK7C,IAAI,SAAS,OAAO;EAClB,MAAM,yBAAyB,MAAM,uBACnC,QACA,wBACF;EAEA,IAAI,wBACF,8BAA8B,QAAQ;GACpC,UAAU;GACV,cAAc,uBAAuB;EACvC,CAAC;CAEL;CAEA,IAAI,SAAS,MAAM;EACjB,kBAAkB,YAAY,QAAQ,IAAI;EAE1C,gBAAgB,OAAO,YAAY,CAEnC,CAAC;CACH;CAEA,MAAM,sBAAsB;EAC1B,YAAY,SAAS,SAAS;EAC9B,aAAa,SAAS,eAAe;EACrC,eAAe;CACjB,CAAC;CAED,IAAI,CAAC,SAAS,SAAS,iBACrB,gBAAgB,KAAK;AAEzB"}
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 locale paths crawlable",
11
- sitemap_sitemapPresent: "sitemap.xml present",
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
- url_hasLocalizedLinks: "localized internal links",
21
+ url_hreflangReciprocal: "hreflang alternates link back",
20
22
  url_hasXDefault: "x-default hreflang",
21
- url_allAnchorsLocalized: "all internal links localized",
22
- url_currentLocale: "current locale detected",
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
- appLogger("\nChecks:");
57
- for (const event of result.events) {
58
- const type = event.type.split("\\")[0];
59
- if (!type) continue;
60
- const label = checkLabels[type] ?? type;
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}` : ""}`);
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:");
@@ -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, type ScanEvent, scanWebsite } 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 locale paths crawlable',\n sitemap_sitemapPresent: 'sitemap.xml 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_hasCanonical: 'canonical link',\n url_hreflang: 'hreflang tags',\n url_hasLocalizedLinks: 'localized internal links',\n url_hasXDefault: 'x-default hreflang',\n url_allAnchorsLocalized: 'all internal links localized',\n url_currentLocale: 'current locale detected',\n url_unusedBundleContent: 'unused bundle locale content',\n};\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/**\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 appLogger('\\nChecks:');\n for (const event of result.events) {\n const type = event.type.split('\\\\')[0];\n\n if (!type) continue;\n\n const label = checkLabels[type] ?? type;\n appLogger(` ${statusIcon(event.status)} ${label}`);\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":";;;;;;;AAgBA,MAAM,cAAsC;CAC1C,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;AAEA,MAAM,cAAc,WAAwC;CAC1D,IAAI,WAAW,WAAW,OAAO,SAAS,KAAK,WAAW,KAAK;CAC/D,IAAI,WAAW,WAAW,OAAO,SAAS,KAAK,WAAW,MAAM;CAChE,OAAO,SAAS,KAAK,WAAW,GAAG;AACrC;AAEA,MAAM,cAAc,UAAkB;CACpC,IAAI,SAAS,IAAI,OAAO,WAAW;CACnC,IAAI,SAAS,IAAI,OAAO,WAAW;CACnC,OAAO,WAAW;AACpB;;;;;;;;;;AAWA,MAAa,OAAO,OAClB,KACA,UAA8B,CAAC,MACb;CAClB,MAAM,gBAAgB,iBAAiB,QAAQ,aAAa;CAC5D,MAAM,YAAY,aAAa,aAAa;CAE5C,MAAM,SAAS,MAAM,YAAY,KAAK,EAAE,MAAM,QAAQ,KAAK,CAAC;CAE5D,IAAI,QAAQ,MAAM;EAChB,UAAU,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;EACzC;CACF;CAEA,UACE,gBAAgB,SAAS,OAAO,KAAK,WAAW,UAAU,EAAE,GAAG,SAC7D,IAAI,OAAO,KAAK,SAChB,WAAW,IACb,EAAE,GACJ;CAEA,UACE,UAAU,SAAS,GAAG,OAAO,MAAM,OAAO,WAAW,OAAO,KAAK,CAAC,GACpE;CACA,UACE,cAAc,SAAS,WAAW,OAAO,aAAa,GAAG,WAAW,IAAI,EAAE,GAAG,SAC3E,SAAS,WAAW,OAAO,QAAQ,EAAE,IACrC,WAAW,IACb,GACF;CACA,IAAI,OAAO,QAAQ,SAAS,GAC1B,UACE,YAAY,SAAS,OAAO,QAAQ,KAAK,IAAI,GAAG,WAAW,KAAK,GAClE;CAGF,UAAU,WAAW;CACrB,KAAK,MAAM,SAAS,OAAO,QAAQ;EACjC,MAAM,OAAO,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC;EAEpC,IAAI,CAAC,MAAM;EAEX,MAAM,QAAQ,YAAY,SAAS;EACnC,UAAU,KAAK,WAAW,MAAM,MAAM,EAAE,GAAG,OAAO;CACpD;CAEA,IAAI,OAAO,UAAU,OAAO,OAAO,kBAAkB,GAAG;EACtD,MAAM,EAAE,WAAW;EACnB,UAAU,yBAAyB;EACnC,UACE,2BAA2B,SAAS,WAAW,OAAO,eAAe,GAAG,WAAW,IAAI,GACzF;EACA,UACE,6BAA6B,SAC3B,GAAG,WAAW,OAAO,qBAAqB,EAAE,IAAI,OAAO,sBAAsB,KAC7E,OAAO,wBAAwB,KAC3B,WAAW,MACX,OAAO,wBAAwB,IAC7B,WAAW,SACX,WAAW,KACnB,GACF;CACF;AACF"}
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 }`\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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"scan.d.ts","names":[],"sources":["../../src/scan.ts"],"mappings":";;;YAOY;;EAEV;;EAEA;EACA,gBAAgB;;;;;;;;;;;qBA2CL,OAAI,aACJ,UACF,uBACR"}
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.10",
3
+ "version": "9.5.12",
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": [
@@ -38,6 +38,11 @@
38
38
  "require": "./dist/cjs/index.cjs",
39
39
  "import": "./dist/esm/index.mjs"
40
40
  },
41
+ "./cli": {
42
+ "types": "./dist/types/cli.d.ts",
43
+ "require": "./dist/cjs/cli.cjs",
44
+ "import": "./dist/esm/cli.mjs"
45
+ },
41
46
  "./package.json": "./package.json"
42
47
  },
43
48
  "main": "dist/cjs/index.cjs",
@@ -67,21 +72,21 @@
67
72
  "typecheck": "tsc --noEmit --project tsconfig.types.json"
68
73
  },
69
74
  "dependencies": {
70
- "@clack/prompts": "1.7.0",
71
- "@intlayer/api": "9.5.10",
72
- "@intlayer/babel": "9.5.10",
73
- "@intlayer/config": "9.5.10",
74
- "@intlayer/core": "9.5.10",
75
- "@intlayer/dictionaries-entry": "9.5.10",
76
- "@intlayer/engine": "9.5.10",
77
- "@intlayer/types": "9.5.10",
75
+ "@clack/prompts": "1.8.1",
76
+ "@intlayer/api": "9.5.12",
77
+ "@intlayer/babel": "9.5.12",
78
+ "@intlayer/config": "9.5.12",
79
+ "@intlayer/core": "9.5.12",
80
+ "@intlayer/dictionaries-entry": "9.5.12",
81
+ "@intlayer/engine": "9.5.12",
82
+ "@intlayer/types": "9.5.12",
78
83
  "commander": "15.0.0",
79
84
  "enquirer": "2.4.1",
80
- "eventsource": "5.1.1",
85
+ "eventsource": "5.1.2",
81
86
  "fast-glob": "3.3.3"
82
87
  },
83
88
  "devDependencies": {
84
- "@intlayer/ai": "9.5.10",
89
+ "@intlayer/ai": "9.5.12",
85
90
  "@types/node": "^22",
86
91
  "@utils/ts-config": "1.0.4",
87
92
  "@utils/ts-config-types": "1.0.4",
@@ -92,7 +97,7 @@
92
97
  "vitest": "5.0.2"
93
98
  },
94
99
  "peerDependencies": {
95
- "@intlayer/ai": "9.5.10"
100
+ "@intlayer/ai": "9.5.12"
96
101
  },
97
102
  "peerDependenciesMeta": {
98
103
  "@intlayer/ai": {