eslint-plugin-intlayer 9.3.1 → 9.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +103 -49
  2. package/dist/cjs/@intlayer/config/dist/esm/extract/constants.cjs +18 -0
  3. package/dist/cjs/@intlayer/config/dist/esm/extract/constants.cjs.map +1 -0
  4. package/dist/cjs/@intlayer/config/dist/esm/extract/shouldExtract.cjs +37 -0
  5. package/dist/cjs/@intlayer/config/dist/esm/extract/shouldExtract.cjs.map +1 -0
  6. package/dist/cjs/_virtual/_rolldown/runtime.cjs +28 -0
  7. package/dist/cjs/index.cjs +41 -29
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/preset.cjs +53 -0
  10. package/dist/cjs/preset.cjs.map +1 -0
  11. package/dist/cjs/rules/enforceAdapterImport.cjs +9 -2
  12. package/dist/cjs/rules/enforceAdapterImport.cjs.map +1 -1
  13. package/dist/cjs/rules/noRawText.cjs +4 -3
  14. package/dist/cjs/rules/noRawText.cjs.map +1 -1
  15. package/dist/cjs/rules/noUnusedContent.cjs +143 -0
  16. package/dist/cjs/rules/noUnusedContent.cjs.map +1 -0
  17. package/dist/cjs/rules/staticDictionaryKey.cjs +2 -2
  18. package/dist/cjs/rules/staticDictionaryKey.cjs.map +1 -1
  19. package/dist/cjs/utils/contentDeclaration.cjs +100 -0
  20. package/dist/cjs/utils/contentDeclaration.cjs.map +1 -0
  21. package/dist/cjs/utils/projectUsage.cjs +437 -0
  22. package/dist/cjs/utils/projectUsage.cjs.map +1 -0
  23. package/dist/cjs/utils/reactVersion.cjs +50 -0
  24. package/dist/cjs/utils/reactVersion.cjs.map +1 -0
  25. package/dist/esm/@intlayer/config/dist/esm/extract/constants.mjs +18 -0
  26. package/dist/esm/@intlayer/config/dist/esm/extract/constants.mjs.map +1 -0
  27. package/dist/esm/@intlayer/config/dist/esm/extract/shouldExtract.mjs +37 -0
  28. package/dist/esm/@intlayer/config/dist/esm/extract/shouldExtract.mjs.map +1 -0
  29. package/dist/esm/_virtual/_rolldown/runtime.mjs +8 -0
  30. package/dist/esm/index.mjs +36 -30
  31. package/dist/esm/index.mjs.map +1 -1
  32. package/dist/esm/preset.mjs +48 -0
  33. package/dist/esm/preset.mjs.map +1 -0
  34. package/dist/esm/rules/enforceAdapterImport.mjs +10 -3
  35. package/dist/esm/rules/enforceAdapterImport.mjs.map +1 -1
  36. package/dist/esm/rules/noRawText.mjs +2 -1
  37. package/dist/esm/rules/noRawText.mjs.map +1 -1
  38. package/dist/esm/rules/noUnusedContent.mjs +142 -0
  39. package/dist/esm/rules/noUnusedContent.mjs.map +1 -0
  40. package/dist/esm/rules/staticDictionaryKey.mjs +2 -2
  41. package/dist/esm/rules/staticDictionaryKey.mjs.map +1 -1
  42. package/dist/esm/utils/contentDeclaration.mjs +99 -0
  43. package/dist/esm/utils/contentDeclaration.mjs.map +1 -0
  44. package/dist/esm/utils/projectUsage.mjs +433 -0
  45. package/dist/esm/utils/projectUsage.mjs.map +1 -0
  46. package/dist/esm/utils/reactVersion.mjs +46 -0
  47. package/dist/esm/utils/reactVersion.mjs.map +1 -0
  48. package/dist/types/index.d.ts +19 -6
  49. package/dist/types/index.d.ts.map +1 -1
  50. package/dist/types/node_modules/.bun/@types_estree@1.0.9/node_modules/@types/estree/index.d.ts +528 -0
  51. package/dist/types/node_modules/.bun/@types_estree@1.0.9/node_modules/@types/estree/index.d.ts.map +1 -0
  52. package/dist/types/preset.d.ts +45 -0
  53. package/dist/types/preset.d.ts.map +1 -0
  54. package/dist/types/rules/enforceAdapterImport.d.ts.map +1 -1
  55. package/dist/types/rules/noUnusedContent.d.ts +33 -0
  56. package/dist/types/rules/noUnusedContent.d.ts.map +1 -0
  57. package/dist/types/utils/ast.d.ts +1 -1
  58. package/dist/types/utils/contentDeclaration.d.ts +42 -0
  59. package/dist/types/utils/contentDeclaration.d.ts.map +1 -0
  60. package/dist/types/utils/projectUsage.d.ts +102 -0
  61. package/dist/types/utils/projectUsage.d.ts.map +1 -0
  62. package/dist/types/utils/reactVersion.d.ts +18 -0
  63. package/dist/types/utils/reactVersion.d.ts.map +1 -0
  64. package/package.json +5 -3
