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.
- package/README.md +103 -49
- package/dist/cjs/@intlayer/config/dist/esm/extract/constants.cjs +18 -0
- package/dist/cjs/@intlayer/config/dist/esm/extract/constants.cjs.map +1 -0
- package/dist/cjs/@intlayer/config/dist/esm/extract/shouldExtract.cjs +37 -0
- package/dist/cjs/@intlayer/config/dist/esm/extract/shouldExtract.cjs.map +1 -0
- package/dist/cjs/_virtual/_rolldown/runtime.cjs +28 -0
- package/dist/cjs/index.cjs +41 -29
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/preset.cjs +53 -0
- package/dist/cjs/preset.cjs.map +1 -0
- package/dist/cjs/rules/enforceAdapterImport.cjs +9 -2
- package/dist/cjs/rules/enforceAdapterImport.cjs.map +1 -1
- package/dist/cjs/rules/noRawText.cjs +4 -3
- package/dist/cjs/rules/noRawText.cjs.map +1 -1
- package/dist/cjs/rules/noUnusedContent.cjs +143 -0
- package/dist/cjs/rules/noUnusedContent.cjs.map +1 -0
- package/dist/cjs/rules/staticDictionaryKey.cjs +2 -2
- package/dist/cjs/rules/staticDictionaryKey.cjs.map +1 -1
- package/dist/cjs/utils/contentDeclaration.cjs +100 -0
- package/dist/cjs/utils/contentDeclaration.cjs.map +1 -0
- package/dist/cjs/utils/projectUsage.cjs +437 -0
- package/dist/cjs/utils/projectUsage.cjs.map +1 -0
- package/dist/cjs/utils/reactVersion.cjs +50 -0
- package/dist/cjs/utils/reactVersion.cjs.map +1 -0
- package/dist/esm/@intlayer/config/dist/esm/extract/constants.mjs +18 -0
- package/dist/esm/@intlayer/config/dist/esm/extract/constants.mjs.map +1 -0
- package/dist/esm/@intlayer/config/dist/esm/extract/shouldExtract.mjs +37 -0
- package/dist/esm/@intlayer/config/dist/esm/extract/shouldExtract.mjs.map +1 -0
- package/dist/esm/_virtual/_rolldown/runtime.mjs +8 -0
- package/dist/esm/index.mjs +36 -30
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/preset.mjs +48 -0
- package/dist/esm/preset.mjs.map +1 -0
- package/dist/esm/rules/enforceAdapterImport.mjs +10 -3
- package/dist/esm/rules/enforceAdapterImport.mjs.map +1 -1
- package/dist/esm/rules/noRawText.mjs +2 -1
- package/dist/esm/rules/noRawText.mjs.map +1 -1
- package/dist/esm/rules/noUnusedContent.mjs +142 -0
- package/dist/esm/rules/noUnusedContent.mjs.map +1 -0
- package/dist/esm/rules/staticDictionaryKey.mjs +2 -2
- package/dist/esm/rules/staticDictionaryKey.mjs.map +1 -1
- package/dist/esm/utils/contentDeclaration.mjs +99 -0
- package/dist/esm/utils/contentDeclaration.mjs.map +1 -0
- package/dist/esm/utils/projectUsage.mjs +433 -0
- package/dist/esm/utils/projectUsage.mjs.map +1 -0
- package/dist/esm/utils/reactVersion.mjs +46 -0
- package/dist/esm/utils/reactVersion.mjs.map +1 -0
- package/dist/types/index.d.ts +19 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/node_modules/.bun/@types_estree@1.0.9/node_modules/@types/estree/index.d.ts +528 -0
- package/dist/types/node_modules/.bun/@types_estree@1.0.9/node_modules/@types/estree/index.d.ts.map +1 -0
- package/dist/types/preset.d.ts +45 -0
- package/dist/types/preset.d.ts.map +1 -0
- package/dist/types/rules/enforceAdapterImport.d.ts.map +1 -1
- package/dist/types/rules/noUnusedContent.d.ts +33 -0
- package/dist/types/rules/noUnusedContent.d.ts.map +1 -0
- package/dist/types/utils/ast.d.ts +1 -1
- package/dist/types/utils/contentDeclaration.d.ts +42 -0
- package/dist/types/utils/contentDeclaration.d.ts.map +1 -0
- package/dist/types/utils/projectUsage.d.ts +102 -0
- package/dist/types/utils/projectUsage.d.ts.map +1 -0
- package/dist/types/utils/reactVersion.d.ts +18 -0
- package/dist/types/utils/reactVersion.d.ts.map +1 -0
- package/package.json +5 -3
package/dist/esm/index.mjs
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
|
+
import { GENERATED_OUTPUT_GLOBS, PLUGIN_NAMESPACE, PLUGIN_PACKAGE_NAME, RECOMMENDED_RULES, getRecommendedRules } from "./preset.mjs";
|
|
1
2
|
import { enforceAdapterImport } from "./rules/enforceAdapterImport.mjs";
|
|
2
3
|
import { noDynamicFieldAccess } from "./rules/noDynamicFieldAccess.mjs";
|
|
3
4
|
import { noRawText } from "./rules/noRawText.mjs";
|
|
5
|
+
import { noUnusedContent } from "./rules/noUnusedContent.mjs";
|
|
4
6
|
import { staticDictionaryKey } from "./rules/staticDictionaryKey.mjs";
|
|
5
|
-
import {
|
|
7
|
+
import { getReactVersion } from "./utils/reactVersion.mjs";
|
|
6
8
|
|
|
7
9
|
//#region src/index.ts
|
|
8
|
-
const PLUGIN_NAME = "intlayer";
|
|
9
10
|
/**
|
|
10
11
|
* Every rule this plugin ships, keyed by the name used in configuration.
|
|
11
12
|
*
|
|
12
|
-
* The rules split into
|
|
13
|
+
* The rules split into three families:
|
|
13
14
|
* - **Coverage** (`no-raw-text`) — user-facing copy that never reached a
|
|
14
15
|
* dictionary. Nothing else in the toolchain reports it.
|
|
15
16
|
* - **Compiler contract** (`static-dictionary-key`, `no-dynamic-field-access`)
|
|
16
17
|
* — call shapes that type-check and run, but that the Babel/SWC passes cannot
|
|
17
18
|
* analyse, so they silently lose the optimize pass or read a purged field.
|
|
19
|
+
* - **Dead content** (`no-unused-content`) — dictionaries and fields nothing
|
|
20
|
+
* reads. Off in every preset: it is the only rule that scans the whole
|
|
21
|
+
* project from disk, so enabling it is a cost the user should opt into.
|
|
18
22
|
*
|
|
19
23
|
* Deliberately absent: unknown dictionary keys, unknown field paths and missing
|
|
20
24
|
* required locales. The generated `__DictionaryRegistry` module augmentation
|
|
@@ -25,7 +29,8 @@ const rules = {
|
|
|
25
29
|
"no-raw-text": noRawText,
|
|
26
30
|
"static-dictionary-key": staticDictionaryKey,
|
|
27
31
|
"no-dynamic-field-access": noDynamicFieldAccess,
|
|
28
|
-
"enforce-adapter-import": enforceAdapterImport
|
|
32
|
+
"enforce-adapter-import": enforceAdapterImport,
|
|
33
|
+
"no-unused-content": noUnusedContent
|
|
29
34
|
};
|
|
30
35
|
const meta = {
|
|
31
36
|
name: "eslint-plugin-intlayer",
|
|
@@ -41,18 +46,22 @@ const plugin = {
|
|
|
41
46
|
* @param name - Config name, surfaced by `--print-config` and the config inspector.
|
|
42
47
|
* @param ruleSeverities - Severity per rule name.
|
|
43
48
|
*/
|
|
44
|
-
const createConfig = (name,
|
|
49
|
+
const createConfig = (name, ruleEntries) => [{
|
|
50
|
+
name: `intlayer/${name}/ignores`,
|
|
51
|
+
ignores: GENERATED_OUTPUT_GLOBS
|
|
52
|
+
}, {
|
|
45
53
|
name: `intlayer/${name}`,
|
|
46
|
-
plugins: { [
|
|
47
|
-
|
|
48
|
-
})
|
|
54
|
+
plugins: { [PLUGIN_NAMESPACE]: plugin },
|
|
55
|
+
settings: { react: { version: getReactVersion() } },
|
|
56
|
+
rules: Object.fromEntries(Object.entries(ruleEntries).map(([ruleName, entry]) => [`${PLUGIN_NAMESPACE}/${ruleName}`, entry]))
|
|
57
|
+
}];
|
|
49
58
|
/**
|
|
50
59
|
* Flat configs, for `eslint.config.js`:
|
|
51
60
|
*
|
|
52
61
|
* ```js
|
|
53
62
|
* import intlayer from 'eslint-plugin-intlayer';
|
|
54
63
|
*
|
|
55
|
-
* export default [intlayer.configs.recommended]
|
|
64
|
+
* export default [...intlayer]; // or [...intlayer.configs.recommended]
|
|
56
65
|
* ```
|
|
57
66
|
*
|
|
58
67
|
* - `recommended` — the compiler-contract rules as errors, plus `no-raw-text`
|
|
@@ -64,27 +73,21 @@ const createConfig = (name, ruleSeverities) => ({
|
|
|
64
73
|
* `enforce-adapter-import` is opt-in: importing from the original package is
|
|
65
74
|
* perfectly valid once the bundler alias is set up, so a project that has done
|
|
66
75
|
* that should not be nagged. Only `strict` turns it on.
|
|
76
|
+
*
|
|
77
|
+
* `no-unused-content` is in no preset at all, `strict` included. It reads the
|
|
78
|
+
* Intlayer configuration and walks the project's source files from disk, which
|
|
79
|
+
* is a different order of cost from the AST-only rules and depends on build
|
|
80
|
+
* output for its duplicate report — spreading a preset should never quietly
|
|
81
|
+
* turn that on.
|
|
67
82
|
*/
|
|
68
83
|
const configs = {
|
|
69
|
-
recommended:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"no-raw-text": "error",
|
|
77
|
-
"static-dictionary-key": "error",
|
|
78
|
-
"no-dynamic-field-access": "error",
|
|
79
|
-
"enforce-adapter-import": "error"
|
|
80
|
-
}),
|
|
81
|
-
rules: {
|
|
82
|
-
"intlayer/no-raw-text": ["error", { includeStringLiterals: true }],
|
|
83
|
-
"intlayer/static-dictionary-key": "error",
|
|
84
|
-
"intlayer/no-dynamic-field-access": "error",
|
|
85
|
-
"intlayer/enforce-adapter-import": "error"
|
|
86
|
-
}
|
|
87
|
-
},
|
|
84
|
+
recommended: createConfig("recommended", RECOMMENDED_RULES),
|
|
85
|
+
strict: createConfig("strict", {
|
|
86
|
+
"no-raw-text": ["error", { includeStringLiterals: true }],
|
|
87
|
+
"static-dictionary-key": "error",
|
|
88
|
+
"no-dynamic-field-access": "error",
|
|
89
|
+
"enforce-adapter-import": "error"
|
|
90
|
+
}),
|
|
88
91
|
"contract-only": createConfig("contract-only", {
|
|
89
92
|
"static-dictionary-key": "error",
|
|
90
93
|
"no-dynamic-field-access": "error"
|
|
@@ -93,9 +96,12 @@ const configs = {
|
|
|
93
96
|
const intlayerPlugin = {
|
|
94
97
|
meta,
|
|
95
98
|
rules,
|
|
96
|
-
configs
|
|
99
|
+
configs,
|
|
100
|
+
*[Symbol.iterator]() {
|
|
101
|
+
yield* configs.recommended;
|
|
102
|
+
}
|
|
97
103
|
};
|
|
98
104
|
|
|
99
105
|
//#endregion
|
|
100
|
-
export { configs, intlayerPlugin as default, meta, rules };
|
|
106
|
+
export { GENERATED_OUTPUT_GLOBS, PLUGIN_NAMESPACE, PLUGIN_PACKAGE_NAME, RECOMMENDED_RULES, configs, intlayerPlugin as default, getReactVersion, getRecommendedRules, meta, rules };
|
|
101
107
|
//# sourceMappingURL=index.mjs.map
|
package/dist/esm/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import type { ESLint, Linter } from 'eslint';\nimport {\n GENERATED_OUTPUT_GLOBS,\n PLUGIN_NAMESPACE,\n RECOMMENDED_RULES,\n} from './preset';\nimport { enforceAdapterImport } from './rules/enforceAdapterImport';\nimport { noDynamicFieldAccess } from './rules/noDynamicFieldAccess';\nimport { noRawText } from './rules/noRawText';\nimport { noUnusedContent } from './rules/noUnusedContent';\nimport { staticDictionaryKey } from './rules/staticDictionaryKey';\nimport { getReactVersion } from './utils/reactVersion';\n\n/**\n * Every rule this plugin ships, keyed by the name used in configuration.\n *\n * The rules split into three families:\n * - **Coverage** (`no-raw-text`) — user-facing copy that never reached a\n * dictionary. Nothing else in the toolchain reports it.\n * - **Compiler contract** (`static-dictionary-key`, `no-dynamic-field-access`)\n * — call shapes that type-check and run, but that the Babel/SWC passes cannot\n * analyse, so they silently lose the optimize pass or read a purged field.\n * - **Dead content** (`no-unused-content`) — dictionaries and fields nothing\n * reads. Off in every preset: it is the only rule that scans the whole\n * project from disk, so enabling it is a cost the user should opt into.\n *\n * Deliberately absent: unknown dictionary keys, unknown field paths and missing\n * required locales. The generated `__DictionaryRegistry` module augmentation\n * already makes all three compile errors, and `@intlayer/lsp` reports the first\n * as an editor diagnostic — a third report would only add noise.\n */\nexport const rules = {\n 'no-raw-text': noRawText,\n 'static-dictionary-key': staticDictionaryKey,\n 'no-dynamic-field-access': noDynamicFieldAccess,\n 'enforce-adapter-import': enforceAdapterImport,\n 'no-unused-content': noUnusedContent,\n} satisfies ESLint.Plugin['rules'];\n\n/** Rule names this plugin exposes. */\nexport type IntlayerRuleName = keyof typeof rules;\n\nconst meta: { name: string; version: string } = {\n name: 'eslint-plugin-intlayer',\n version: '9.3.1',\n};\n\nconst plugin: ESLint.Plugin = { meta, rules };\n\n/** The flat configs this plugin ships. */\nexport type IntlayerConfigName = 'recommended' | 'strict' | 'contract-only';\n\n/**\n * Build a flat config enabling the given rules under the `intlayer/` prefix.\n *\n * @param name - Config name, surfaced by `--print-config` and the config inspector.\n * @param ruleSeverities - Severity per rule name.\n */\nconst createConfig = (\n name: string,\n ruleEntries: Partial<Record<IntlayerRuleName, Linter.RuleEntry>>\n): Linter.Config[] => [\n // A config carrying only `ignores` is a *global* ignore in flat config, so\n // this keeps ESLint away from generated output wherever the preset is spread.\n {\n name: `intlayer/${name}/ignores`,\n ignores: GENERATED_OUTPUT_GLOBS,\n },\n {\n name: `intlayer/${name}`,\n plugins: { [PLUGIN_NAMESPACE]: plugin },\n settings: {\n react: {\n version: getReactVersion(),\n },\n },\n rules: Object.fromEntries(\n Object.entries(ruleEntries).map(([ruleName, entry]) => [\n `${PLUGIN_NAMESPACE}/${ruleName}`,\n entry,\n ])\n ),\n },\n];\n\n/**\n * Flat configs, for `eslint.config.js`:\n *\n * ```js\n * import intlayer from 'eslint-plugin-intlayer';\n *\n * export default [...intlayer]; // or [...intlayer.configs.recommended]\n * ```\n *\n * - `recommended` — the compiler-contract rules as errors, plus `no-raw-text`\n * as a warning so an existing codebase is not blocked on day one.\n * - `strict` — everything as an error, including string literals outside JSX.\n * - `contract-only` — just the rules that protect the build output; use this\n * when translation coverage is tracked by `intlayer test` instead.\n *\n * `enforce-adapter-import` is opt-in: importing from the original package is\n * perfectly valid once the bundler alias is set up, so a project that has done\n * that should not be nagged. Only `strict` turns it on.\n *\n * `no-unused-content` is in no preset at all, `strict` included. It reads the\n * Intlayer configuration and walks the project's source files from disk, which\n * is a different order of cost from the AST-only rules and depends on build\n * output for its duplicate report — spreading a preset should never quietly\n * turn that on.\n */\nexport const configs: Record<IntlayerConfigName, Linter.Config[]> = {\n recommended: createConfig('recommended', RECOMMENDED_RULES),\n strict: createConfig('strict', {\n 'no-raw-text': ['error', { includeStringLiterals: true }],\n 'static-dictionary-key': 'error',\n 'no-dynamic-field-access': 'error',\n 'enforce-adapter-import': 'error',\n }),\n 'contract-only': createConfig('contract-only', {\n 'static-dictionary-key': 'error',\n 'no-dynamic-field-access': 'error',\n }),\n};\n\nexport {\n GENERATED_OUTPUT_GLOBS,\n getRecommendedRules,\n PLUGIN_NAMESPACE,\n PLUGIN_PACKAGE_NAME,\n RECOMMENDED_RULES,\n} from './preset';\nexport type { NoRawTextOptions } from './rules/noRawText';\nexport type { NoUnusedContentOptions } from './rules/noUnusedContent';\nexport { getReactVersion } from './utils/reactVersion';\nexport { meta };\n\n/** The plugin object, as consumed by `eslint.config.js`. */\nexport type IntlayerPlugin = ESLint.Plugin &\n Iterable<Linter.Config> & {\n meta: { name: string; version: string };\n rules: typeof rules;\n configs: Record<IntlayerConfigName, Linter.Config[]>;\n };\n\nconst intlayerPlugin: IntlayerPlugin = {\n meta,\n rules,\n configs,\n *[Symbol.iterator]() {\n yield* configs.recommended;\n },\n};\n\nexport default intlayerPlugin;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,QAAQ;CACnB,eAAe;CACf,yBAAyB;CACzB,2BAA2B;CAC3B,0BAA0B;CAC1B,qBAAqB;AACvB;AAKA,MAAM,OAA0C;CAC9C,MAAM;CACN,SAAS;AACX;AAEA,MAAM,SAAwB;CAAE;CAAM;AAAM;;;;;;;AAW5C,MAAM,gBACJ,MACA,gBACoB,CAGpB;CACE,MAAM,YAAY,KAAK;CACvB,SAAS;AACX,GACA;CACE,MAAM,YAAY;CAClB,SAAS,GAAG,mBAAmB,OAAO;CACtC,UAAU,EACR,OAAO,EACL,SAAS,gBAAgB,EAC3B,EACF;CACA,OAAO,OAAO,YACZ,OAAO,QAAQ,WAAW,CAAC,CAAC,KAAK,CAAC,UAAU,WAAW,CACrD,GAAG,iBAAiB,GAAG,YACvB,KACF,CAAC,CACH;AACF,CACF;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAa,UAAuD;CAClE,aAAa,aAAa,eAAe,iBAAiB;CAC1D,QAAQ,aAAa,UAAU;EAC7B,eAAe,CAAC,SAAS,EAAE,uBAAuB,KAAK,CAAC;EACxD,yBAAyB;EACzB,2BAA2B;EAC3B,0BAA0B;CAC5B,CAAC;CACD,iBAAiB,aAAa,iBAAiB;EAC7C,yBAAyB;EACzB,2BAA2B;CAC7B,CAAC;AACH;AAsBA,MAAM,iBAAiC;CACrC;CACA;CACA;CACA,EAAE,OAAO,YAAY;EACnB,OAAO,QAAQ;CACjB;AACF"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/preset.ts
|
|
2
|
+
/** npm package name of this plugin. */
|
|
3
|
+
const PLUGIN_PACKAGE_NAME = "eslint-plugin-intlayer";
|
|
4
|
+
/** Prefix the rules are registered under, e.g. `intlayer/no-raw-text`. */
|
|
5
|
+
const PLUGIN_NAMESPACE = "intlayer";
|
|
6
|
+
/**
|
|
7
|
+
* Intlayer's generated output, which every preset ignores.
|
|
8
|
+
*
|
|
9
|
+
* `.intlayer/` holds compiled dictionaries and generated types — machine-written
|
|
10
|
+
* files that are never source. Linting them is at best wasted work and at worst
|
|
11
|
+
* a hard failure, since third-party rules can crash on generated shapes they
|
|
12
|
+
* were never meant to see. The rest of the toolchain already excludes this
|
|
13
|
+
* directory (`content.watchExclude`, the build globs), so the lint preset
|
|
14
|
+
* matches.
|
|
15
|
+
*/
|
|
16
|
+
const GENERATED_OUTPUT_GLOBS = ["**/.intlayer/**"];
|
|
17
|
+
/**
|
|
18
|
+
* Rules enabled by the `recommended` preset, keyed by unprefixed rule name.
|
|
19
|
+
*
|
|
20
|
+
* Exported because oxlint resolves a plugin's *rules* but not its shipped
|
|
21
|
+
* *configs*: `intlayer init` reads this to spell the preset out explicitly in
|
|
22
|
+
* `.oxlintrc.json`. Keeping the definition next to the rules it names means
|
|
23
|
+
* enabling one by default is a single-line change.
|
|
24
|
+
*
|
|
25
|
+
* `no-raw-text` stays a warning on purpose — pointed at an existing codebase it
|
|
26
|
+
* surfaces every untranslated string at once, which should not break a build on
|
|
27
|
+
* day one. `enforce-adapter-import` is deliberately absent: importing from the
|
|
28
|
+
* original package is valid once the bundler alias is configured, so it is
|
|
29
|
+
* opt-in through the `strict` preset. `no-unused-content` is absent from every
|
|
30
|
+
* preset — it scans the project from disk, which no preset should impose.
|
|
31
|
+
*/
|
|
32
|
+
const RECOMMENDED_RULES = {
|
|
33
|
+
"no-raw-text": "warn",
|
|
34
|
+
"static-dictionary-key": "error",
|
|
35
|
+
"no-dynamic-field-access": "error"
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The `recommended` preset with every rule name prefixed by the plugin
|
|
39
|
+
* namespace — the shape both ESLint flat config and `.oxlintrc.json` expect.
|
|
40
|
+
*
|
|
41
|
+
* `intlayer init` uses this to write the preset into `.oxlintrc.json`, since
|
|
42
|
+
* oxlint resolves a plugin's rules but not its configs.
|
|
43
|
+
*/
|
|
44
|
+
const getRecommendedRules = () => Object.fromEntries(Object.entries(RECOMMENDED_RULES).map(([ruleName, entry]) => [`${PLUGIN_NAMESPACE}/${ruleName}`, entry]));
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { GENERATED_OUTPUT_GLOBS, PLUGIN_NAMESPACE, PLUGIN_PACKAGE_NAME, RECOMMENDED_RULES, getRecommendedRules };
|
|
48
|
+
//# sourceMappingURL=preset.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preset.mjs","names":[],"sources":["../../src/preset.ts"],"sourcesContent":["import type { Linter } from 'eslint';\nimport type { IntlayerRuleName } from './index';\n\n/** npm package name of this plugin. */\nexport const PLUGIN_PACKAGE_NAME = 'eslint-plugin-intlayer';\n\n/** Prefix the rules are registered under, e.g. `intlayer/no-raw-text`. */\nexport const PLUGIN_NAMESPACE = 'intlayer';\n\n/**\n * Intlayer's generated output, which every preset ignores.\n *\n * `.intlayer/` holds compiled dictionaries and generated types — machine-written\n * files that are never source. Linting them is at best wasted work and at worst\n * a hard failure, since third-party rules can crash on generated shapes they\n * were never meant to see. The rest of the toolchain already excludes this\n * directory (`content.watchExclude`, the build globs), so the lint preset\n * matches.\n */\nexport const GENERATED_OUTPUT_GLOBS = ['**/.intlayer/**'];\n\n/**\n * Rules enabled by the `recommended` preset, keyed by unprefixed rule name.\n *\n * Exported because oxlint resolves a plugin's *rules* but not its shipped\n * *configs*: `intlayer init` reads this to spell the preset out explicitly in\n * `.oxlintrc.json`. Keeping the definition next to the rules it names means\n * enabling one by default is a single-line change.\n *\n * `no-raw-text` stays a warning on purpose — pointed at an existing codebase it\n * surfaces every untranslated string at once, which should not break a build on\n * day one. `enforce-adapter-import` is deliberately absent: importing from the\n * original package is valid once the bundler alias is configured, so it is\n * opt-in through the `strict` preset. `no-unused-content` is absent from every\n * preset — it scans the project from disk, which no preset should impose.\n */\nexport const RECOMMENDED_RULES: Partial<\n Record<IntlayerRuleName, Linter.RuleEntry>\n> = {\n 'no-raw-text': 'warn',\n 'static-dictionary-key': 'error',\n 'no-dynamic-field-access': 'error',\n};\n\n/**\n * The `recommended` preset with every rule name prefixed by the plugin\n * namespace — the shape both ESLint flat config and `.oxlintrc.json` expect.\n *\n * `intlayer init` uses this to write the preset into `.oxlintrc.json`, since\n * oxlint resolves a plugin's rules but not its configs.\n */\nexport const getRecommendedRules = (): Record<string, Linter.RuleEntry> =>\n Object.fromEntries(\n Object.entries(RECOMMENDED_RULES).map(([ruleName, entry]) => [\n `${PLUGIN_NAMESPACE}/${ruleName}`,\n entry,\n ])\n );\n"],"mappings":";;AAIA,MAAa,sBAAsB;;AAGnC,MAAa,mBAAmB;;;;;;;;;;;AAYhC,MAAa,yBAAyB,CAAC,iBAAiB;;;;;;;;;;;;;;;;AAiBxD,MAAa,oBAET;CACF,eAAe;CACf,yBAAyB;CACzB,2BAA2B;AAC7B;;;;;;;;AASA,MAAa,4BACX,OAAO,YACL,OAAO,QAAQ,iBAAiB,CAAC,CAAC,KAAK,CAAC,UAAU,WAAW,CAC3D,GAAG,iBAAiB,GAAG,YACvB,KACF,CAAC,CACH"}
|
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
import { toAstNode } from "../utils/ast.mjs";
|
|
2
2
|
import { createRule } from "../utils/createRule.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { COMPAT_CALLERS } from "@intlayer/config/callers";
|
|
4
4
|
|
|
5
5
|
//#region src/rules/enforceAdapterImport.ts
|
|
6
6
|
const ADAPTER_SCOPE = "@intlayer/";
|
|
7
7
|
/**
|
|
8
|
-
* Original library specifier → the `@intlayer/*` adapter that
|
|
8
|
+
* Original third-party library specifier → the `@intlayer/*` adapter that
|
|
9
|
+
* replaces it, e.g. `next-intl` → `@intlayer/next-intl`.
|
|
9
10
|
*
|
|
10
11
|
* Derived from the shared registry rather than hard-coded: every compat
|
|
11
12
|
* descriptor lists both its original specifiers and their adapter equivalents in
|
|
12
13
|
* `importSources`, so a library added to `@intlayer/config/callers` is covered
|
|
13
14
|
* here for free.
|
|
14
15
|
*
|
|
16
|
+
* Built from `COMPAT_CALLERS` only, never `ALL_CALLERS`. Intlayer's own packages
|
|
17
|
+
* are not a migration pair: `BASE_CALLERS` lists `intlayer` alongside
|
|
18
|
+
* `@intlayer/core` because `getIntlayer` is importable from both, and treating
|
|
19
|
+
* the scoped one as an "adapter" would tell users to rewrite ordinary
|
|
20
|
+
* `from 'intlayer'` imports.
|
|
21
|
+
*
|
|
15
22
|
* Sub-paths line up by position within a descriptor — `next-intl/server` and
|
|
16
23
|
* `@intlayer/next-intl/server` are listed together — so the mapping is built by
|
|
17
24
|
* matching each original against the adapter that ends with the same tail.
|
|
18
25
|
*/
|
|
19
26
|
const buildAdapterMap = () => {
|
|
20
27
|
const adapterBySpecifier = /* @__PURE__ */ new Map();
|
|
21
|
-
for (const descriptor of
|
|
28
|
+
for (const descriptor of COMPAT_CALLERS) {
|
|
22
29
|
const adapters = descriptor.importSources.filter((source) => source.startsWith(ADAPTER_SCOPE));
|
|
23
30
|
if (adapters.length === 0) continue;
|
|
24
31
|
const originals = descriptor.importSources.filter((source) => !source.startsWith(ADAPTER_SCOPE));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enforceAdapterImport.mjs","names":[],"sources":["../../../src/rules/enforceAdapterImport.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"enforceAdapterImport.mjs","names":[],"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,cAAc,gBAAgB;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,uBAAuB,WAClC,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,YAAY,UAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;GAEA,yBAAyB,SAAS;IAChC,YAAY,UAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;GAEA,uBAAuB,SAAS;IAC9B,YAAY,UAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;GAEA,mBAAmB,SAAS;IAC1B,YAAY,UAAU,IAAI,CAAC,CAAC,SAAoB;GAClD;EACF;CACF;AACF,CACF"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { getJsxAttributeName, getJsxAttributeValueNode, getJsxElementName, getStaticStringValue, toAstNode } from "../utils/ast.mjs";
|
|
2
2
|
import { createRule } from "../utils/createRule.mjs";
|
|
3
|
+
import { shouldExtract } from "../@intlayer/config/dist/esm/extract/shouldExtract.mjs";
|
|
4
|
+
import { ATTRIBUTES_TO_EXTRACT } from "../@intlayer/config/dist/esm/extract/constants.mjs";
|
|
3
5
|
import { combineVisitors, createTemplateVisitor } from "../utils/templates.mjs";
|
|
4
6
|
import { FILE_EXTENSIONS } from "@intlayer/config/defaultValues";
|
|
5
|
-
import { ATTRIBUTES_TO_EXTRACT, shouldExtract } from "@intlayer/config/extract";
|
|
6
7
|
|
|
7
8
|
//#region src/rules/noRawText.ts
|
|
8
9
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noRawText.mjs","names":[],"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":";;;;;;;;;;;AAsBA,MAAM,2BAA2B;CAAC;CAAQ;CAAO;CAAU;AAAO;;;;;;AA4BlE,MAAM,4BAA4B,aAChC,gBAAgB,MAAM,cAAc,SAAS,SAAS,SAAS,CAAC;;;;;;AAOlE,MAAM,2BACJ,SACkB;CAClB,IAAI,UAAU;CAEd,OAAO,SAAS;EACd,IAAI,QAAQ,SAAS,cACnB,OAAO,kBAAkB,QAAQ,iBAA4B;EAG/D,IAAI,QAAQ,SAAS,qBACnB,OAAO,kBAAkB,OAAO;EAGlC,UAAU,QAAQ;CACpB;CAEA,OAAO;AACT;;;;;;;;AASA,MAAM,wBAAwB,SAAqC;CACjE,MAAM,SAAS,KAAK;CAEpB,IAAI,CAAC,QAAQ,OAAO;CAEpB,QAAQ,OAAO,MAAf;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,uBACH,OAAO;EAET,KAAK,YAEH,OAAO,OAAO,WAAW,QAAQ,OAAO,gBAAgB;EAE1D,KAAK,oBACH,OAAO,OAAO,gBAAgB;EAEhC,SACE,OAAO;CACX;AACF;AAEA,MAAa,YAAY,WACvB,eACA;CACE,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,6EACJ;EACA,UAAU;GACR,SACE;GACF,cACE;GACF,kBACE;EACJ;EACA,QAAQ,CACN;GACE,MAAM;GACN,YAAY;IACV,YAAY;KAAE,MAAM;KAAS,OAAO,EAAE,MAAM,SAAS;IAAE;IACvD,gBAAgB;KAAE,MAAM;KAAS,OAAO,EAAE,MAAM,SAAS;IAAE;IAC3D,gBAAgB;KAAE,MAAM;KAAS,OAAO,EAAE,MAAM,SAAS;IAAE;IAC3D,uBAAuB,EAAE,MAAM,UAAU;GAC3C;GACA,sBAAsB;EACxB,CACF;CACF;CAEA,SAAS,YAAY;EACnB,MAAM,WAAW,QAAQ;EAGzB,IAAI,yBAAyB,QAAQ,GAAG,OAAO,CAAC;EAEhD,MAAM,CAAC,UAAU,CAAC,KAAK,QAAQ;EAC/B,MAAM,aAAa,IAAI,IACrB,QAAQ,cAAc,qBACxB;EACA,MAAM,kBAAkB,IAAI,IAC1B,QAAQ,kBAAkB,wBAC5B;EACA,MAAM,kBAAkB,QAAQ,kBAAkB,CAAC,EAAC,CAAE,KACnD,YAAY,IAAI,OAAO,SAAS,GAAG,CACtC;EACA,MAAM,wBAAwB,QAAQ,0BAA0B;;EAGhE,MAAM,oBAAoB,SACxB,cAAc,IAAI,KAClB,CAAC,eAAe,MAAM,YAAY,QAAQ,KAAK,KAAK,KAAK,CAAC,CAAC;;EAG7D,MAAM,cAAc,SAAyB;GAC3C,MAAM,aAAa,KAAK,QAAQ,SAAS,GAAG,CAAC,CAAC,KAAK;GAEnD,OAAO,WAAW,SAAS,KACvB,GAAG,WAAW,MAAM,GAAG,EAAE,EAAE,KAC3B;EACN;;;;;;;EAQA,MAAM,kBAAkB,uBACrB,EAAE,MAAM,MAAM,eAAe,kBAAkB;GAC9C,IAAI,eAAe,gBAAgB,IAAI,WAAW,GAAG;GAErD,IAAI,kBAAkB,QAAQ,CAAC,WAAW,IAAI,aAAa,GAAG;GAE9D,IAAI,CAAC,iBAAiB,IAAI,GAAG;GAE7B,QAAQ,OAAO;IACP;IACN,WAAW,kBAAkB,OAAO,YAAY;IAChD,MAAM;KACJ,MAAM,WAAW,IAAI;KACrB,GAAI,kBAAkB,OAAO,CAAC,IAAI,EAAE,WAAW,cAAc;IAC/D;GACF,CAAC;EACH,CACF;EA+DA,OAAO,gBACL,QAAQ,WAAW,gBACnB;GA9DA,UAAU,SAAS;IACjB,MAAM,UAAU,UAAU,IAAa;IACvC,MAAM,OAAO,QAAQ;IAErB,IAAI,CAAC,iBAAiB,IAAI,GAAG;IAE7B,MAAM,cAAc,wBAAwB,QAAQ,MAAM;IAE1D,IAAI,eAAe,gBAAgB,IAAI,WAAW,GAAG;IAErD,QAAQ,OAAO;KACP;KACN,WAAW;KACX,MAAM,EAAE,MAAM,WAAW,IAAI,EAAE;IACjC,CAAC;GACH;GAEA,eAAe,SAAS;IACtB,MAAM,UAAU,UAAU,IAAa;IACvC,MAAM,gBAAgB,oBAAoB,OAAO;IAEjD,IAAI,CAAC,iBAAiB,CAAC,WAAW,IAAI,aAAa,GAAG;IAEtD,MAAM,YAAY,yBAAyB,OAAO;IAClD,MAAM,OAAO,qBAAqB,SAAS;IAE3C,IAAI,SAAS,QAAQ,CAAC,iBAAiB,IAAI,GAAG;IAE9C,MAAM,cAAc,wBAAwB,QAAQ,MAAM;IAE1D,IAAI,eAAe,gBAAgB,IAAI,WAAW,GAAG;IAErD,QAAQ,OAAO;KACP;KACN,WAAW;KACX,MAAM;MAAE,WAAW;MAAe,MAAM,WAAW,IAAI;KAAE;IAC3D,CAAC;GACH;GAEA,UAAU,SAAS;IACjB,IAAI,CAAC,uBAAuB;IAE5B,MAAM,UAAU,UAAU,IAAa;IAEvC,IAAI,OAAO,QAAQ,aAAa,UAAU;IAE1C,IAAI,qBAAqB,OAAO,GAAG;IAEnC,MAAM,OAAO,QAAQ;IAErB,IAAI,CAAC,iBAAiB,IAAI,GAAG;IAE7B,QAAQ,OAAO;KACP;KACN,WAAW;KACX,MAAM,EAAE,MAAM,WAAW,IAAI,EAAE;IACjC,CAAC;GACH;EAKY,GACZ,eACF;CACF;AACF,CACF"}
|
|
1
|
+
{"version":3,"file":"noRawText.mjs","names":[],"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,142 @@
|
|
|
1
|
+
import { toAstNode } from "../utils/ast.mjs";
|
|
2
|
+
import { createRule } from "../utils/createRule.mjs";
|
|
3
|
+
import { readContentDeclaration } from "../utils/contentDeclaration.mjs";
|
|
4
|
+
import { getDuplicateDeclarations, getProjectConfiguration, getProjectUsageIndex, stripModuleExtension } from "../utils/projectUsage.mjs";
|
|
5
|
+
import { FILE_EXTENSIONS } from "@intlayer/config/defaultValues";
|
|
6
|
+
|
|
7
|
+
//#region src/rules/noUnusedContent.ts
|
|
8
|
+
/** Lifetime of the cached project scan, in milliseconds. */
|
|
9
|
+
const DEFAULT_CACHE_TTL_MS = 3e4;
|
|
10
|
+
/**
|
|
11
|
+
* True for a content declaration file — the only files this rule inspects.
|
|
12
|
+
*
|
|
13
|
+
* @param filename - Absolute path of the linted file.
|
|
14
|
+
* @param contentFileExtensions - Extensions the project declares content with.
|
|
15
|
+
*/
|
|
16
|
+
const isContentDeclarationFile = (filename, contentFileExtensions) => contentFileExtensions.some((extension) => filename.endsWith(extension));
|
|
17
|
+
/**
|
|
18
|
+
* True when a shorter prefix of `fieldPath` is already reported, so the nested
|
|
19
|
+
* field would only repeat what the parent report says.
|
|
20
|
+
*
|
|
21
|
+
* @param fieldPath - Dotted path being considered.
|
|
22
|
+
* @param reportedPaths - Paths already reported for this dictionary.
|
|
23
|
+
*/
|
|
24
|
+
const hasReportedAncestor = (fieldPath, reportedPaths) => {
|
|
25
|
+
const segments = fieldPath.split(".");
|
|
26
|
+
for (let depth = 1; depth < segments.length; depth++) if (reportedPaths.has(segments.slice(0, depth).join("."))) return true;
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Compile the `ignoreFields` option, dropping any source that is not a valid
|
|
31
|
+
* regular expression rather than failing the whole lint run.
|
|
32
|
+
*
|
|
33
|
+
* @param sources - Regular-expression sources from the rule options.
|
|
34
|
+
*/
|
|
35
|
+
const compileIgnorePatterns = (sources) => {
|
|
36
|
+
const patterns = [];
|
|
37
|
+
for (const source of sources) try {
|
|
38
|
+
patterns.push(new RegExp(source));
|
|
39
|
+
} catch {}
|
|
40
|
+
return patterns;
|
|
41
|
+
};
|
|
42
|
+
const noUnusedContent = createRule("no-unused-content", {
|
|
43
|
+
meta: {
|
|
44
|
+
type: "suggestion",
|
|
45
|
+
docs: { description: "Report Intlayer dictionaries and content fields that nothing in the project reads" },
|
|
46
|
+
schema: [{
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
reportUnusedDictionaries: { type: "boolean" },
|
|
50
|
+
reportUnusedFields: { type: "boolean" },
|
|
51
|
+
reportDuplicateKeys: { type: "boolean" },
|
|
52
|
+
ignoreFields: {
|
|
53
|
+
type: "array",
|
|
54
|
+
items: { type: "string" }
|
|
55
|
+
},
|
|
56
|
+
baseDir: { type: "string" },
|
|
57
|
+
cacheTtl: {
|
|
58
|
+
type: "number",
|
|
59
|
+
minimum: 0
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
additionalProperties: false
|
|
63
|
+
}],
|
|
64
|
+
messages: {
|
|
65
|
+
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.",
|
|
66
|
+
unusedField: "The content field `{{fieldPath}}` of `{{dictionaryKey}}` is never read. The compiler strips it from the build, so it only costs translation work.",
|
|
67
|
+
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."
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
create: (context) => {
|
|
71
|
+
const [options = {}] = context.options;
|
|
72
|
+
const { reportUnusedDictionaries = true, reportUnusedFields = true, reportDuplicateKeys = true, ignoreFields = [], baseDir = context.cwd, cacheTtl = DEFAULT_CACHE_TTL_MS } = options;
|
|
73
|
+
const configuration = getProjectConfiguration(baseDir, cacheTtl);
|
|
74
|
+
const contentFileExtensions = configuration?.content.fileExtensions ?? FILE_EXTENSIONS;
|
|
75
|
+
if (!isContentDeclarationFile(context.filename, contentFileExtensions)) return {};
|
|
76
|
+
const ignorePatterns = compileIgnorePatterns(ignoreFields);
|
|
77
|
+
/** Intlayer declares one dictionary per file; the outermost match is it. */
|
|
78
|
+
let hasHandledDeclaration = false;
|
|
79
|
+
/** Report every field of a dictionary that nothing in the project reads. */
|
|
80
|
+
const reportUnusedFieldsOf = (declaration, usage) => {
|
|
81
|
+
if (usage.tracksEveryField || declaration.isPartiallyEnumerated) return;
|
|
82
|
+
const reportedPaths = /* @__PURE__ */ new Set();
|
|
83
|
+
for (const field of declaration.fields) {
|
|
84
|
+
if (usage.usedFieldPaths.has(field.path)) continue;
|
|
85
|
+
if (hasReportedAncestor(field.path, reportedPaths)) continue;
|
|
86
|
+
if (ignorePatterns.some((pattern) => pattern.test(field.path))) continue;
|
|
87
|
+
reportedPaths.add(field.path);
|
|
88
|
+
context.report({
|
|
89
|
+
node: field.keyNode,
|
|
90
|
+
messageId: "unusedField",
|
|
91
|
+
data: {
|
|
92
|
+
fieldPath: field.path,
|
|
93
|
+
dictionaryKey: declaration.dictionaryKey
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
/** Report the key when the same dictionary is declared somewhere else too. */
|
|
99
|
+
const reportDuplicatesOf = (declaration) => {
|
|
100
|
+
if (!reportDuplicateKeys || !configuration) return;
|
|
101
|
+
const duplicates = getDuplicateDeclarations(configuration, declaration.dictionaryKey, context.filename);
|
|
102
|
+
if (duplicates.length === 0) return;
|
|
103
|
+
context.report({
|
|
104
|
+
node: declaration.keyNode,
|
|
105
|
+
messageId: "duplicateDictionary",
|
|
106
|
+
data: {
|
|
107
|
+
dictionaryKey: declaration.dictionaryKey,
|
|
108
|
+
locations: duplicates.map((duplicate) => duplicate.location === "remote" ? "the CMS" : `\`${duplicate.filePath}\``).join(", ")
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
/** Report the declaration itself when nothing references its key. */
|
|
113
|
+
const reportUsage = (declaration, index) => {
|
|
114
|
+
const isImportedDirectly = index.importedModulePaths.has(stripModuleExtension(context.filename));
|
|
115
|
+
const usage = index.byDictionaryKey.get(declaration.dictionaryKey);
|
|
116
|
+
if (!usage) {
|
|
117
|
+
if (reportUnusedDictionaries && !isImportedDirectly) context.report({
|
|
118
|
+
node: declaration.keyNode,
|
|
119
|
+
messageId: "unusedDictionary",
|
|
120
|
+
data: { dictionaryKey: declaration.dictionaryKey }
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (reportUnusedFields && !isImportedDirectly) reportUnusedFieldsOf(declaration, usage);
|
|
125
|
+
};
|
|
126
|
+
return { ObjectExpression: (node) => {
|
|
127
|
+
if (hasHandledDeclaration) return;
|
|
128
|
+
const declaration = readContentDeclaration(toAstNode(node));
|
|
129
|
+
if (!declaration) return;
|
|
130
|
+
hasHandledDeclaration = true;
|
|
131
|
+
reportDuplicatesOf(declaration);
|
|
132
|
+
if (!reportUnusedDictionaries && !reportUnusedFields) return;
|
|
133
|
+
const index = getProjectUsageIndex(baseDir, cacheTtl);
|
|
134
|
+
if (!index.isAvailable) return;
|
|
135
|
+
reportUsage(declaration, index);
|
|
136
|
+
} };
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
//#endregion
|
|
141
|
+
export { noUnusedContent };
|
|
142
|
+
//# sourceMappingURL=noUnusedContent.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noUnusedContent.mjs","names":[],"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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"}
|
|
@@ -24,8 +24,8 @@ const staticDictionaryKey = createRule("static-dictionary-key", {
|
|
|
24
24
|
docs: { description: "Require the dictionary key of an Intlayer content read to be a static string literal" },
|
|
25
25
|
messages: {
|
|
26
26
|
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.",
|
|
27
|
-
dynamicTemplateKey: "The dictionary key passed to `{{caller}}` interpolates a value.
|
|
28
|
-
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."
|
|
27
|
+
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.",
|
|
28
|
+
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"
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
create: (context) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staticDictionaryKey.mjs","names":[],"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.
|
|
1
|
+
{"version":3,"file":"staticDictionaryKey.mjs","names":[],"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,sBAAsB,WACjC,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,oBAAoB,qCAAqB,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,oBAAoB,qBAClB,qBAAqB,UAAU,IAAI,CAAC,CACtC;GACF;GAEA,iBAAiB,SAAS;IACxB,MAAM,QAAQ,YAAY,UAAU,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,QAAQ,YAAY,UAAU,IAAI,GAAG,iBAAiB;IAE5D,IAAI,OAAO,uBAAuB,KAAK;GACzC;GAEA,oBAAoB,SAAS;IAC3B,MAAM,UAAU,UAAU,IAAI;IAC9B,MAAM,aAAa,eACjB,kBAAkB,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,gBAAgB,oBAAoB,SAAS;KAEnD,IAAI,CAAC,iBAAiB,CAAC,iBAAiB,SAAS,aAAa,GAC5D;KAGF,MAAM,YAAY,yBAAyB,SAAS;KAEpD,IAAI,CAAC,WAAW;KAEhB,IAAI,qBAAqB,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"}
|