@@ -1 +1 @@
1
- {"version":3,"file":"enforceAdapterImport.cjs","names":["ALL_CALLERS","createRule","toAstNode"],"sources":["../../../src/rules/enforceAdapterImport.ts"],"sourcesContent":["import { ALL_CALLERS } from '@intlayer/config/callers';\nimport { type AstNode, toAstNode } from '../utils/ast';\nimport { createRule } from '../utils/createRule';\n\ntype MessageIds = 'useAdapterImport';\n\nconst ADAPTER_SCOPE = '@intlayer/';\n\n/**\n * Original library specifier → the `@intlayer/*` adapter that replaces it.\n *\n * Derived from the shared registry rather than hard-coded: every compat\n * descriptor lists both its original specifiers and their adapter equivalents in\n * `importSources`, so a library added to `@intlayer/config/callers` is covered\n * here for free.\n *\n * Sub-paths line up by position within a descriptor — `next-intl/server` and\n * `@intlayer/next-intl/server` are listed together — so the mapping is built by\n * matching each original against the adapter that ends with the same tail.\n */\nconst buildAdapterMap = (): Map<string, string> => {\n const adapterBySpecifier = new Map<string, string>();\n\n for (const descriptor of ALL_CALLERS) {\n const adapters = descriptor.importSources.filter((source) =>\n source.startsWith(ADAPTER_SCOPE)\n );\n\n if (adapters.length === 0) continue;\n\n const originals = descriptor.importSources.filter(\n (source) => !source.startsWith(ADAPTER_SCOPE)\n );\n\n for (const original of originals) {\n // `next-intl/server` → prefer `@intlayer/next-intl/server` over\n // `@intlayer/next-intl`, so match on the longest shared suffix.\n const adapter =\n adapters.find(\n (candidate) => candidate === `${ADAPTER_SCOPE}${original}`\n ) ??\n adapters.find((candidate) => candidate.endsWith(`/${original}`)) ??\n adapters[0];\n\n if (adapter && !adapterBySpecifier.has(original)) {\n adapterBySpecifier.set(original, adapter);\n }\n }\n }\n\n return adapterBySpecifier;\n};\n\nconst ADAPTER_BY_SPECIFIER = buildAdapterMap();\n\nexport const enforceAdapterImport = createRule<[], MessageIds>(\n 'enforce-adapter-import',\n {\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Import compat-library APIs from their `@intlayer/*` adapter rather than the original package',\n },\n messages: {\n useAdapterImport:\n 'Import from `{{adapter}}` instead of `{{original}}`. The original specifier only resolves to Intlayer when the bundler alias is configured; the adapter always does.',\n },\n },\n\n create: (context) => {\n /** Report and rewrite a module specifier that has an adapter equivalent. */\n const checkSource = (node: AstNode | null | undefined): void => {\n if (!node) return;\n\n const original = node['value'];\n\n if (typeof original !== 'string') return;\n\n const adapter = ADAPTER_BY_SPECIFIER.get(original);\n\n if (!adapter) return;\n\n context.report({\n node: node as never,\n messageId: 'useAdapterImport',\n data: { adapter, original },\n fix: (fixer) => {\n const raw = (node['raw'] as string | undefined) ?? `'${original}'`;\n const quote = raw.startsWith('\"') ? '\"' : \"'\";\n\n return fixer.replaceText(\n node as never,\n `${quote}${adapter}${quote}`\n );\n },\n });\n };\n\n return {\n ImportDeclaration: (node) => {\n checkSource(toAstNode(node)['source'] as AstNode);\n },\n\n ExportNamedDeclaration: (node) => {\n checkSource(toAstNode(node)['source'] as AstNode);\n },\n\n ExportAllDeclaration: (node) => {\n checkSource(toAstNode(node)['source'] as AstNode);\n },\n\n ImportExpression: (node) => {\n checkSource(toAstNode(node)['source'] as AstNode);\n },\n };\n },\n }\n);\n"],"mappings":";;;;;;AAMA,MAAM,gBAAgB;;;;;;;;;;;;;AActB,MAAM,wBAA6C;CACjD,MAAM,qCAAqB,IAAI,IAAoB;CAEnD,KAAK,MAAM,cAAcA,sCAAa;EACpC,MAAM,WAAW,WAAW,cAAc,QAAQ,WAChD,OAAO,WAAW,aAAa,CACjC;EAEA,IAAI,SAAS,WAAW,GAAG;EAE3B,MAAM,YAAY,WAAW,cAAc,QACxC,WAAW,CAAC,OAAO,WAAW,aAAa,CAC9C;EAEA,KAAK,MAAM,YAAY,WAAW;GAGhC,MAAM,UACJ,SAAS,MACN,cAAc,cAAc,GAAG,gBAAgB,UAClD,KACA,SAAS,MAAM,cAAc,UAAU,SAAS,IAAI,UAAU,CAAC,KAC/D,SAAS;GAEX,IAAI,WAAW,CAAC,mBAAmB,IAAI,QAAQ,GAC7C,mBAAmB,IAAI,UAAU,OAAO;EAE5C;CACF;CAEA,OAAO;AACT;AAEA,MAAM,uBAAuB,gBAAgB;AAE7C,MAAa,uBAAuBC,oCAClC,0BACA;CACE,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,+FACJ;EACA,UAAU,EACR,kBACE,uKACJ;CACF;CAEA,SAAS,YAAY;;EAEnB,MAAM,eAAe,SAA2C;GAC9D,IAAI,CAAC,MAAM;GAEX,MAAM,WAAW,KAAK;GAEtB,IAAI,OAAO,aAAa,UAAU;GAElC,MAAM,UAAU,qBAAqB,IAAI,QAAQ;GAEjD,IAAI,CAAC,SAAS;GAEd,QAAQ,OAAO;IACP;IACN,WAAW;IACX,MAAM;KAAE;KAAS;IAAS;IAC1B,MAAM,UAAU;KAEd,MAAM,SADO,KAAK,UAAiC,IAAI,SAAS,GAC/C,CAAC,WAAW,IAAG,IAAI,OAAM;KAE1C,OAAO,MAAM,YACX,MACA,GAAG,QAAQ,UAAU,OACvB;IACF;GACF,CAAC;EACH;EAEA,OAAO;GACL,oBAAoB,SAAS;IAC3B,YAAYC,4BAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;GAEA,yBAAyB,SAAS;IAChC,YAAYA,4BAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;GAEA,uBAAuB,SAAS;IAC9B,YAAYA,4BAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;GAEA,mBAAmB,SAAS;IAC1B,YAAYA,4BAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;EACF;CACF;AACF,CACF"}
1
+ {"version":3,"file":"enforceAdapterImport.cjs","names":["COMPAT_CALLERS","createRule","toAstNode"],"sources":["../../../src/rules/enforceAdapterImport.ts"],"sourcesContent":["import { COMPAT_CALLERS } from '@intlayer/config/callers';\nimport { type AstNode, toAstNode } from '../utils/ast';\nimport { createRule } from '../utils/createRule';\n\ntype MessageIds = 'useAdapterImport';\n\nconst ADAPTER_SCOPE = '@intlayer/';\n\n/**\n * Original third-party library specifier → the `@intlayer/*` adapter that\n * replaces it, e.g. `next-intl` → `@intlayer/next-intl`.\n *\n * Derived from the shared registry rather than hard-coded: every compat\n * descriptor lists both its original specifiers and their adapter equivalents in\n * `importSources`, so a library added to `@intlayer/config/callers` is covered\n * here for free.\n *\n * Built from `COMPAT_CALLERS` only, never `ALL_CALLERS`. Intlayer's own packages\n * are not a migration pair: `BASE_CALLERS` lists `intlayer` alongside\n * `@intlayer/core` because `getIntlayer` is importable from both, and treating\n * the scoped one as an \"adapter\" would tell users to rewrite ordinary\n * `from 'intlayer'` imports.\n *\n * Sub-paths line up by position within a descriptor — `next-intl/server` and\n * `@intlayer/next-intl/server` are listed together — so the mapping is built by\n * matching each original against the adapter that ends with the same tail.\n */\nconst buildAdapterMap = (): Map<string, string> => {\n const adapterBySpecifier = new Map<string, string>();\n\n for (const descriptor of COMPAT_CALLERS) {\n const adapters = descriptor.importSources.filter((source) =>\n source.startsWith(ADAPTER_SCOPE)\n );\n\n if (adapters.length === 0) continue;\n\n const originals = descriptor.importSources.filter(\n (source) => !source.startsWith(ADAPTER_SCOPE)\n );\n\n for (const original of originals) {\n // `next-intl/server` → prefer `@intlayer/next-intl/server` over\n // `@intlayer/next-intl`, so match on the longest shared suffix.\n const adapter =\n adapters.find(\n (candidate) => candidate === `${ADAPTER_SCOPE}${original}`\n ) ??\n adapters.find((candidate) => candidate.endsWith(`/${original}`)) ??\n adapters[0];\n\n if (adapter && !adapterBySpecifier.has(original)) {\n adapterBySpecifier.set(original, adapter);\n }\n }\n }\n\n return adapterBySpecifier;\n};\n\nconst ADAPTER_BY_SPECIFIER = buildAdapterMap();\n\nexport const enforceAdapterImport = createRule<[], MessageIds>(\n 'enforce-adapter-import',\n {\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Import compat-library APIs from their `@intlayer/*` adapter rather than the original package',\n },\n messages: {\n useAdapterImport:\n 'Import from `{{adapter}}` instead of `{{original}}`. The original specifier only resolves to Intlayer when the bundler alias is configured; the adapter always does.',\n },\n },\n\n create: (context) => {\n /** Report and rewrite a module specifier that has an adapter equivalent. */\n const checkSource = (node: AstNode | null | undefined): void => {\n if (!node) return;\n\n const original = node['value'];\n\n if (typeof original !== 'string') return;\n\n const adapter = ADAPTER_BY_SPECIFIER.get(original);\n\n if (!adapter) return;\n\n context.report({\n node: node as never,\n messageId: 'useAdapterImport',\n data: { adapter, original },\n fix: (fixer) => {\n const raw = (node['raw'] as string | undefined) ?? `'${original}'`;\n const quote = raw.startsWith('\"') ? '\"' : \"'\";\n\n return fixer.replaceText(\n node as never,\n `${quote}${adapter}${quote}`\n );\n },\n });\n };\n\n return {\n ImportDeclaration: (node) => {\n checkSource(toAstNode(node)['source'] as AstNode);\n },\n\n ExportNamedDeclaration: (node) => {\n checkSource(toAstNode(node)['source'] as AstNode);\n },\n\n ExportAllDeclaration: (node) => {\n checkSource(toAstNode(node)['source'] as AstNode);\n },\n\n ImportExpression: (node) => {\n checkSource(toAstNode(node)['source'] as AstNode);\n },\n };\n },\n }\n);\n"],"mappings":";;;;;;AAMA,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;AAqBtB,MAAM,wBAA6C;CACjD,MAAM,qCAAqB,IAAI,IAAoB;CAEnD,KAAK,MAAM,cAAcA,yCAAgB;EACvC,MAAM,WAAW,WAAW,cAAc,QAAQ,WAChD,OAAO,WAAW,aAAa,CACjC;EAEA,IAAI,SAAS,WAAW,GAAG;EAE3B,MAAM,YAAY,WAAW,cAAc,QACxC,WAAW,CAAC,OAAO,WAAW,aAAa,CAC9C;EAEA,KAAK,MAAM,YAAY,WAAW;GAGhC,MAAM,UACJ,SAAS,MACN,cAAc,cAAc,GAAG,gBAAgB,UAClD,KACA,SAAS,MAAM,cAAc,UAAU,SAAS,IAAI,UAAU,CAAC,KAC/D,SAAS;GAEX,IAAI,WAAW,CAAC,mBAAmB,IAAI,QAAQ,GAC7C,mBAAmB,IAAI,UAAU,OAAO;EAE5C;CACF;CAEA,OAAO;AACT;AAEA,MAAM,uBAAuB,gBAAgB;AAE7C,MAAa,uBAAuBC,oCAClC,0BACA;CACE,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,+FACJ;EACA,UAAU,EACR,kBACE,uKACJ;CACF;CAEA,SAAS,YAAY;;EAEnB,MAAM,eAAe,SAA2C;GAC9D,IAAI,CAAC,MAAM;GAEX,MAAM,WAAW,KAAK;GAEtB,IAAI,OAAO,aAAa,UAAU;GAElC,MAAM,UAAU,qBAAqB,IAAI,QAAQ;GAEjD,IAAI,CAAC,SAAS;GAEd,QAAQ,OAAO;IACP;IACN,WAAW;IACX,MAAM;KAAE;KAAS;IAAS;IAC1B,MAAM,UAAU;KAEd,MAAM,SADO,KAAK,UAAiC,IAAI,SAAS,GAC/C,CAAC,WAAW,IAAG,IAAI,OAAM;KAE1C,OAAO,MAAM,YACX,MACA,GAAG,QAAQ,UAAU,OACvB;IACF;GACF,CAAC;EACH;EAEA,OAAO;GACL,oBAAoB,SAAS;IAC3B,YAAYC,4BAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;GAEA,yBAAyB,SAAS;IAChC,YAAYA,4BAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;GAEA,uBAAuB,SAAS;IAC9B,YAAYA,4BAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;GAEA,mBAAmB,SAAS;IAC1B,YAAYA,4BAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;EACF;CACF;AACF,CACF"}
@@ -1,9 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_utils_ast = require('../utils/ast.cjs');
3
3
  const require_utils_createRule = require('../utils/createRule.cjs');
4
+ const require_shouldExtract = require('../@intlayer/config/dist/esm/extract/shouldExtract.cjs');
5
+ const require_constants = require('../@intlayer/config/dist/esm/extract/constants.cjs');
4
6
  const require_utils_templates = require('../utils/templates.cjs');
5
7
  let _intlayer_config_defaultValues = require("@intlayer/config/defaultValues");
6
- let _intlayer_config_extract = require("@intlayer/config/extract");
7
8
 
8
9
  //#region src/rules/noRawText.ts
9
10
  /**
@@ -93,12 +94,12 @@ const noRawText = require_utils_createRule.createRule("no-raw-text", {
93
94
  const filename = context.filename;
94
95
  if (isContentDeclarationFile(filename)) return {};
95
96
  const [options = {}] = context.options;
96
- const attributes = new Set(options.attributes ?? _intlayer_config_extract.ATTRIBUTES_TO_EXTRACT);
97
+ const attributes = new Set(options.attributes ?? require_constants.ATTRIBUTES_TO_EXTRACT);
97
98
  const ignoredElements = new Set(options.ignoreElements ?? DEFAULT_IGNORED_ELEMENTS);
98
99
  const ignorePatterns = (options.ignorePatterns ?? []).map((pattern) => new RegExp(pattern, "u"));
99
100
  const includeStringLiterals = options.includeStringLiterals === true;
100
101
  /** True when the text is copy the extractor would pick up. */
101
- const isReportableText = (text) => (0, _intlayer_config_extract.shouldExtract)(text) && !ignorePatterns.some((pattern) => pattern.test(text.trim()));
102
+ const isReportableText = (text) => require_shouldExtract.shouldExtract(text) && !ignorePatterns.some((pattern) => pattern.test(text.trim()));
102
103
  /** Collapse whitespace so multi-line JSX text reads well in the report. */
103
104
  const formatText = (text) => {
104
105
  const normalized = text.replace(/\s+/gu, " ").trim();
@@ -1 +1 @@
1
- {"version":3,"file":"noRawText.cjs","names":["FILE_EXTENSIONS","getJsxElementName","createRule","ATTRIBUTES_TO_EXTRACT","shouldExtract","createTemplateVisitor","combineVisitors","toAstNode","getJsxAttributeName","getJsxAttributeValueNode","getStaticStringValue"],"sources":["../../../src/rules/noRawText.ts"],"sourcesContent":["import { FILE_EXTENSIONS } from '@intlayer/config/defaultValues';\nimport { ATTRIBUTES_TO_EXTRACT, shouldExtract } from '@intlayer/config/extract';\nimport {\n type AstNode,\n type AstNodeWithParent,\n getJsxAttributeName,\n getJsxAttributeValueNode,\n getJsxElementName,\n getStaticStringValue,\n toAstNode,\n} from '../utils/ast';\nimport { createRule } from '../utils/createRule';\nimport {\n combineVisitors,\n createTemplateVisitor,\n type TemplateVisitor,\n} from '../utils/templates';\n\n/**\n * Elements whose text is markup, code or metadata rather than user-facing copy.\n * Extracting them would be wrong, so they are skipped by default.\n */\nconst DEFAULT_IGNORED_ELEMENTS = ['code', 'pre', 'script', 'style'];\n\nexport type NoRawTextOptions = {\n /**\n * JSX attributes whose string value is user-facing copy.\n * Defaults to the compiler's own list, so the linter flags exactly what\n * `intlayer extract` would rewrite.\n */\n attributes?: string[];\n /** JSX elements whose children are never translatable. */\n ignoreElements?: string[];\n /** Regular-expression sources for text that should never be reported. */\n ignorePatterns?: string[];\n /**\n * Also report string literals outside JSX (function arguments, object values,\n * …). Off by default: outside JSX the false-positive rate is much higher, and\n * the compiler's own extractor is the better first pass.\n */\n includeStringLiterals?: boolean;\n};\n\ntype MessageIds = 'rawText' | 'rawAttribute' | 'rawStringLiteral';\n\n/**\n * True for a content declaration file, where raw strings are the whole point.\n *\n * @param filename - Absolute path of the linted file.\n */\nconst isContentDeclarationFile = (filename: string): boolean =>\n FILE_EXTENSIONS.some((extension) => filename.endsWith(extension));\n\n/**\n * Walk up from a JSX child or attribute to the tag name that encloses it.\n *\n * @param node - The reported node.\n */\nconst getEnclosingElementName = (\n node: AstNodeWithParent | undefined\n): string | null => {\n let current = node;\n\n while (current) {\n if (current.type === 'JSXElement') {\n return getJsxElementName(current['openingElement'] as AstNode);\n }\n\n if (current.type === 'JSXOpeningElement') {\n return getJsxElementName(current);\n }\n\n current = current.parent;\n }\n\n return null;\n};\n\n/**\n * True when the string literal sits in a position that is structural rather\n * than user-facing: module specifiers, property keys, directives, and the\n * arguments of the Intlayer getters themselves.\n *\n * @param node - The Literal node.\n */\nconst isStructuralPosition = (node: AstNodeWithParent): boolean => {\n const parent = node.parent;\n\n if (!parent) return true;\n\n switch (parent.type) {\n case 'ImportDeclaration':\n case 'ImportExpression':\n case 'ExportNamedDeclaration':\n case 'ExportAllDeclaration':\n case 'JSXAttribute':\n case 'TSLiteralType':\n case 'TSEnumMember':\n case 'ExpressionStatement':\n return true;\n\n case 'Property':\n // Keys are structural; values are not.\n return parent['key'] === node && parent['computed'] !== true;\n\n case 'MemberExpression':\n return parent['property'] === node;\n\n default:\n return false;\n }\n};\n\nexport const noRawText = createRule<[NoRawTextOptions?], MessageIds>(\n 'no-raw-text',\n {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Report user-facing strings that are not declared in an Intlayer dictionary',\n },\n messages: {\n rawText:\n 'Untranslated text \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n rawAttribute:\n 'Untranslated `{{attribute}}` value \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n rawStringLiteral:\n 'Untranslated string \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n attributes: { type: 'array', items: { type: 'string' } },\n ignoreElements: { type: 'array', items: { type: 'string' } },\n ignorePatterns: { type: 'array', items: { type: 'string' } },\n includeStringLiterals: { type: 'boolean' },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create: (context) => {\n const filename = context.filename;\n\n // Content declaration files hold the translations themselves.\n if (isContentDeclarationFile(filename)) return {};\n\n const [options = {}] = context.options;\n const attributes = new Set<string>(\n options.attributes ?? ATTRIBUTES_TO_EXTRACT\n );\n const ignoredElements = new Set<string>(\n options.ignoreElements ?? DEFAULT_IGNORED_ELEMENTS\n );\n const ignorePatterns = (options.ignorePatterns ?? []).map(\n (pattern) => new RegExp(pattern, 'u')\n );\n const includeStringLiterals = options.includeStringLiterals === true;\n\n /** True when the text is copy the extractor would pick up. */\n const isReportableText = (text: string): boolean =>\n shouldExtract(text) &&\n !ignorePatterns.some((pattern) => pattern.test(text.trim()));\n\n /** Collapse whitespace so multi-line JSX text reads well in the report. */\n const formatText = (text: string): string => {\n const normalized = text.replace(/\\s+/gu, ' ').trim();\n\n return normalized.length > 40\n ? `${normalized.slice(0, 40)}…`\n : normalized;\n };\n\n /**\n * Handlers for the markup languages that are not JSX — Vue, Svelte and\n * Angular templates. Reports through the same predicate and options as\n * the JSX path so a project gets identical results whatever it is written\n * in.\n */\n const templateVisitor = createTemplateVisitor(\n ({ node, text, attributeName, elementName }) => {\n if (elementName && ignoredElements.has(elementName)) return;\n\n if (attributeName !== null && !attributes.has(attributeName)) return;\n\n if (!isReportableText(text)) return;\n\n context.report({\n node: node as never,\n messageId: attributeName === null ? 'rawText' : 'rawAttribute',\n data: {\n text: formatText(text),\n ...(attributeName === null ? {} : { attribute: attributeName }),\n },\n });\n }\n );\n\n const scriptVisitor: TemplateVisitor = {\n JSXText: (node) => {\n const astNode = toAstNode(node as never);\n const text = astNode['value'] as string;\n\n if (!isReportableText(text)) return;\n\n const elementName = getEnclosingElementName(astNode.parent);\n\n if (elementName && ignoredElements.has(elementName)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawText',\n data: { text: formatText(text) },\n });\n },\n\n JSXAttribute: (node) => {\n const astNode = toAstNode(node as never);\n const attributeName = getJsxAttributeName(astNode);\n\n if (!attributeName || !attributes.has(attributeName)) return;\n\n const valueNode = getJsxAttributeValueNode(astNode);\n const text = getStaticStringValue(valueNode);\n\n if (text === null || !isReportableText(text)) return;\n\n const elementName = getEnclosingElementName(astNode.parent);\n\n if (elementName && ignoredElements.has(elementName)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawAttribute',\n data: { attribute: attributeName, text: formatText(text) },\n });\n },\n\n Literal: (node) => {\n if (!includeStringLiterals) return;\n\n const astNode = toAstNode(node as never);\n\n if (typeof astNode['value'] !== 'string') return;\n\n if (isStructuralPosition(astNode)) return;\n\n const text = astNode['value'] as string;\n\n if (!isReportableText(text)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawStringLiteral',\n data: { text: formatText(text) },\n });\n },\n };\n\n return combineVisitors(\n context.sourceCode.parserServices,\n scriptVisitor,\n templateVisitor\n );\n },\n }\n);\n"],"mappings":"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"}
1
+ {"version":3,"file":"noRawText.cjs","names":["FILE_EXTENSIONS","getJsxElementName","createRule","ATTRIBUTES_TO_EXTRACT","shouldExtract","createTemplateVisitor","combineVisitors","toAstNode","getJsxAttributeName","getJsxAttributeValueNode","getStaticStringValue"],"sources":["../../../src/rules/noRawText.ts"],"sourcesContent":["import { FILE_EXTENSIONS } from '@intlayer/config/defaultValues';\nimport { ATTRIBUTES_TO_EXTRACT, shouldExtract } from '@intlayer/config/extract';\nimport {\n type AstNode,\n type AstNodeWithParent,\n getJsxAttributeName,\n getJsxAttributeValueNode,\n getJsxElementName,\n getStaticStringValue,\n toAstNode,\n} from '../utils/ast';\nimport { createRule } from '../utils/createRule';\nimport {\n combineVisitors,\n createTemplateVisitor,\n type TemplateVisitor,\n} from '../utils/templates';\n\n/**\n * Elements whose text is markup, code or metadata rather than user-facing copy.\n * Extracting them would be wrong, so they are skipped by default.\n */\nconst DEFAULT_IGNORED_ELEMENTS = ['code', 'pre', 'script', 'style'];\n\nexport type NoRawTextOptions = {\n /**\n * JSX attributes whose string value is user-facing copy.\n * Defaults to the compiler's own list, so the linter flags exactly what\n * `intlayer extract` would rewrite.\n */\n attributes?: string[];\n /** JSX elements whose children are never translatable. */\n ignoreElements?: string[];\n /** Regular-expression sources for text that should never be reported. */\n ignorePatterns?: string[];\n /**\n * Also report string literals outside JSX (function arguments, object values,\n * …). Off by default: outside JSX the false-positive rate is much higher, and\n * the compiler's own extractor is the better first pass.\n */\n includeStringLiterals?: boolean;\n};\n\ntype MessageIds = 'rawText' | 'rawAttribute' | 'rawStringLiteral';\n\n/**\n * True for a content declaration file, where raw strings are the whole point.\n *\n * @param filename - Absolute path of the linted file.\n */\nconst isContentDeclarationFile = (filename: string): boolean =>\n FILE_EXTENSIONS.some((extension) => filename.endsWith(extension));\n\n/**\n * Walk up from a JSX child or attribute to the tag name that encloses it.\n *\n * @param node - The reported node.\n */\nconst getEnclosingElementName = (\n node: AstNodeWithParent | undefined\n): string | null => {\n let current = node;\n\n while (current) {\n if (current.type === 'JSXElement') {\n return getJsxElementName(current['openingElement'] as AstNode);\n }\n\n if (current.type === 'JSXOpeningElement') {\n return getJsxElementName(current);\n }\n\n current = current.parent;\n }\n\n return null;\n};\n\n/**\n * True when the string literal sits in a position that is structural rather\n * than user-facing: module specifiers, property keys, directives, and the\n * arguments of the Intlayer getters themselves.\n *\n * @param node - The Literal node.\n */\nconst isStructuralPosition = (node: AstNodeWithParent): boolean => {\n const parent = node.parent;\n\n if (!parent) return true;\n\n switch (parent.type) {\n case 'ImportDeclaration':\n case 'ImportExpression':\n case 'ExportNamedDeclaration':\n case 'ExportAllDeclaration':\n case 'JSXAttribute':\n case 'TSLiteralType':\n case 'TSEnumMember':\n case 'ExpressionStatement':\n return true;\n\n case 'Property':\n // Keys are structural; values are not.\n return parent['key'] === node && parent['computed'] !== true;\n\n case 'MemberExpression':\n return parent['property'] === node;\n\n default:\n return false;\n }\n};\n\nexport const noRawText = createRule<[NoRawTextOptions?], MessageIds>(\n 'no-raw-text',\n {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Report user-facing strings that are not declared in an Intlayer dictionary',\n },\n messages: {\n rawText:\n 'Untranslated text \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n rawAttribute:\n 'Untranslated `{{attribute}}` value \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n rawStringLiteral:\n 'Untranslated string \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n attributes: { type: 'array', items: { type: 'string' } },\n ignoreElements: { type: 'array', items: { type: 'string' } },\n ignorePatterns: { type: 'array', items: { type: 'string' } },\n includeStringLiterals: { type: 'boolean' },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create: (context) => {\n const filename = context.filename;\n\n // Content declaration files hold the translations themselves.\n if (isContentDeclarationFile(filename)) return {};\n\n const [options = {}] = context.options;\n const attributes = new Set<string>(\n options.attributes ?? ATTRIBUTES_TO_EXTRACT\n );\n const ignoredElements = new Set<string>(\n options.ignoreElements ?? DEFAULT_IGNORED_ELEMENTS\n );\n const ignorePatterns = (options.ignorePatterns ?? []).map(\n (pattern) => new RegExp(pattern, 'u')\n );\n const includeStringLiterals = options.includeStringLiterals === true;\n\n /** True when the text is copy the extractor would pick up. */\n const isReportableText = (text: string): boolean =>\n shouldExtract(text) &&\n !ignorePatterns.some((pattern) => pattern.test(text.trim()));\n\n /** Collapse whitespace so multi-line JSX text reads well in the report. */\n const formatText = (text: string): string => {\n const normalized = text.replace(/\\s+/gu, ' ').trim();\n\n return normalized.length > 40\n ? `${normalized.slice(0, 40)}…`\n : normalized;\n };\n\n /**\n * Handlers for the markup languages that are not JSX — Vue, Svelte and\n * Angular templates. Reports through the same predicate and options as\n * the JSX path so a project gets identical results whatever it is written\n * in.\n */\n const templateVisitor = createTemplateVisitor(\n ({ node, text, attributeName, elementName }) => {\n if (elementName && ignoredElements.has(elementName)) return;\n\n if (attributeName !== null && !attributes.has(attributeName)) return;\n\n if (!isReportableText(text)) return;\n\n context.report({\n node: node as never,\n messageId: attributeName === null ? 'rawText' : 'rawAttribute',\n data: {\n text: formatText(text),\n ...(attributeName === null ? {} : { attribute: attributeName }),\n },\n });\n }\n );\n\n const scriptVisitor: TemplateVisitor = {\n JSXText: (node) => {\n const astNode = toAstNode(node as never);\n const text = astNode['value'] as string;\n\n if (!isReportableText(text)) return;\n\n const elementName = getEnclosingElementName(astNode.parent);\n\n if (elementName && ignoredElements.has(elementName)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawText',\n data: { text: formatText(text) },\n });\n },\n\n JSXAttribute: (node) => {\n const astNode = toAstNode(node as never);\n const attributeName = getJsxAttributeName(astNode);\n\n if (!attributeName || !attributes.has(attributeName)) return;\n\n const valueNode = getJsxAttributeValueNode(astNode);\n const text = getStaticStringValue(valueNode);\n\n if (text === null || !isReportableText(text)) return;\n\n const elementName = getEnclosingElementName(astNode.parent);\n\n if (elementName && ignoredElements.has(elementName)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawAttribute',\n data: { attribute: attributeName, text: formatText(text) },\n });\n },\n\n Literal: (node) => {\n if (!includeStringLiterals) return;\n\n const astNode = toAstNode(node as never);\n\n if (typeof astNode['value'] !== 'string') return;\n\n if (isStructuralPosition(astNode)) return;\n\n const text = astNode['value'] as string;\n\n if (!isReportableText(text)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawStringLiteral',\n data: { text: formatText(text) },\n });\n },\n };\n\n return combineVisitors(\n context.sourceCode.parserServices,\n scriptVisitor,\n templateVisitor\n );\n },\n }\n);\n"],"mappings":"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"}
@@ -0,0 +1,143 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_utils_ast = require('../utils/ast.cjs');
3
+ const require_utils_createRule = require('../utils/createRule.cjs');
4
+ const require_utils_contentDeclaration = require('../utils/contentDeclaration.cjs');
5
+ const require_utils_projectUsage = require('../utils/projectUsage.cjs');
6
+ let _intlayer_config_defaultValues = require("@intlayer/config/defaultValues");
7
+
8
+ //#region src/rules/noUnusedContent.ts
9
+ /** Lifetime of the cached project scan, in milliseconds. */
10
+ const DEFAULT_CACHE_TTL_MS = 3e4;
11
+ /**
12
+ * True for a content declaration file — the only files this rule inspects.
13
+ *
14
+ * @param filename - Absolute path of the linted file.
15
+ * @param contentFileExtensions - Extensions the project declares content with.
16
+ */
17
+ const isContentDeclarationFile = (filename, contentFileExtensions) => contentFileExtensions.some((extension) => filename.endsWith(extension));
18
+ /**
19
+ * True when a shorter prefix of `fieldPath` is already reported, so the nested
20
+ * field would only repeat what the parent report says.
21
+ *
22
+ * @param fieldPath - Dotted path being considered.
23
+ * @param reportedPaths - Paths already reported for this dictionary.
24
+ */
25
+ const hasReportedAncestor = (fieldPath, reportedPaths) => {
26
+ const segments = fieldPath.split(".");
27
+ for (let depth = 1; depth < segments.length; depth++) if (reportedPaths.has(segments.slice(0, depth).join("."))) return true;
28
+ return false;
29
+ };
30
+ /**
31
+ * Compile the `ignoreFields` option, dropping any source that is not a valid
32
+ * regular expression rather than failing the whole lint run.
33
+ *
34
+ * @param sources - Regular-expression sources from the rule options.
35
+ */
36
+ const compileIgnorePatterns = (sources) => {
37
+ const patterns = [];
38
+ for (const source of sources) try {
39
+ patterns.push(new RegExp(source));
40
+ } catch {}
41
+ return patterns;
42
+ };
43
+ const noUnusedContent = require_utils_createRule.createRule("no-unused-content", {
44
+ meta: {
45
+ type: "suggestion",
46
+ docs: { description: "Report Intlayer dictionaries and content fields that nothing in the project reads" },
47
+ schema: [{
48
+ type: "object",
49
+ properties: {
50
+ reportUnusedDictionaries: { type: "boolean" },
51
+ reportUnusedFields: { type: "boolean" },
52
+ reportDuplicateKeys: { type: "boolean" },
53
+ ignoreFields: {
54
+ type: "array",
55
+ items: { type: "string" }
56
+ },
57
+ baseDir: { type: "string" },
58
+ cacheTtl: {
59
+ type: "number",
60
+ minimum: 0
61
+ }
62
+ },
63
+ additionalProperties: false
64
+ }],
65
+ messages: {
66
+ unusedDictionary: "The dictionary `{{dictionaryKey}}` is never read: no `useIntlayer`, `getIntlayer` or compat caller in the project asks for it, and no module imports this declaration.",
67
+ unusedField: "The content field `{{fieldPath}}` of `{{dictionaryKey}}` is never read. The compiler strips it from the build, so it only costs translation work.",
68
+ duplicateDictionary: "The dictionary key `{{dictionaryKey}}` is also declared in {{locations}}. Declarations sharing a key are merged, so a field defined twice silently keeps one of the two values."
69
+ }
70
+ },
71
+ create: (context) => {
72
+ const [options = {}] = context.options;
73
+ const { reportUnusedDictionaries = true, reportUnusedFields = true, reportDuplicateKeys = true, ignoreFields = [], baseDir = context.cwd, cacheTtl = DEFAULT_CACHE_TTL_MS } = options;
74
+ const configuration = require_utils_projectUsage.getProjectConfiguration(baseDir, cacheTtl);
75
+ const contentFileExtensions = configuration?.content.fileExtensions ?? _intlayer_config_defaultValues.FILE_EXTENSIONS;
76
+ if (!isContentDeclarationFile(context.filename, contentFileExtensions)) return {};
77
+ const ignorePatterns = compileIgnorePatterns(ignoreFields);
78
+ /** Intlayer declares one dictionary per file; the outermost match is it. */
79
+ let hasHandledDeclaration = false;
80
+ /** Report every field of a dictionary that nothing in the project reads. */
81
+ const reportUnusedFieldsOf = (declaration, usage) => {
82
+ if (usage.tracksEveryField || declaration.isPartiallyEnumerated) return;
83
+ const reportedPaths = /* @__PURE__ */ new Set();
84
+ for (const field of declaration.fields) {
85
+ if (usage.usedFieldPaths.has(field.path)) continue;
86
+ if (hasReportedAncestor(field.path, reportedPaths)) continue;
87
+ if (ignorePatterns.some((pattern) => pattern.test(field.path))) continue;
88
+ reportedPaths.add(field.path);
89
+ context.report({
90
+ node: field.keyNode,
91
+ messageId: "unusedField",
92
+ data: {
93
+ fieldPath: field.path,
94
+ dictionaryKey: declaration.dictionaryKey
95
+ }
96
+ });
97
+ }
98
+ };
99
+ /** Report the key when the same dictionary is declared somewhere else too. */
100
+ const reportDuplicatesOf = (declaration) => {
101
+ if (!reportDuplicateKeys || !configuration) return;
102
+ const duplicates = require_utils_projectUsage.getDuplicateDeclarations(configuration, declaration.dictionaryKey, context.filename);
103
+ if (duplicates.length === 0) return;
104
+ context.report({
105
+ node: declaration.keyNode,
106
+ messageId: "duplicateDictionary",
107
+ data: {
108
+ dictionaryKey: declaration.dictionaryKey,
109
+ locations: duplicates.map((duplicate) => duplicate.location === "remote" ? "the CMS" : `\`${duplicate.filePath}\``).join(", ")
110
+ }
111
+ });
112
+ };
113
+ /** Report the declaration itself when nothing references its key. */
114
+ const reportUsage = (declaration, index) => {
115
+ const isImportedDirectly = index.importedModulePaths.has(require_utils_projectUsage.stripModuleExtension(context.filename));
116
+ const usage = index.byDictionaryKey.get(declaration.dictionaryKey);
117
+ if (!usage) {
118
+ if (reportUnusedDictionaries && !isImportedDirectly) context.report({
119
+ node: declaration.keyNode,
120
+ messageId: "unusedDictionary",
121
+ data: { dictionaryKey: declaration.dictionaryKey }
122
+ });
123
+ return;
124
+ }
125
+ if (reportUnusedFields && !isImportedDirectly) reportUnusedFieldsOf(declaration, usage);
126
+ };
127
+ return { ObjectExpression: (node) => {
128
+ if (hasHandledDeclaration) return;
129
+ const declaration = require_utils_contentDeclaration.readContentDeclaration(require_utils_ast.toAstNode(node));
130
+ if (!declaration) return;
131
+ hasHandledDeclaration = true;
132
+ reportDuplicatesOf(declaration);
133
+ if (!reportUnusedDictionaries && !reportUnusedFields) return;
134
+ const index = require_utils_projectUsage.getProjectUsageIndex(baseDir, cacheTtl);
135
+ if (!index.isAvailable) return;
136
+ reportUsage(declaration, index);
137
+ } };
138
+ }
139
+ });
140
+
141
+ //#endregion
142
+ exports.noUnusedContent = noUnusedContent;
143
+ //# sourceMappingURL=noUnusedContent.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noUnusedContent.cjs","names":["createRule","getProjectConfiguration","FILE_EXTENSIONS","getDuplicateDeclarations","stripModuleExtension","readContentDeclaration","toAstNode","getProjectUsageIndex"],"sources":["../../../src/rules/noUnusedContent.ts"],"sourcesContent":["import { FILE_EXTENSIONS } from '@intlayer/config/defaultValues';\nimport { toAstNode } from '../utils/ast';\nimport {\n type ContentDeclaration,\n readContentDeclaration,\n} from '../utils/contentDeclaration';\nimport { createRule } from '../utils/createRule';\nimport {\n type DictionaryUsage,\n getDuplicateDeclarations,\n getProjectConfiguration,\n getProjectUsageIndex,\n type ProjectUsageIndex,\n stripModuleExtension,\n} from '../utils/projectUsage';\n\n/** Lifetime of the cached project scan, in milliseconds. */\nconst DEFAULT_CACHE_TTL_MS = 30_000;\n\nexport type NoUnusedContentOptions = {\n /** Report a dictionary whose key is never referenced anywhere. Default: `true`. */\n reportUnusedDictionaries?: boolean;\n /** Report a content field that is never read anywhere. Default: `true`. */\n reportUnusedFields?: boolean;\n /**\n * Report a dictionary key that is also declared elsewhere. Needs the unmerged\n * dictionaries on disk, so it stays silent until the project has been built.\n * Default: `true`.\n */\n reportDuplicateKeys?: boolean;\n /** Regular-expression sources for field paths that are never reported. */\n ignoreFields?: string[];\n /**\n * Project root the scan starts from. Defaults to ESLint's working directory,\n * which is the right answer unless one lint run spans several Intlayer\n * projects in a monorepo.\n */\n baseDir?: string;\n /**\n * How long one project scan is reused, in milliseconds. Lower it when linting\n * from a long-lived editor server and you want edits reflected sooner; `0`\n * rescans on every file. Default: `30000`.\n */\n cacheTtl?: number;\n};\n\ntype MessageIds = 'unusedDictionary' | 'unusedField' | 'duplicateDictionary';\n\n/**\n * True for a content declaration file — the only files this rule inspects.\n *\n * @param filename - Absolute path of the linted file.\n * @param contentFileExtensions - Extensions the project declares content with.\n */\nconst isContentDeclarationFile = (\n filename: string,\n contentFileExtensions: string[]\n): boolean =>\n contentFileExtensions.some((extension) => filename.endsWith(extension));\n\n/**\n * True when a shorter prefix of `fieldPath` is already reported, so the nested\n * field would only repeat what the parent report says.\n *\n * @param fieldPath - Dotted path being considered.\n * @param reportedPaths - Paths already reported for this dictionary.\n */\nconst hasReportedAncestor = (\n fieldPath: string,\n reportedPaths: Set<string>\n): boolean => {\n const segments = fieldPath.split('.');\n\n for (let depth = 1; depth < segments.length; depth++) {\n if (reportedPaths.has(segments.slice(0, depth).join('.'))) return true;\n }\n\n return false;\n};\n\n/**\n * Compile the `ignoreFields` option, dropping any source that is not a valid\n * regular expression rather than failing the whole lint run.\n *\n * @param sources - Regular-expression sources from the rule options.\n */\nconst compileIgnorePatterns = (sources: string[]): RegExp[] => {\n const patterns: RegExp[] = [];\n\n for (const source of sources) {\n try {\n patterns.push(new RegExp(source));\n } catch {\n // An unparsable pattern ignores nothing; the rest still apply.\n }\n }\n\n return patterns;\n};\n\nexport const noUnusedContent = createRule<\n [NoUnusedContentOptions?],\n MessageIds\n>('no-unused-content', {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Report Intlayer dictionaries and content fields that nothing in the project reads',\n },\n schema: [\n {\n type: 'object',\n properties: {\n reportUnusedDictionaries: { type: 'boolean' },\n reportUnusedFields: { type: 'boolean' },\n reportDuplicateKeys: { type: 'boolean' },\n ignoreFields: { type: 'array', items: { type: 'string' } },\n baseDir: { type: 'string' },\n cacheTtl: { type: 'number', minimum: 0 },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n unusedDictionary:\n 'The dictionary `{{dictionaryKey}}` is never read: no `useIntlayer`, `getIntlayer` or compat caller in the project asks for it, and no module imports this declaration.',\n unusedField:\n 'The content field `{{fieldPath}}` of `{{dictionaryKey}}` is never read. The compiler strips it from the build, so it only costs translation work.',\n duplicateDictionary:\n 'The dictionary key `{{dictionaryKey}}` is also declared in {{locations}}. Declarations sharing a key are merged, so a field defined twice silently keeps one of the two values.',\n },\n },\n\n create: (context) => {\n const [options = {}] = context.options;\n const {\n reportUnusedDictionaries = true,\n reportUnusedFields = true,\n reportDuplicateKeys = true,\n ignoreFields = [],\n baseDir = context.cwd,\n cacheTtl = DEFAULT_CACHE_TTL_MS,\n } = options;\n\n const configuration = getProjectConfiguration(baseDir, cacheTtl);\n const contentFileExtensions =\n configuration?.content.fileExtensions ?? FILE_EXTENSIONS;\n\n if (!isContentDeclarationFile(context.filename, contentFileExtensions)) {\n return {};\n }\n\n const ignorePatterns = compileIgnorePatterns(ignoreFields);\n\n /** Intlayer declares one dictionary per file; the outermost match is it. */\n let hasHandledDeclaration = false;\n\n /** Report every field of a dictionary that nothing in the project reads. */\n const reportUnusedFieldsOf = (\n declaration: ContentDeclaration,\n usage: DictionaryUsage\n ): void => {\n if (usage.tracksEveryField || declaration.isPartiallyEnumerated) return;\n\n const reportedPaths = new Set<string>();\n\n for (const field of declaration.fields) {\n if (usage.usedFieldPaths.has(field.path)) continue;\n\n if (hasReportedAncestor(field.path, reportedPaths)) continue;\n\n if (ignorePatterns.some((pattern) => pattern.test(field.path)))\n continue;\n\n reportedPaths.add(field.path);\n\n context.report({\n node: field.keyNode as never,\n messageId: 'unusedField',\n data: {\n fieldPath: field.path,\n dictionaryKey: declaration.dictionaryKey,\n },\n });\n }\n };\n\n /** Report the key when the same dictionary is declared somewhere else too. */\n const reportDuplicatesOf = (declaration: ContentDeclaration): void => {\n if (!reportDuplicateKeys || !configuration) return;\n\n const duplicates = getDuplicateDeclarations(\n configuration,\n declaration.dictionaryKey,\n context.filename\n );\n\n if (duplicates.length === 0) return;\n\n context.report({\n node: declaration.keyNode as never,\n messageId: 'duplicateDictionary',\n data: {\n dictionaryKey: declaration.dictionaryKey,\n locations: duplicates\n .map((duplicate) =>\n duplicate.location === 'remote'\n ? 'the CMS'\n : `\\`${duplicate.filePath}\\``\n )\n .join(', '),\n },\n });\n };\n\n /** Report the declaration itself when nothing references its key. */\n const reportUsage = (\n declaration: ContentDeclaration,\n index: ProjectUsageIndex\n ): void => {\n // A declaration imported directly (`useDictionary(myDictionary)`) is\n // consumed without ever naming its key.\n const isImportedDirectly = index.importedModulePaths.has(\n stripModuleExtension(context.filename)\n );\n\n const usage = index.byDictionaryKey.get(declaration.dictionaryKey);\n\n if (!usage) {\n if (reportUnusedDictionaries && !isImportedDirectly) {\n context.report({\n node: declaration.keyNode as never,\n messageId: 'unusedDictionary',\n data: { dictionaryKey: declaration.dictionaryKey },\n });\n }\n\n return;\n }\n\n if (reportUnusedFields && !isImportedDirectly) {\n reportUnusedFieldsOf(declaration, usage);\n }\n };\n\n return {\n ObjectExpression: (node) => {\n if (hasHandledDeclaration) return;\n\n const declaration = readContentDeclaration(toAstNode(node));\n\n if (!declaration) return;\n\n hasHandledDeclaration = true;\n\n reportDuplicatesOf(declaration);\n\n if (!reportUnusedDictionaries && !reportUnusedFields) return;\n\n const index = getProjectUsageIndex(baseDir, cacheTtl);\n\n // Nothing could be scanned — reporting every dictionary as unused would\n // be the worst possible answer.\n if (!index.isAvailable) return;\n\n reportUsage(declaration, index);\n },\n };\n },\n});\n"],"mappings":"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"}
@@ -25,8 +25,8 @@ const staticDictionaryKey = require_utils_createRule.createRule("static-dictiona
25
25
  docs: { description: "Require the dictionary key of an Intlayer content read to be a static string literal" },
26
26
  messages: {
27
27
  dynamicKey: "The dictionary key passed to `{{caller}}` must be a string literal. A computed key is invisible to the Intlayer compiler, which then skips the optimize pass and bundles every dictionary.",
28
- dynamicTemplateKey: "The dictionary key passed to `{{caller}}` interpolates a value. Use a string literal — an interpolated key is invisible to the Intlayer compiler, which then skips the optimize pass and bundles every dictionary.",
29
- dynamicJsxKey: "The `{{attribute}}` of `<{{caller}}>` must be a static string. A computed value is invisible to the Intlayer compiler, which then cannot resolve the dictionary at build time."
28
+ dynamicTemplateKey: "The dictionary key passed to `{{caller}}` interpolates a value. An interpolated key is invisible to the Intlayer compiler, which then skips the optimize pass and bundles every dictionary. Use a string literal, or variant (https://intlayer.org/doc/concept/variants) instead.",
29
+ dynamicJsxKey: "The `{{attribute}}` of `<{{caller}}>` must be a static string. A computed value is invisible to the Intlayer compiler, which then cannot resolve the dictionary at build time. Use a static string, or variant (https://intlayer.org/doc/concept/variants) instead"
30
30
  }
31
31
  },
32
32
  create: (context) => {
@@ -1 +1 @@
1
- {"version":3,"file":"staticDictionaryKey.cjs","names":["createRule","getActiveDescriptors","collectImportSources","toAstNode","matchCaller","matchJsxCaller","getJsxElementName","getJsxAttributeName","getJsxAttributeValueNode","getStaticStringValue"],"sources":["../../../src/rules/staticDictionaryKey.ts"],"sourcesContent":["import type { CallerDescriptor } from '@intlayer/config/callers';\nimport {\n type AstNode,\n getJsxAttributeName,\n getJsxAttributeValueNode,\n getJsxElementName,\n getStaticStringValue,\n toAstNode,\n} from '../utils/ast';\nimport {\n type CallerMatch,\n collectImportSources,\n getActiveDescriptors,\n matchCaller,\n matchJsxCaller,\n} from '../utils/callerMatcher';\nimport { createRule } from '../utils/createRule';\n\ntype MessageIds = 'dynamicKey' | 'dynamicTemplateKey' | 'dynamicJsxKey';\n\n/**\n * True when the call may legitimately omit its namespace — next-intl's\n * `useTranslations()` root scope derives the dictionary from each message id\n * instead, so an absent argument is not a violation.\n *\n * @param descriptor - The matched registry entry.\n */\nconst allowsMissingNamespace = (descriptor: CallerDescriptor): boolean =>\n descriptor.allowRootScope === true;\n\n/**\n * Distinguish an interpolated template from any other runtime expression, so\n * the report can name the actual problem.\n *\n * @param node - The namespace node.\n */\nconst isInterpolatedTemplate = (node: AstNode): boolean =>\n node.type === 'TemplateLiteral' &&\n ((node['expressions'] as unknown[] | undefined)?.length ?? 0) > 0;\n\nexport const staticDictionaryKey = createRule<[], MessageIds>(\n 'static-dictionary-key',\n {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require the dictionary key of an Intlayer content read to be a static string literal',\n },\n messages: {\n dynamicKey:\n 'The dictionary key passed to `{{caller}}` must be a string literal. A computed key is invisible to the Intlayer compiler, which then skips the optimize pass and bundles every dictionary.',\n dynamicTemplateKey:\n 'The dictionary key passed to `{{caller}}` interpolates a value. Use a string literal — an interpolated key is invisible to the Intlayer compiler, which then skips the optimize pass and bundles every dictionary.',\n dynamicJsxKey:\n 'The `{{attribute}}` of `<{{caller}}>` must be a static string. A computed value is invisible to the Intlayer compiler, which then cannot resolve the dictionary at build time.',\n },\n },\n\n create: (context) => {\n let activeDescriptors = getActiveDescriptors(new Set());\n\n /** Report the namespace of a matched call when it is not static. */\n const reportDynamicNamespace = (match: CallerMatch): void => {\n const { descriptor, namespace } = match;\n\n if (!namespace) return;\n\n // A `fixed` namespace is a compile-time constant by construction.\n if (namespace.source.from === 'fixed') return;\n\n if (!namespace.node) {\n // Nothing was passed at all — only a problem for callers that require it.\n return;\n }\n\n if (namespace.value !== null) return;\n\n context.report({\n node: namespace.node as never,\n messageId: isInterpolatedTemplate(namespace.node)\n ? 'dynamicTemplateKey'\n : 'dynamicKey',\n data: { caller: descriptor.callerName },\n });\n };\n\n return {\n Program: (node) => {\n activeDescriptors = getActiveDescriptors(\n collectImportSources(toAstNode(node))\n );\n },\n\n CallExpression: (node) => {\n const match = matchCaller(toAstNode(node), activeDescriptors);\n\n if (!match) return;\n\n if (!match.namespace && allowsMissingNamespace(match.descriptor)) {\n return;\n }\n\n reportDynamicNamespace(match);\n },\n\n TaggedTemplateExpression: (node) => {\n const match = matchCaller(toAstNode(node), activeDescriptors);\n\n if (match) reportDynamicNamespace(match);\n },\n\n JSXOpeningElement: (node) => {\n const astNode = toAstNode(node);\n const descriptor = matchJsxCaller(\n getJsxElementName(astNode),\n activeDescriptors\n );\n\n if (!descriptor) return;\n\n const attributes =\n (astNode['attributes'] as AstNode[] | undefined) ?? [];\n\n const targetAttributes = [\n descriptor.jsxIdAttribute,\n descriptor.jsxNamespaceAttribute,\n ].filter((name): name is string => typeof name === 'string');\n\n for (const attribute of attributes) {\n const attributeName = getJsxAttributeName(attribute);\n\n if (!attributeName || !targetAttributes.includes(attributeName)) {\n continue;\n }\n\n const valueNode = getJsxAttributeValueNode(attribute);\n\n if (!valueNode) continue;\n\n if (getStaticStringValue(valueNode) !== null) continue;\n\n context.report({\n node: attribute as never,\n messageId: 'dynamicJsxKey',\n data: {\n attribute: attributeName,\n caller: descriptor.callerName,\n },\n });\n }\n },\n };\n },\n }\n);\n"],"mappings":";;;;;;;;;;;;;AA2BA,MAAM,0BAA0B,eAC9B,WAAW,mBAAmB;;;;;;;AAQhC,MAAM,0BAA0B,SAC9B,KAAK,SAAS,sBACZ,KAAK,cAAc,EAA4B,UAAU,KAAK;AAElE,MAAa,sBAAsBA,oCACjC,yBACA;CACE,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,uFACJ;EACA,UAAU;GACR,YACE;GACF,oBACE;GACF,eACE;EACJ;CACF;CAEA,SAAS,YAAY;EACnB,IAAI,oBAAoBC,iEAAqB,IAAI,IAAI,CAAC;;EAGtD,MAAM,0BAA0B,UAA6B;GAC3D,MAAM,EAAE,YAAY,cAAc;GAElC,IAAI,CAAC,WAAW;GAGhB,IAAI,UAAU,OAAO,SAAS,SAAS;GAEvC,IAAI,CAAC,UAAU,MAEb;GAGF,IAAI,UAAU,UAAU,MAAM;GAE9B,QAAQ,OAAO;IACb,MAAM,UAAU;IAChB,WAAW,uBAAuB,UAAU,IAAI,IAC5C,uBACA;IACJ,MAAM,EAAE,QAAQ,WAAW,WAAW;GACxC,CAAC;EACH;EAEA,OAAO;GACL,UAAU,SAAS;IACjB,oBAAoBA,iDAClBC,iDAAqBC,4BAAU,IAAI,CAAC,CACtC;GACF;GAEA,iBAAiB,SAAS;IACxB,MAAM,QAAQC,wCAAYD,4BAAU,IAAI,GAAG,iBAAiB;IAE5D,IAAI,CAAC,OAAO;IAEZ,IAAI,CAAC,MAAM,aAAa,uBAAuB,MAAM,UAAU,GAC7D;IAGF,uBAAuB,KAAK;GAC9B;GAEA,2BAA2B,SAAS;IAClC,MAAM,QAAQC,wCAAYD,4BAAU,IAAI,GAAG,iBAAiB;IAE5D,IAAI,OAAO,uBAAuB,KAAK;GACzC;GAEA,oBAAoB,SAAS;IAC3B,MAAM,UAAUA,4BAAU,IAAI;IAC9B,MAAM,aAAaE,2CACjBC,oCAAkB,OAAO,GACzB,iBACF;IAEA,IAAI,CAAC,YAAY;IAEjB,MAAM,aACH,QAAQ,iBAA2C,CAAC;IAEvD,MAAM,mBAAmB,CACvB,WAAW,gBACX,WAAW,qBACb,CAAC,CAAC,QAAQ,SAAyB,OAAO,SAAS,QAAQ;IAE3D,KAAK,MAAM,aAAa,YAAY;KAClC,MAAM,gBAAgBC,sCAAoB,SAAS;KAEnD,IAAI,CAAC,iBAAiB,CAAC,iBAAiB,SAAS,aAAa,GAC5D;KAGF,MAAM,YAAYC,2CAAyB,SAAS;KAEpD,IAAI,CAAC,WAAW;KAEhB,IAAIC,uCAAqB,SAAS,MAAM,MAAM;KAE9C,QAAQ,OAAO;MACb,MAAM;MACN,WAAW;MACX,MAAM;OACJ,WAAW;OACX,QAAQ,WAAW;MACrB;KACF,CAAC;IACH;GACF;EACF;CACF;AACF,CACF"}
1
+ {"version":3,"file":"staticDictionaryKey.cjs","names":["createRule","getActiveDescriptors","collectImportSources","toAstNode","matchCaller","matchJsxCaller","getJsxElementName","getJsxAttributeName","getJsxAttributeValueNode","getStaticStringValue"],"sources":["../../../src/rules/staticDictionaryKey.ts"],"sourcesContent":["import type { CallerDescriptor } from '@intlayer/config/callers';\nimport {\n type AstNode,\n getJsxAttributeName,\n getJsxAttributeValueNode,\n getJsxElementName,\n getStaticStringValue,\n toAstNode,\n} from '../utils/ast';\nimport {\n type CallerMatch,\n collectImportSources,\n getActiveDescriptors,\n matchCaller,\n matchJsxCaller,\n} from '../utils/callerMatcher';\nimport { createRule } from '../utils/createRule';\n\ntype MessageIds = 'dynamicKey' | 'dynamicTemplateKey' | 'dynamicJsxKey';\n\n/**\n * True when the call may legitimately omit its namespace — next-intl's\n * `useTranslations()` root scope derives the dictionary from each message id\n * instead, so an absent argument is not a violation.\n *\n * @param descriptor - The matched registry entry.\n */\nconst allowsMissingNamespace = (descriptor: CallerDescriptor): boolean =>\n descriptor.allowRootScope === true;\n\n/**\n * Distinguish an interpolated template from any other runtime expression, so\n * the report can name the actual problem.\n *\n * @param node - The namespace node.\n */\nconst isInterpolatedTemplate = (node: AstNode): boolean =>\n node.type === 'TemplateLiteral' &&\n ((node['expressions'] as unknown[] | undefined)?.length ?? 0) > 0;\n\nexport const staticDictionaryKey = createRule<[], MessageIds>(\n 'static-dictionary-key',\n {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require the dictionary key of an Intlayer content read to be a static string literal',\n },\n messages: {\n dynamicKey:\n 'The dictionary key passed to `{{caller}}` must be a string literal. A computed key is invisible to the Intlayer compiler, which then skips the optimize pass and bundles every dictionary.',\n dynamicTemplateKey:\n 'The dictionary key passed to `{{caller}}` interpolates a value. An interpolated key is invisible to the Intlayer compiler, which then skips the optimize pass and bundles every dictionary. Use a string literal, or variant (https://intlayer.org/doc/concept/variants) instead.',\n dynamicJsxKey:\n 'The `{{attribute}}` of `<{{caller}}>` must be a static string. A computed value is invisible to the Intlayer compiler, which then cannot resolve the dictionary at build time. Use a static string, or variant (https://intlayer.org/doc/concept/variants) instead',\n },\n },\n\n create: (context) => {\n let activeDescriptors = getActiveDescriptors(new Set());\n\n /** Report the namespace of a matched call when it is not static. */\n const reportDynamicNamespace = (match: CallerMatch): void => {\n const { descriptor, namespace } = match;\n\n if (!namespace) return;\n\n // A `fixed` namespace is a compile-time constant by construction.\n if (namespace.source.from === 'fixed') return;\n\n if (!namespace.node) {\n // Nothing was passed at all — only a problem for callers that require it.\n return;\n }\n\n if (namespace.value !== null) return;\n\n context.report({\n node: namespace.node as never,\n messageId: isInterpolatedTemplate(namespace.node)\n ? 'dynamicTemplateKey'\n : 'dynamicKey',\n data: { caller: descriptor.callerName },\n });\n };\n\n return {\n Program: (node) => {\n activeDescriptors = getActiveDescriptors(\n collectImportSources(toAstNode(node))\n );\n },\n\n CallExpression: (node) => {\n const match = matchCaller(toAstNode(node), activeDescriptors);\n\n if (!match) return;\n\n if (!match.namespace && allowsMissingNamespace(match.descriptor)) {\n return;\n }\n\n reportDynamicNamespace(match);\n },\n\n TaggedTemplateExpression: (node) => {\n const match = matchCaller(toAstNode(node), activeDescriptors);\n\n if (match) reportDynamicNamespace(match);\n },\n\n JSXOpeningElement: (node) => {\n const astNode = toAstNode(node);\n const descriptor = matchJsxCaller(\n getJsxElementName(astNode),\n activeDescriptors\n );\n\n if (!descriptor) return;\n\n const attributes =\n (astNode['attributes'] as AstNode[] | undefined) ?? [];\n\n const targetAttributes = [\n descriptor.jsxIdAttribute,\n descriptor.jsxNamespaceAttribute,\n ].filter((name): name is string => typeof name === 'string');\n\n for (const attribute of attributes) {\n const attributeName = getJsxAttributeName(attribute);\n\n if (!attributeName || !targetAttributes.includes(attributeName)) {\n continue;\n }\n\n const valueNode = getJsxAttributeValueNode(attribute);\n\n if (!valueNode) continue;\n\n if (getStaticStringValue(valueNode) !== null) continue;\n\n context.report({\n node: attribute as never,\n messageId: 'dynamicJsxKey',\n data: {\n attribute: attributeName,\n caller: descriptor.callerName,\n },\n });\n }\n },\n };\n },\n }\n);\n"],"mappings":";;;;;;;;;;;;;AA2BA,MAAM,0BAA0B,eAC9B,WAAW,mBAAmB;;;;;;;AAQhC,MAAM,0BAA0B,SAC9B,KAAK,SAAS,sBACZ,KAAK,cAAc,EAA4B,UAAU,KAAK;AAElE,MAAa,sBAAsBA,oCACjC,yBACA;CACE,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,uFACJ;EACA,UAAU;GACR,YACE;GACF,oBACE;GACF,eACE;EACJ;CACF;CAEA,SAAS,YAAY;EACnB,IAAI,oBAAoBC,iEAAqB,IAAI,IAAI,CAAC;;EAGtD,MAAM,0BAA0B,UAA6B;GAC3D,MAAM,EAAE,YAAY,cAAc;GAElC,IAAI,CAAC,WAAW;GAGhB,IAAI,UAAU,OAAO,SAAS,SAAS;GAEvC,IAAI,CAAC,UAAU,MAEb;GAGF,IAAI,UAAU,UAAU,MAAM;GAE9B,QAAQ,OAAO;IACb,MAAM,UAAU;IAChB,WAAW,uBAAuB,UAAU,IAAI,IAC5C,uBACA;IACJ,MAAM,EAAE,QAAQ,WAAW,WAAW;GACxC,CAAC;EACH;EAEA,OAAO;GACL,UAAU,SAAS;IACjB,oBAAoBA,iDAClBC,iDAAqBC,4BAAU,IAAI,CAAC,CACtC;GACF;GAEA,iBAAiB,SAAS;IACxB,MAAM,QAAQC,wCAAYD,4BAAU,IAAI,GAAG,iBAAiB;IAE5D,IAAI,CAAC,OAAO;IAEZ,IAAI,CAAC,MAAM,aAAa,uBAAuB,MAAM,UAAU,GAC7D;IAGF,uBAAuB,KAAK;GAC9B;GAEA,2BAA2B,SAAS;IAClC,MAAM,QAAQC,wCAAYD,4BAAU,IAAI,GAAG,iBAAiB;IAE5D,IAAI,OAAO,uBAAuB,KAAK;GACzC;GAEA,oBAAoB,SAAS;IAC3B,MAAM,UAAUA,4BAAU,IAAI;IAC9B,MAAM,aAAaE,2CACjBC,oCAAkB,OAAO,GACzB,iBACF;IAEA,IAAI,CAAC,YAAY;IAEjB,MAAM,aACH,QAAQ,iBAA2C,CAAC;IAEvD,MAAM,mBAAmB,CACvB,WAAW,gBACX,WAAW,qBACb,CAAC,CAAC,QAAQ,SAAyB,OAAO,SAAS,QAAQ;IAE3D,KAAK,MAAM,aAAa,YAAY;KAClC,MAAM,gBAAgBC,sCAAoB,SAAS;KAEnD,IAAI,CAAC,iBAAiB,CAAC,iBAAiB,SAAS,aAAa,GAC5D;KAGF,MAAM,YAAYC,2CAAyB,SAAS;KAEpD,IAAI,CAAC,WAAW;KAEhB,IAAIC,uCAAqB,SAAS,MAAM,MAAM;KAE9C,QAAQ,OAAO;MACb,MAAM;MACN,WAAW;MACX,MAAM;OACJ,WAAW;OACX,QAAQ,WAAW;MACrB;KACF,CAAC;IACH;GACF;EACF;CACF;AACF,CACF"}
@@ -0,0 +1,100 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_utils_ast = require('./ast.cjs');
3
+
4
+ //#region src/utils/contentDeclaration.ts
5
+ /**
6
+ * Static name of an object property key, or `null` for computed and
7
+ * non-literal keys.
8
+ *
9
+ * @param property - A `Property` node.
10
+ */
11
+ const getPropertyKeyName = (property) => {
12
+ if (property["computed"] === true) return null;
13
+ const key = property["key"];
14
+ if (key?.type === "Identifier") return key["name"];
15
+ if (key?.type === "Literal") {
16
+ const value = key["value"];
17
+ return typeof value === "string" || typeof value === "number" ? String(value) : null;
18
+ }
19
+ return null;
20
+ };
21
+ /**
22
+ * Read a property's value node by name, or `null` when absent.
23
+ *
24
+ * @param objectExpression - An `ObjectExpression` node.
25
+ * @param propertyName - Property to look up.
26
+ */
27
+ const findPropertyValue = (objectExpression, propertyName) => {
28
+ const properties = objectExpression["properties"] ?? [];
29
+ for (const property of properties) {
30
+ if (property.type !== "Property") continue;
31
+ if (getPropertyKeyName(property) !== propertyName) continue;
32
+ return require_utils_ast.unwrapExpression(property["value"] ?? null);
33
+ }
34
+ return null;
35
+ };
36
+ /**
37
+ * Walk a content object, recording every declared field path.
38
+ *
39
+ * Only plain object literals are descended into. A translation node
40
+ * (`t({ en: … })`, `enu({ … })`, `md(…)`) is a leaf: the keys inside it are
41
+ * locales and enumeration values, not content fields the compiler prunes.
42
+ * Arrays are leaves too — an unused array element is not something worth
43
+ * reporting on its own.
44
+ *
45
+ * @param objectExpression - The object to walk.
46
+ * @param collector - Accumulated result.
47
+ * @param parentPath - Path segments leading to this object.
48
+ */
49
+ const collectFields = (objectExpression, collector, parentPath) => {
50
+ const properties = objectExpression["properties"] ?? [];
51
+ for (const property of properties) {
52
+ if (property.type !== "Property") {
53
+ collector.isPartiallyEnumerated = true;
54
+ continue;
55
+ }
56
+ const name = getPropertyKeyName(property);
57
+ if (name === null) {
58
+ collector.isPartiallyEnumerated = true;
59
+ continue;
60
+ }
61
+ const path = [...parentPath, name];
62
+ const keyNode = property["key"];
63
+ collector.fields.push({
64
+ path: path.join("."),
65
+ keyNode
66
+ });
67
+ const value = require_utils_ast.unwrapExpression(property["value"] ?? null);
68
+ if (value?.type === "ObjectExpression") collectFields(value, collector, path);
69
+ }
70
+ };
71
+ /**
72
+ * Read the dictionary declaration an object literal represents, or `null` when
73
+ * it is not one.
74
+ *
75
+ * @param objectExpression - Any `ObjectExpression` node from the linted file.
76
+ */
77
+ const readContentDeclaration = (objectExpression) => {
78
+ if (objectExpression.type !== "ObjectExpression") return null;
79
+ const keyNode = findPropertyValue(objectExpression, "key");
80
+ const contentNode = findPropertyValue(objectExpression, "content");
81
+ if (!keyNode || !contentNode) return null;
82
+ const dictionaryKey = require_utils_ast.getStaticStringValue(keyNode);
83
+ if (!dictionaryKey) return null;
84
+ const collector = {
85
+ fields: [],
86
+ isPartiallyEnumerated: false
87
+ };
88
+ if (contentNode.type === "ObjectExpression") collectFields(contentNode, collector, []);
89
+ else collector.isPartiallyEnumerated = true;
90
+ return {
91
+ dictionaryKey,
92
+ keyNode,
93
+ fields: collector.fields,
94
+ isPartiallyEnumerated: collector.isPartiallyEnumerated
95
+ };
96
+ };
97
+
98
+ //#endregion
99
+ exports.readContentDeclaration = readContentDeclaration;
100
+ //# sourceMappingURL=contentDeclaration.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contentDeclaration.cjs","names":["unwrapExpression","getStaticStringValue"],"sources":["../../../src/utils/contentDeclaration.ts"],"sourcesContent":["import type { AstNode } from './ast';\nimport { getStaticStringValue, unwrapExpression } from './ast';\n\n/**\n * Reading an Intlayer content declaration out of the file being linted.\n *\n * A declaration is any object literal carrying both a `key` and a `content`\n * property — the same shape the VS Code extension looks for, which covers\n * `export default { … }`, `const dictionary = { … } satisfies Dictionary` and\n * every variant in between without caring how the object reaches the export.\n */\n\n/** One field declared inside a dictionary's `content`. */\nexport type DeclaredField = {\n /** Dotted path from the content root, e.g. `hero.title`. */\n path: string;\n /** The property key node, so a report points at the field name. */\n keyNode: AstNode;\n};\n\n/** A dictionary declaration found in the linted file. */\nexport type ContentDeclaration = {\n /** The static dictionary key. */\n dictionaryKey: string;\n /** The node holding the key, for reporting a fully unused dictionary. */\n keyNode: AstNode;\n /** Every field declared under `content`, depth-first. */\n fields: DeclaredField[];\n /**\n * True when the content object holds a spread or a computed key, so the field\n * list is not exhaustive. No field may be reported unused in that case — the\n * missing entries could be exactly the ones being read.\n */\n isPartiallyEnumerated: boolean;\n};\n\n/**\n * Static name of an object property key, or `null` for computed and\n * non-literal keys.\n *\n * @param property - A `Property` node.\n */\nconst getPropertyKeyName = (property: AstNode): string | null => {\n if (property['computed'] === true) return null;\n\n const key = property['key'] as AstNode | undefined;\n\n if (key?.type === 'Identifier') return key['name'] as string;\n\n if (key?.type === 'Literal') {\n const value = key['value'];\n\n return typeof value === 'string' || typeof value === 'number'\n ? String(value)\n : null;\n }\n\n return null;\n};\n\n/**\n * Read a property's value node by name, or `null` when absent.\n *\n * @param objectExpression - An `ObjectExpression` node.\n * @param propertyName - Property to look up.\n */\nconst findPropertyValue = (\n objectExpression: AstNode,\n propertyName: string\n): AstNode | null => {\n const properties =\n (objectExpression['properties'] as AstNode[] | undefined) ?? [];\n\n for (const property of properties) {\n if (property.type !== 'Property') continue;\n\n if (getPropertyKeyName(property) !== propertyName) continue;\n\n return unwrapExpression((property['value'] as AstNode) ?? null);\n }\n\n return null;\n};\n\n/** Collector state threaded through the recursive field walk. */\ntype FieldCollector = {\n fields: DeclaredField[];\n isPartiallyEnumerated: boolean;\n};\n\n/**\n * Walk a content object, recording every declared field path.\n *\n * Only plain object literals are descended into. A translation node\n * (`t({ en: … })`, `enu({ … })`, `md(…)`) is a leaf: the keys inside it are\n * locales and enumeration values, not content fields the compiler prunes.\n * Arrays are leaves too — an unused array element is not something worth\n * reporting on its own.\n *\n * @param objectExpression - The object to walk.\n * @param collector - Accumulated result.\n * @param parentPath - Path segments leading to this object.\n */\nconst collectFields = (\n objectExpression: AstNode,\n collector: FieldCollector,\n parentPath: string[]\n): void => {\n const properties =\n (objectExpression['properties'] as AstNode[] | undefined) ?? [];\n\n for (const property of properties) {\n if (property.type !== 'Property') {\n // A spread hides fields this walk can never name.\n collector.isPartiallyEnumerated = true;\n continue;\n }\n\n const name = getPropertyKeyName(property);\n\n if (name === null) {\n collector.isPartiallyEnumerated = true;\n continue;\n }\n\n const path = [...parentPath, name];\n const keyNode = property['key'] as AstNode;\n\n collector.fields.push({ path: path.join('.'), keyNode });\n\n const value = unwrapExpression((property['value'] as AstNode) ?? null);\n\n if (value?.type === 'ObjectExpression') {\n collectFields(value, collector, path);\n }\n }\n};\n\n/**\n * Read the dictionary declaration an object literal represents, or `null` when\n * it is not one.\n *\n * @param objectExpression - Any `ObjectExpression` node from the linted file.\n */\nexport const readContentDeclaration = (\n objectExpression: AstNode\n): ContentDeclaration | null => {\n if (objectExpression.type !== 'ObjectExpression') return null;\n\n const keyNode = findPropertyValue(objectExpression, 'key');\n const contentNode = findPropertyValue(objectExpression, 'content');\n\n if (!keyNode || !contentNode) return null;\n\n const dictionaryKey = getStaticStringValue(keyNode);\n\n // A computed key is already reported by `static-dictionary-key`, and there is\n // no key to look usages up by.\n if (!dictionaryKey) return null;\n\n const collector: FieldCollector = {\n fields: [],\n isPartiallyEnumerated: false,\n };\n\n if (contentNode.type === 'ObjectExpression') {\n collectFields(contentNode, collector, []);\n } else {\n // `content` built by a call or spread from elsewhere — nothing to enumerate.\n collector.isPartiallyEnumerated = true;\n }\n\n return {\n dictionaryKey,\n keyNode,\n fields: collector.fields,\n isPartiallyEnumerated: collector.isPartiallyEnumerated,\n };\n};\n"],"mappings":";;;;;;;;;;AA0CA,MAAM,sBAAsB,aAAqC;CAC/D,IAAI,SAAS,gBAAgB,MAAM,OAAO;CAE1C,MAAM,MAAM,SAAS;CAErB,IAAI,KAAK,SAAS,cAAc,OAAO,IAAI;CAE3C,IAAI,KAAK,SAAS,WAAW;EAC3B,MAAM,QAAQ,IAAI;EAElB,OAAO,OAAO,UAAU,YAAY,OAAO,UAAU,WACjD,OAAO,KAAK,IACZ;CACN;CAEA,OAAO;AACT;;;;;;;AAQA,MAAM,qBACJ,kBACA,iBACmB;CACnB,MAAM,aACH,iBAAiB,iBAA2C,CAAC;CAEhE,KAAK,MAAM,YAAY,YAAY;EACjC,IAAI,SAAS,SAAS,YAAY;EAElC,IAAI,mBAAmB,QAAQ,MAAM,cAAc;EAEnD,OAAOA,mCAAkB,SAAS,YAAwB,IAAI;CAChE;CAEA,OAAO;AACT;;;;;;;;;;;;;;AAqBA,MAAM,iBACJ,kBACA,WACA,eACS;CACT,MAAM,aACH,iBAAiB,iBAA2C,CAAC;CAEhE,KAAK,MAAM,YAAY,YAAY;EACjC,IAAI,SAAS,SAAS,YAAY;GAEhC,UAAU,wBAAwB;GAClC;EACF;EAEA,MAAM,OAAO,mBAAmB,QAAQ;EAExC,IAAI,SAAS,MAAM;GACjB,UAAU,wBAAwB;GAClC;EACF;EAEA,MAAM,OAAO,CAAC,GAAG,YAAY,IAAI;EACjC,MAAM,UAAU,SAAS;EAEzB,UAAU,OAAO,KAAK;GAAE,MAAM,KAAK,KAAK,GAAG;GAAG;EAAQ,CAAC;EAEvD,MAAM,QAAQA,mCAAkB,SAAS,YAAwB,IAAI;EAErE,IAAI,OAAO,SAAS,oBAClB,cAAc,OAAO,WAAW,IAAI;CAExC;AACF;;;;;;;AAQA,MAAa,0BACX,qBAC8B;CAC9B,IAAI,iBAAiB,SAAS,oBAAoB,OAAO;CAEzD,MAAM,UAAU,kBAAkB,kBAAkB,KAAK;CACzD,MAAM,cAAc,kBAAkB,kBAAkB,SAAS;CAEjE,IAAI,CAAC,WAAW,CAAC,aAAa,OAAO;CAErC,MAAM,gBAAgBC,uCAAqB,OAAO;CAIlD,IAAI,CAAC,eAAe,OAAO;CAE3B,MAAM,YAA4B;EAChC,QAAQ,CAAC;EACT,uBAAuB;CACzB;CAEA,IAAI,YAAY,SAAS,oBACvB,cAAc,aAAa,WAAW,CAAC,CAAC;MAGxC,UAAU,wBAAwB;CAGpC,OAAO;EACL;EACA;EACA,QAAQ,UAAU;EAClB,uBAAuB,UAAU;CACnC;AACF"}