eslint-plugin-intlayer 9.3.1 → 9.3.2
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/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/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/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/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/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 +4 -2
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { __require } from "../_virtual/_rolldown/runtime.mjs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/utils/projectUsage.ts
|
|
7
|
+
/** `require` resolved from this package — the fallback when the host project cannot resolve a module. */
|
|
8
|
+
const packageRequire = typeof import.meta.url === "string" ? createRequire(import.meta.url) : __require;
|
|
9
|
+
/**
|
|
10
|
+
* Require a module from the linted project first, falling back to this
|
|
11
|
+
* package's own resolution.
|
|
12
|
+
*
|
|
13
|
+
* Preferring the project copy keeps the analyser in step with the Intlayer
|
|
14
|
+
* version the project actually builds with; the fallback covers strict
|
|
15
|
+
* `node_modules` layouts (pnpm) where the plugin's own dependencies are not
|
|
16
|
+
* hoisted into the project tree.
|
|
17
|
+
*
|
|
18
|
+
* @param moduleName - Specifier to resolve.
|
|
19
|
+
* @param baseDir - Directory the project resolution starts from.
|
|
20
|
+
*/
|
|
21
|
+
const requireFromProject = (moduleName, baseDir) => {
|
|
22
|
+
try {
|
|
23
|
+
return createRequire(join(baseDir, "noop.js"))(moduleName);
|
|
24
|
+
} catch {}
|
|
25
|
+
try {
|
|
26
|
+
return packageRequire(moduleName);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Extensions the `@intlayer/lsp` analyser parses directly.
|
|
33
|
+
*
|
|
34
|
+
* Single-file components (`.vue`, `.svelte`, `.astro`) hold their script in a
|
|
35
|
+
* markup wrapper the parser rejects, so they take the conservative path below
|
|
36
|
+
* instead of being silently read as "uses nothing".
|
|
37
|
+
*/
|
|
38
|
+
const ANALYZABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
39
|
+
".ts",
|
|
40
|
+
".tsx",
|
|
41
|
+
".mts",
|
|
42
|
+
".cts",
|
|
43
|
+
".js",
|
|
44
|
+
".jsx",
|
|
45
|
+
".mjs",
|
|
46
|
+
".cjs"
|
|
47
|
+
]);
|
|
48
|
+
/** Extensions stripped when comparing an import specifier to a file path. */
|
|
49
|
+
const MODULE_EXTENSIONS = [
|
|
50
|
+
".ts",
|
|
51
|
+
".tsx",
|
|
52
|
+
".mts",
|
|
53
|
+
".cts",
|
|
54
|
+
".js",
|
|
55
|
+
".jsx",
|
|
56
|
+
".mjs",
|
|
57
|
+
".cjs",
|
|
58
|
+
".vue",
|
|
59
|
+
".svelte",
|
|
60
|
+
".astro"
|
|
61
|
+
];
|
|
62
|
+
/**
|
|
63
|
+
* Drop the module extension from a path so `./home.content` and
|
|
64
|
+
* `/project/src/home.content.ts` compare equal.
|
|
65
|
+
*
|
|
66
|
+
* @param filePath - Absolute or relative path.
|
|
67
|
+
*/
|
|
68
|
+
const stripModuleExtension = (filePath) => {
|
|
69
|
+
const extension = extname(filePath);
|
|
70
|
+
return MODULE_EXTENSIONS.includes(extension) ? filePath.slice(0, -extension.length) : filePath;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Relative import specifiers, covering the static form, dynamic `import()` and
|
|
74
|
+
* `require()`. Bare specifiers are ignored: a content declaration is always
|
|
75
|
+
* reached through a relative path or an alias, and alias resolution would need
|
|
76
|
+
* the project's TypeScript configuration.
|
|
77
|
+
*/
|
|
78
|
+
const RELATIVE_IMPORT_REGEX = /(?:from|import|require)\s*\(?\s*['"](\.{1,2}\/[^'"]+)['"]/g;
|
|
79
|
+
/**
|
|
80
|
+
* `nest("other-dictionary")` inside a content declaration.
|
|
81
|
+
*
|
|
82
|
+
* Nesting is the one way a dictionary is consumed from another *content* file,
|
|
83
|
+
* and content files are excluded from the usage scan — without this the nested
|
|
84
|
+
* dictionary would look unreferenced.
|
|
85
|
+
*/
|
|
86
|
+
const NEST_CALL_REGEX = /\bnest\s*(?:<[^<>()]*>)?\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
87
|
+
/**
|
|
88
|
+
* Dictionary keys passed to a caller, for the conservative fallback.
|
|
89
|
+
*
|
|
90
|
+
* Built from the shared caller registry so it recognises the compat adapters
|
|
91
|
+
* too, and matched against files the precise analyser could not read.
|
|
92
|
+
*
|
|
93
|
+
* @param callerNamesAlternation - `useIntlayer|useTranslations|…`.
|
|
94
|
+
*/
|
|
95
|
+
const buildFallbackKeyRegex = (callerNamesAlternation) => new RegExp(`\\b(?:${callerNamesAlternation})\\b\\s*(?:<[^<>()]*>)?\\s*\\(\\s*(?:['"\`]([^'"\`]+)['"\`]|\\{[^{}]*?(?:id|namespace)\\s*:\\s*['"\`]([^'"\`]+)['"\`])`, "g");
|
|
96
|
+
/**
|
|
97
|
+
* Cheap pre-filter: does this file mention any caller at all?
|
|
98
|
+
*
|
|
99
|
+
* @param callerNamesAlternation - `useIntlayer|useTranslations|…`.
|
|
100
|
+
*/
|
|
101
|
+
const buildCallerMentionRegex = (callerNamesAlternation) => new RegExp(`\\b(?:${callerNamesAlternation})\\b`);
|
|
102
|
+
const configurationCache = /* @__PURE__ */ new Map();
|
|
103
|
+
const usageIndexCache = /* @__PURE__ */ new Map();
|
|
104
|
+
/** An index that reports nothing, used whenever the scan cannot run. */
|
|
105
|
+
const UNAVAILABLE_INDEX = {
|
|
106
|
+
byDictionaryKey: /* @__PURE__ */ new Map(),
|
|
107
|
+
importedModulePaths: /* @__PURE__ */ new Set(),
|
|
108
|
+
isAvailable: false
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Read a cache entry that has not outlived `cacheTtlMs`.
|
|
112
|
+
*
|
|
113
|
+
* @param cache - The cache to read.
|
|
114
|
+
* @param cacheKey - Entry key.
|
|
115
|
+
* @param cacheTtlMs - Lifetime in milliseconds; `0` disables caching entirely.
|
|
116
|
+
*/
|
|
117
|
+
const readCache = (cache, cacheKey, cacheTtlMs) => {
|
|
118
|
+
if (cacheTtlMs <= 0) return void 0;
|
|
119
|
+
const entry = cache.get(cacheKey);
|
|
120
|
+
if (!entry) return void 0;
|
|
121
|
+
if (Date.now() - entry.builtAt > cacheTtlMs) {
|
|
122
|
+
cache.delete(cacheKey);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
return entry.value;
|
|
126
|
+
};
|
|
127
|
+
/** Drop every cached configuration and usage index. Exposed for tests. */
|
|
128
|
+
const clearProjectUsageCache = () => {
|
|
129
|
+
configurationCache.clear();
|
|
130
|
+
usageIndexCache.clear();
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Load the Intlayer configuration for a project, or `null` when it cannot be
|
|
134
|
+
* read — a project without Intlayer installed must not break the lint run.
|
|
135
|
+
*
|
|
136
|
+
* @param baseDir - Project root, normally ESLint's working directory.
|
|
137
|
+
* @param cacheTtlMs - Lifetime of the cached configuration.
|
|
138
|
+
*/
|
|
139
|
+
const getProjectConfiguration = (baseDir, cacheTtlMs) => {
|
|
140
|
+
const cached = readCache(configurationCache, baseDir, cacheTtlMs);
|
|
141
|
+
if (cached !== void 0) return cached;
|
|
142
|
+
const configLoader = requireFromProject("@intlayer/config/node", baseDir);
|
|
143
|
+
let configuration = null;
|
|
144
|
+
try {
|
|
145
|
+
configuration = configLoader?.getConfiguration({ baseDir }) ?? null;
|
|
146
|
+
} catch {
|
|
147
|
+
configuration = null;
|
|
148
|
+
}
|
|
149
|
+
configurationCache.set(baseDir, {
|
|
150
|
+
builtAt: Date.now(),
|
|
151
|
+
value: configuration
|
|
152
|
+
});
|
|
153
|
+
return configuration;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Translate the configuration into the globs the scan runs, mirroring how the
|
|
157
|
+
* build pipeline decides which files are project source.
|
|
158
|
+
*
|
|
159
|
+
* Content declarations are excluded from the source scan — a dictionary must
|
|
160
|
+
* not count as its own consumer — and globbed separately for `nest()` calls.
|
|
161
|
+
*
|
|
162
|
+
* @param configuration - The project configuration.
|
|
163
|
+
*/
|
|
164
|
+
const getScanTargets = (configuration) => {
|
|
165
|
+
const { transformPattern } = configuration.compiler ?? {};
|
|
166
|
+
const compilerPatterns = transformPattern ? Array.isArray(transformPattern) ? transformPattern : [transformPattern] : [];
|
|
167
|
+
const allPatterns = [...configuration.build.traversePattern ?? [], ...compilerPatterns];
|
|
168
|
+
const contentFilePatterns = (configuration.content.fileExtensions ?? []).map((extension) => `**/*${extension}`);
|
|
169
|
+
const roots = [configuration.system.baseDir, ...configuration.content.codeDir ?? []].map((root) => resolve(root));
|
|
170
|
+
return {
|
|
171
|
+
roots: [...new Set(roots)],
|
|
172
|
+
includePatterns: allPatterns.filter((pattern) => !pattern.startsWith("!")),
|
|
173
|
+
excludePatterns: [...allPatterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1)), ...contentFilePatterns],
|
|
174
|
+
contentFilePatterns
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* List the files matching `patterns` under every root, de-duplicated.
|
|
179
|
+
*
|
|
180
|
+
* @param glob - The lazily required `fast-glob`.
|
|
181
|
+
* @param targets - Roots to walk.
|
|
182
|
+
* @param patterns - Patterns to include.
|
|
183
|
+
* @param excludePatterns - Patterns to skip.
|
|
184
|
+
*/
|
|
185
|
+
const listFiles = (glob, targets, patterns, excludePatterns) => {
|
|
186
|
+
if (patterns.length === 0) return [];
|
|
187
|
+
const files = /* @__PURE__ */ new Set();
|
|
188
|
+
for (const root of targets.roots) {
|
|
189
|
+
if (!existsSync(root)) continue;
|
|
190
|
+
try {
|
|
191
|
+
for (const file of glob.sync(patterns, {
|
|
192
|
+
cwd: root,
|
|
193
|
+
ignore: excludePatterns,
|
|
194
|
+
absolute: true,
|
|
195
|
+
dot: false,
|
|
196
|
+
followSymbolicLinks: false
|
|
197
|
+
})) files.add(file);
|
|
198
|
+
} catch {}
|
|
199
|
+
}
|
|
200
|
+
return [...files];
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* The usage record for `dictionaryKey`, created on first sight.
|
|
204
|
+
*
|
|
205
|
+
* @param index - Index being built.
|
|
206
|
+
* @param dictionaryKey - Key to look up.
|
|
207
|
+
*/
|
|
208
|
+
const getOrCreateUsage = (index, dictionaryKey) => {
|
|
209
|
+
const existing = index.byDictionaryKey.get(dictionaryKey);
|
|
210
|
+
if (existing) return existing;
|
|
211
|
+
const created = {
|
|
212
|
+
usedFieldPaths: /* @__PURE__ */ new Set(),
|
|
213
|
+
tracksEveryField: false
|
|
214
|
+
};
|
|
215
|
+
index.byDictionaryKey.set(dictionaryKey, created);
|
|
216
|
+
return created;
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Record a read of `fieldPath`, marking every parent prefix used as well:
|
|
220
|
+
* reading `hero.title` means the `hero` object is read too.
|
|
221
|
+
*
|
|
222
|
+
* @param usage - The record to update.
|
|
223
|
+
* @param fieldPath - Field path segments from the content root.
|
|
224
|
+
*/
|
|
225
|
+
const addUsedFieldPath = (usage, fieldPath) => {
|
|
226
|
+
for (let depth = 1; depth <= fieldPath.length; depth++) usage.usedFieldPaths.add(fieldPath.slice(0, depth).join("."));
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* Fold one analysed file into the index.
|
|
230
|
+
*
|
|
231
|
+
* Mirrors the VS Code decoration's interpretation of the analyser output: a
|
|
232
|
+
* bare content reference or a bound translator function means any field may be
|
|
233
|
+
* read, and a dictionary referenced without any trackable binding proves only
|
|
234
|
+
* that the dictionary itself is alive.
|
|
235
|
+
*
|
|
236
|
+
* @param index - Index being built.
|
|
237
|
+
* @param usages - Message usages found in the file.
|
|
238
|
+
* @param bindings - Caller bindings found in the file.
|
|
239
|
+
*/
|
|
240
|
+
const foldFileAnalysis = (index, usages, bindings) => {
|
|
241
|
+
const keysWithFieldUsage = /* @__PURE__ */ new Set();
|
|
242
|
+
for (const usage of usages) {
|
|
243
|
+
const record = getOrCreateUsage(index, usage.dictionaryKey);
|
|
244
|
+
if (usage.kind === "namespace") continue;
|
|
245
|
+
if (usage.fieldPath.length === 0) {
|
|
246
|
+
record.tracksEveryField = true;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
keysWithFieldUsage.add(usage.dictionaryKey);
|
|
250
|
+
addUsedFieldPath(record, usage.fieldPath);
|
|
251
|
+
}
|
|
252
|
+
const keysWithBinding = /* @__PURE__ */ new Set();
|
|
253
|
+
for (const binding of bindings) {
|
|
254
|
+
const record = getOrCreateUsage(index, binding.dictionaryKey);
|
|
255
|
+
keysWithBinding.add(binding.dictionaryKey);
|
|
256
|
+
if (binding.bindingKind === "translator") {
|
|
257
|
+
record.tracksEveryField = true;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (!keysWithFieldUsage.has(binding.dictionaryKey)) record.tracksEveryField = true;
|
|
261
|
+
}
|
|
262
|
+
for (const usage of usages) {
|
|
263
|
+
if (usage.kind !== "namespace") continue;
|
|
264
|
+
if (keysWithFieldUsage.has(usage.dictionaryKey)) continue;
|
|
265
|
+
if (keysWithBinding.has(usage.dictionaryKey)) continue;
|
|
266
|
+
getOrCreateUsage(index, usage.dictionaryKey).tracksEveryField = true;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* Fold a file the precise analyser could not read — a single-file component, or
|
|
271
|
+
* anything that failed to parse — by marking every dictionary key it mentions
|
|
272
|
+
* as fully used.
|
|
273
|
+
*
|
|
274
|
+
* @param index - Index being built.
|
|
275
|
+
* @param text - File contents.
|
|
276
|
+
* @param fallbackKeyRegex - Key-extraction pattern from the caller registry.
|
|
277
|
+
*/
|
|
278
|
+
const foldFallbackAnalysis = (index, text, fallbackKeyRegex) => {
|
|
279
|
+
fallbackKeyRegex.lastIndex = 0;
|
|
280
|
+
let match = fallbackKeyRegex.exec(text);
|
|
281
|
+
while (match) {
|
|
282
|
+
const dictionaryKey = match[1] ?? match[2];
|
|
283
|
+
if (dictionaryKey) {
|
|
284
|
+
const [rootKey] = dictionaryKey.split(".");
|
|
285
|
+
getOrCreateUsage(index, rootKey ?? dictionaryKey).tracksEveryField = true;
|
|
286
|
+
}
|
|
287
|
+
match = fallbackKeyRegex.exec(text);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Record every module a file imports through a relative specifier.
|
|
292
|
+
*
|
|
293
|
+
* @param index - Index being built.
|
|
294
|
+
* @param filePath - Absolute path of the importing file.
|
|
295
|
+
* @param text - File contents.
|
|
296
|
+
*/
|
|
297
|
+
const foldImports = (index, filePath, text) => {
|
|
298
|
+
RELATIVE_IMPORT_REGEX.lastIndex = 0;
|
|
299
|
+
let match = RELATIVE_IMPORT_REGEX.exec(text);
|
|
300
|
+
while (match) {
|
|
301
|
+
const specifier = match[1];
|
|
302
|
+
if (specifier) {
|
|
303
|
+
const resolved = resolve(dirname(filePath), specifier);
|
|
304
|
+
index.importedModulePaths.add(stripModuleExtension(resolved));
|
|
305
|
+
index.importedModulePaths.add(join(resolved, "index"));
|
|
306
|
+
}
|
|
307
|
+
match = RELATIVE_IMPORT_REGEX.exec(text);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
/**
|
|
311
|
+
* Mark dictionaries pulled in by `nest()` from another content declaration.
|
|
312
|
+
*
|
|
313
|
+
* @param index - Index being built.
|
|
314
|
+
* @param text - Content declaration source.
|
|
315
|
+
*/
|
|
316
|
+
const foldNestedDictionaries = (index, text) => {
|
|
317
|
+
NEST_CALL_REGEX.lastIndex = 0;
|
|
318
|
+
let match = NEST_CALL_REGEX.exec(text);
|
|
319
|
+
while (match) {
|
|
320
|
+
const dictionaryKey = match[1];
|
|
321
|
+
if (dictionaryKey) getOrCreateUsage(index, dictionaryKey).tracksEveryField = true;
|
|
322
|
+
match = NEST_CALL_REGEX.exec(text);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* Scan the project once and aggregate every dictionary reference it contains.
|
|
327
|
+
*
|
|
328
|
+
* @param baseDir - Project root, normally ESLint's working directory.
|
|
329
|
+
* @param cacheTtlMs - Lifetime of the cached index.
|
|
330
|
+
*/
|
|
331
|
+
const getProjectUsageIndex = (baseDir, cacheTtlMs) => {
|
|
332
|
+
const cached = readCache(usageIndexCache, baseDir, cacheTtlMs);
|
|
333
|
+
if (cached) return cached;
|
|
334
|
+
const index = buildProjectUsageIndex(baseDir, cacheTtlMs);
|
|
335
|
+
usageIndexCache.set(baseDir, {
|
|
336
|
+
builtAt: Date.now(),
|
|
337
|
+
value: index
|
|
338
|
+
});
|
|
339
|
+
return index;
|
|
340
|
+
};
|
|
341
|
+
/**
|
|
342
|
+
* The uncached body of {@link getProjectUsageIndex}.
|
|
343
|
+
*
|
|
344
|
+
* @param baseDir - Project root.
|
|
345
|
+
* @param cacheTtlMs - Lifetime of the cached configuration.
|
|
346
|
+
*/
|
|
347
|
+
const buildProjectUsageIndex = (baseDir, cacheTtlMs) => {
|
|
348
|
+
const configuration = getProjectConfiguration(baseDir, cacheTtlMs);
|
|
349
|
+
if (!configuration) return UNAVAILABLE_INDEX;
|
|
350
|
+
const analyzer = requireFromProject("@intlayer/lsp/utils", baseDir);
|
|
351
|
+
const globModule = requireFromProject("fast-glob", baseDir);
|
|
352
|
+
const glob = globModule?.sync ? globModule : globModule?.default;
|
|
353
|
+
if (!analyzer || !glob) return UNAVAILABLE_INDEX;
|
|
354
|
+
const targets = getScanTargets(configuration);
|
|
355
|
+
const sourceFiles = listFiles(glob, targets, targets.includePatterns, targets.excludePatterns);
|
|
356
|
+
if (sourceFiles.length === 0) return UNAVAILABLE_INDEX;
|
|
357
|
+
const callerNamesAlternation = analyzer.getCallerNamesAlternation();
|
|
358
|
+
const callerMentionRegex = buildCallerMentionRegex(callerNamesAlternation);
|
|
359
|
+
const fallbackKeyRegex = buildFallbackKeyRegex(callerNamesAlternation);
|
|
360
|
+
const index = {
|
|
361
|
+
byDictionaryKey: /* @__PURE__ */ new Map(),
|
|
362
|
+
importedModulePaths: /* @__PURE__ */ new Set(),
|
|
363
|
+
isAvailable: true
|
|
364
|
+
};
|
|
365
|
+
for (const filePath of sourceFiles) {
|
|
366
|
+
let text;
|
|
367
|
+
try {
|
|
368
|
+
text = readFileSync(filePath, "utf-8");
|
|
369
|
+
} catch {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
foldImports(index, filePath, text);
|
|
373
|
+
if (!callerMentionRegex.test(text)) continue;
|
|
374
|
+
if (!ANALYZABLE_EXTENSIONS.has(extname(filePath))) {
|
|
375
|
+
foldFallbackAnalysis(index, text, fallbackKeyRegex);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const usages = analyzer.collectMessageUsages(text);
|
|
379
|
+
if (usages.length === 0) {
|
|
380
|
+
foldFallbackAnalysis(index, text, fallbackKeyRegex);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
foldFileAnalysis(index, usages, analyzer.collectCallerBindings(text));
|
|
384
|
+
}
|
|
385
|
+
for (const filePath of listFiles(glob, targets, targets.contentFilePatterns, [])) try {
|
|
386
|
+
foldNestedDictionaries(index, readFileSync(filePath, "utf-8"));
|
|
387
|
+
} catch {}
|
|
388
|
+
return index;
|
|
389
|
+
};
|
|
390
|
+
/**
|
|
391
|
+
* Other declarations of the same dictionary key, read from the unmerged
|
|
392
|
+
* dictionaries the build writes to disk.
|
|
393
|
+
*
|
|
394
|
+
* Returns an empty list when the build output is missing — the rule then simply
|
|
395
|
+
* has nothing to say about duplicates.
|
|
396
|
+
*
|
|
397
|
+
* @param configuration - The project configuration.
|
|
398
|
+
* @param dictionaryKey - Key declared by the linted file.
|
|
399
|
+
* @param currentFilePath - Absolute path of the linted file.
|
|
400
|
+
*/
|
|
401
|
+
const getDuplicateDeclarations = (configuration, dictionaryKey, currentFilePath) => {
|
|
402
|
+
const dictionaryPath = join(configuration.system.unmergedDictionariesDir, `${dictionaryKey}.json`);
|
|
403
|
+
if (!existsSync(dictionaryPath)) return [];
|
|
404
|
+
let entries;
|
|
405
|
+
try {
|
|
406
|
+
const parsed = JSON.parse(readFileSync(dictionaryPath, "utf-8"));
|
|
407
|
+
if (!Array.isArray(parsed)) return [];
|
|
408
|
+
entries = parsed;
|
|
409
|
+
} catch {
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
const duplicates = [];
|
|
413
|
+
for (const entry of entries) {
|
|
414
|
+
if (entry.location === "remote") {
|
|
415
|
+
duplicates.push({
|
|
416
|
+
filePath: "remote",
|
|
417
|
+
location: "remote"
|
|
418
|
+
});
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (!entry.filePath) continue;
|
|
422
|
+
if (resolve(configuration.system.baseDir, entry.filePath) === currentFilePath) continue;
|
|
423
|
+
duplicates.push({
|
|
424
|
+
filePath: entry.filePath,
|
|
425
|
+
location: "local"
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
return duplicates;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
//#endregion
|
|
432
|
+
export { clearProjectUsageCache, getDuplicateDeclarations, getProjectConfiguration, getProjectUsageIndex, stripModuleExtension };
|
|
433
|
+
//# sourceMappingURL=projectUsage.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectUsage.mjs","names":[],"sources":["../../../src/utils/projectUsage.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, extname, join, resolve } from 'node:path';\nimport type { CallerVariableBinding, MessageUsage } from '@intlayer/lsp/utils';\n\n/**\n * Project-wide dictionary usage, shared by the `no-unused-content` rule.\n *\n * ESLint lints one file at a time and offers no end-of-run hook, so a rule that\n * needs to know whether a *declared* field is read *somewhere else* has to build\n * that knowledge itself. This module does exactly what the VS Code extension's\n * unused-content decoration does — glob the project's source files, run the\n * shared `@intlayer/lsp` analyser over each one, and aggregate the result —\n * behind a process-level cache so the scan happens once per lint run rather\n * than once per content file.\n *\n * Everything here is loaded lazily. The rule is off by default, and a plugin\n * that eagerly pulled a native parser and the config loader into every lint run\n * would tax the projects that never enable it.\n */\n\n// ── Lazily required modules ──────────────────────────────────────────────────\n\n/**\n * True when this file runs from the ESM build. `import.meta.url` is rewritten\n * to a `__filename`-based shim in the CJS build, so both branches below stay\n * correct whichever bundle the host loads.\n */\nconst isEsModule = typeof import.meta.url === 'string';\n\n/** `require` resolved from this package — the fallback when the host project cannot resolve a module. */\nconst packageRequire: NodeJS.Require = isEsModule\n ? createRequire(import.meta.url)\n : require;\n\n/**\n * Require a module from the linted project first, falling back to this\n * package's own resolution.\n *\n * Preferring the project copy keeps the analyser in step with the Intlayer\n * version the project actually builds with; the fallback covers strict\n * `node_modules` layouts (pnpm) where the plugin's own dependencies are not\n * hoisted into the project tree.\n *\n * @param moduleName - Specifier to resolve.\n * @param baseDir - Directory the project resolution starts from.\n */\nconst requireFromProject = <ModuleExports>(\n moduleName: string,\n baseDir: string\n): ModuleExports | null => {\n try {\n return createRequire(join(baseDir, 'noop.js'))(moduleName) as ModuleExports;\n } catch {\n // Not resolvable from the project — try this package's own dependencies.\n }\n\n try {\n return packageRequire(moduleName) as ModuleExports;\n } catch {\n return null;\n }\n};\n\n/** The subset of `@intlayer/lsp/utils` this module drives. */\ntype LspAnalyzer = {\n collectMessageUsages: (text: string) => MessageUsage[];\n collectCallerBindings: (text: string) => CallerVariableBinding[];\n getCallerNamesAlternation: () => string;\n};\n\n/** The subset of `@intlayer/config/node` this module drives. */\ntype ConfigLoader = {\n getConfiguration: (options: { baseDir: string }) => IntlayerProjectConfig;\n};\n\n/** The subset of `fast-glob` this module drives. */\ntype Glob = {\n sync: (\n patterns: string[],\n options: {\n cwd: string;\n ignore: string[];\n absolute: boolean;\n dot: boolean;\n followSymbolicLinks: boolean;\n }\n ) => string[];\n};\n\n/**\n * The configuration fields the scan reads.\n *\n * Declared structurally rather than imported from `@intlayer/types` so the\n * plugin keeps its single runtime dependency, and so the exact surface this\n * rule depends on stays visible in one place.\n */\nexport type IntlayerProjectConfig = {\n content: { fileExtensions?: string[]; codeDir?: string[] };\n build: { traversePattern?: string[] };\n compiler?: { transformPattern?: string | string[] };\n system: { baseDir: string; unmergedDictionariesDir: string };\n};\n\n// ── Public shapes ────────────────────────────────────────────────────────────\n\n/** How one dictionary key is consumed across the project. */\nexport type DictionaryUsage = {\n /**\n * Dotted content-field paths read somewhere, every parent prefix included —\n * reading `hero.title` marks `hero` used too.\n */\n usedFieldPaths: Set<string>;\n /**\n * True when at least one reference consumes the dictionary in a way field\n * tracking cannot follow: the content object escapes as a whole, a translator\n * function is bound, or the referencing file could not be parsed. No field of\n * the dictionary may be reported unused once this is set.\n */\n tracksEveryField: boolean;\n};\n\n/** The result of one project scan. */\nexport type ProjectUsageIndex = {\n /** Every dictionary key referenced by the project, and how. */\n byDictionaryKey: Map<string, DictionaryUsage>;\n /**\n * Extension-less absolute paths of modules imported by the scanned files. A\n * content declaration reached through a direct import (`useDictionary(myDict)`)\n * carries no key at the call site, so importing it at all counts as a use.\n */\n importedModulePaths: Set<string>;\n /**\n * False when the project could not be scanned — no config, no analyser, or no\n * matching source file. Every report is suppressed in that case rather than\n * flagging a whole project as unused.\n */\n isAvailable: boolean;\n};\n\n/** One other place a dictionary key is declared. */\nexport type DuplicateDeclaration = {\n /** Path as recorded in the unmerged dictionary, or `'remote'` for CMS entries. */\n filePath: string;\n /** Whether the other declaration lives in the project or in the CMS. */\n location: 'local' | 'remote';\n};\n\n// ── File extensions ──────────────────────────────────────────────────────────\n\n/**\n * Extensions the `@intlayer/lsp` analyser parses directly.\n *\n * Single-file components (`.vue`, `.svelte`, `.astro`) hold their script in a\n * markup wrapper the parser rejects, so they take the conservative path below\n * instead of being silently read as \"uses nothing\".\n */\nconst ANALYZABLE_EXTENSIONS = new Set([\n '.ts',\n '.tsx',\n '.mts',\n '.cts',\n '.js',\n '.jsx',\n '.mjs',\n '.cjs',\n]);\n\n/** Extensions stripped when comparing an import specifier to a file path. */\nconst MODULE_EXTENSIONS = [\n '.ts',\n '.tsx',\n '.mts',\n '.cts',\n '.js',\n '.jsx',\n '.mjs',\n '.cjs',\n '.vue',\n '.svelte',\n '.astro',\n];\n\n/**\n * Drop the module extension from a path so `./home.content` and\n * `/project/src/home.content.ts` compare equal.\n *\n * @param filePath - Absolute or relative path.\n */\nexport const stripModuleExtension = (filePath: string): string => {\n const extension = extname(filePath);\n\n return MODULE_EXTENSIONS.includes(extension)\n ? filePath.slice(0, -extension.length)\n : filePath;\n};\n\n// ── Regular expressions ──────────────────────────────────────────────────────\n\n/**\n * Relative import specifiers, covering the static form, dynamic `import()` and\n * `require()`. Bare specifiers are ignored: a content declaration is always\n * reached through a relative path or an alias, and alias resolution would need\n * the project's TypeScript configuration.\n */\nconst RELATIVE_IMPORT_REGEX =\n /(?:from|import|require)\\s*\\(?\\s*['\"](\\.{1,2}\\/[^'\"]+)['\"]/g;\n\n/**\n * `nest(\"other-dictionary\")` inside a content declaration.\n *\n * Nesting is the one way a dictionary is consumed from another *content* file,\n * and content files are excluded from the usage scan — without this the nested\n * dictionary would look unreferenced.\n */\nconst NEST_CALL_REGEX = /\\bnest\\s*(?:<[^<>()]*>)?\\s*\\(\\s*['\"`]([^'\"`]+)['\"`]/g;\n\n/**\n * Dictionary keys passed to a caller, for the conservative fallback.\n *\n * Built from the shared caller registry so it recognises the compat adapters\n * too, and matched against files the precise analyser could not read.\n *\n * @param callerNamesAlternation - `useIntlayer|useTranslations|…`.\n */\nconst buildFallbackKeyRegex = (callerNamesAlternation: string): RegExp =>\n new RegExp(\n `\\\\b(?:${callerNamesAlternation})\\\\b\\\\s*(?:<[^<>()]*>)?\\\\s*\\\\(\\\\s*(?:['\"\\`]([^'\"\\`]+)['\"\\`]|\\\\{[^{}]*?(?:id|namespace)\\\\s*:\\\\s*['\"\\`]([^'\"\\`]+)['\"\\`])`,\n 'g'\n );\n\n/**\n * Cheap pre-filter: does this file mention any caller at all?\n *\n * @param callerNamesAlternation - `useIntlayer|useTranslations|…`.\n */\nconst buildCallerMentionRegex = (callerNamesAlternation: string): RegExp =>\n new RegExp(`\\\\b(?:${callerNamesAlternation})\\\\b`);\n\n// ── Caches ───────────────────────────────────────────────────────────────────\n\ntype CacheEntry<Value> = { builtAt: number; value: Value };\n\nconst configurationCache = new Map<\n string,\n CacheEntry<IntlayerProjectConfig | null>\n>();\nconst usageIndexCache = new Map<string, CacheEntry<ProjectUsageIndex>>();\n\n/** An index that reports nothing, used whenever the scan cannot run. */\nconst UNAVAILABLE_INDEX: ProjectUsageIndex = {\n byDictionaryKey: new Map(),\n importedModulePaths: new Set(),\n isAvailable: false,\n};\n\n/**\n * Read a cache entry that has not outlived `cacheTtlMs`.\n *\n * @param cache - The cache to read.\n * @param cacheKey - Entry key.\n * @param cacheTtlMs - Lifetime in milliseconds; `0` disables caching entirely.\n */\nconst readCache = <Value>(\n cache: Map<string, CacheEntry<Value>>,\n cacheKey: string,\n cacheTtlMs: number\n): Value | undefined => {\n if (cacheTtlMs <= 0) return undefined;\n\n const entry = cache.get(cacheKey);\n\n if (!entry) return undefined;\n\n if (Date.now() - entry.builtAt > cacheTtlMs) {\n cache.delete(cacheKey);\n\n return undefined;\n }\n\n return entry.value;\n};\n\n/** Drop every cached configuration and usage index. Exposed for tests. */\nexport const clearProjectUsageCache = (): void => {\n configurationCache.clear();\n usageIndexCache.clear();\n};\n\n// ── Configuration ────────────────────────────────────────────────────────────\n\n/**\n * Load the Intlayer configuration for a project, or `null` when it cannot be\n * read — a project without Intlayer installed must not break the lint run.\n *\n * @param baseDir - Project root, normally ESLint's working directory.\n * @param cacheTtlMs - Lifetime of the cached configuration.\n */\nexport const getProjectConfiguration = (\n baseDir: string,\n cacheTtlMs: number\n): IntlayerProjectConfig | null => {\n const cached = readCache(configurationCache, baseDir, cacheTtlMs);\n\n if (cached !== undefined) return cached;\n\n const configLoader = requireFromProject<ConfigLoader>(\n '@intlayer/config/node',\n baseDir\n );\n\n let configuration: IntlayerProjectConfig | null = null;\n\n try {\n configuration = configLoader?.getConfiguration({ baseDir }) ?? null;\n } catch {\n configuration = null;\n }\n\n configurationCache.set(baseDir, {\n builtAt: Date.now(),\n value: configuration,\n });\n\n return configuration;\n};\n\n// ── Scanning ─────────────────────────────────────────────────────────────────\n\n/** The glob patterns and roots one project scan walks. */\ntype ScanTargets = {\n roots: string[];\n includePatterns: string[];\n excludePatterns: string[];\n contentFilePatterns: string[];\n};\n\n/**\n * Translate the configuration into the globs the scan runs, mirroring how the\n * build pipeline decides which files are project source.\n *\n * Content declarations are excluded from the source scan — a dictionary must\n * not count as its own consumer — and globbed separately for `nest()` calls.\n *\n * @param configuration - The project configuration.\n */\nconst getScanTargets = (configuration: IntlayerProjectConfig): ScanTargets => {\n const { transformPattern } = configuration.compiler ?? {};\n\n const compilerPatterns = transformPattern\n ? Array.isArray(transformPattern)\n ? transformPattern\n : [transformPattern]\n : [];\n\n const allPatterns = [\n ...(configuration.build.traversePattern ?? []),\n ...compilerPatterns,\n ];\n\n const contentFilePatterns = (configuration.content.fileExtensions ?? []).map(\n (extension) => `**/*${extension}`\n );\n\n const roots = [\n configuration.system.baseDir,\n ...(configuration.content.codeDir ?? []),\n ].map((root) => resolve(root));\n\n return {\n roots: [...new Set(roots)],\n includePatterns: allPatterns.filter((pattern) => !pattern.startsWith('!')),\n excludePatterns: [\n ...allPatterns\n .filter((pattern) => pattern.startsWith('!'))\n .map((pattern) => pattern.slice(1)),\n ...contentFilePatterns,\n ],\n contentFilePatterns,\n };\n};\n\n/**\n * List the files matching `patterns` under every root, de-duplicated.\n *\n * @param glob - The lazily required `fast-glob`.\n * @param targets - Roots to walk.\n * @param patterns - Patterns to include.\n * @param excludePatterns - Patterns to skip.\n */\nconst listFiles = (\n glob: Glob,\n targets: ScanTargets,\n patterns: string[],\n excludePatterns: string[]\n): string[] => {\n if (patterns.length === 0) return [];\n\n const files = new Set<string>();\n\n for (const root of targets.roots) {\n if (!existsSync(root)) continue;\n\n try {\n for (const file of glob.sync(patterns, {\n cwd: root,\n ignore: excludePatterns,\n absolute: true,\n dot: false,\n followSymbolicLinks: false,\n })) {\n files.add(file);\n }\n } catch {\n // An unreadable root contributes nothing; the rest of the scan stands.\n }\n }\n\n return [...files];\n};\n\n/**\n * The usage record for `dictionaryKey`, created on first sight.\n *\n * @param index - Index being built.\n * @param dictionaryKey - Key to look up.\n */\nconst getOrCreateUsage = (\n index: ProjectUsageIndex,\n dictionaryKey: string\n): DictionaryUsage => {\n const existing = index.byDictionaryKey.get(dictionaryKey);\n\n if (existing) return existing;\n\n const created: DictionaryUsage = {\n usedFieldPaths: new Set(),\n tracksEveryField: false,\n };\n\n index.byDictionaryKey.set(dictionaryKey, created);\n\n return created;\n};\n\n/**\n * Record a read of `fieldPath`, marking every parent prefix used as well:\n * reading `hero.title` means the `hero` object is read too.\n *\n * @param usage - The record to update.\n * @param fieldPath - Field path segments from the content root.\n */\nconst addUsedFieldPath = (\n usage: DictionaryUsage,\n fieldPath: string[]\n): void => {\n for (let depth = 1; depth <= fieldPath.length; depth++) {\n usage.usedFieldPaths.add(fieldPath.slice(0, depth).join('.'));\n }\n};\n\n/**\n * Fold one analysed file into the index.\n *\n * Mirrors the VS Code decoration's interpretation of the analyser output: a\n * bare content reference or a bound translator function means any field may be\n * read, and a dictionary referenced without any trackable binding proves only\n * that the dictionary itself is alive.\n *\n * @param index - Index being built.\n * @param usages - Message usages found in the file.\n * @param bindings - Caller bindings found in the file.\n */\nconst foldFileAnalysis = (\n index: ProjectUsageIndex,\n usages: MessageUsage[],\n bindings: CallerVariableBinding[]\n): void => {\n const keysWithFieldUsage = new Set<string>();\n\n for (const usage of usages) {\n const record = getOrCreateUsage(index, usage.dictionaryKey);\n\n if (usage.kind === 'namespace') continue;\n\n if (usage.fieldPath.length === 0) {\n // The content object itself is consumed — any field may be read from it.\n record.tracksEveryField = true;\n continue;\n }\n\n keysWithFieldUsage.add(usage.dictionaryKey);\n addUsedFieldPath(record, usage.fieldPath);\n }\n\n const keysWithBinding = new Set<string>();\n\n for (const binding of bindings) {\n const record = getOrCreateUsage(index, binding.dictionaryKey);\n\n keysWithBinding.add(binding.dictionaryKey);\n\n // A translator function may be forwarded, called with a computed key, or\n // used inside a template region the parser never saw.\n if (binding.bindingKind === 'translator') {\n record.tracksEveryField = true;\n continue;\n }\n\n // A content binding whose fields were never seen being read means the reads\n // happen somewhere this analysis cannot follow.\n if (!keysWithFieldUsage.has(binding.dictionaryKey)) {\n record.tracksEveryField = true;\n }\n }\n\n // A dictionary this file names but neither binds nor reads a field from — a\n // bare `useIntlayer('home')` statement — proves existence only. The test is\n // deliberately per file: what another file did with the same key says nothing\n // about what this one does with it.\n for (const usage of usages) {\n if (usage.kind !== 'namespace') continue;\n\n if (keysWithFieldUsage.has(usage.dictionaryKey)) continue;\n\n if (keysWithBinding.has(usage.dictionaryKey)) continue;\n\n getOrCreateUsage(index, usage.dictionaryKey).tracksEveryField = true;\n }\n};\n\n/**\n * Fold a file the precise analyser could not read — a single-file component, or\n * anything that failed to parse — by marking every dictionary key it mentions\n * as fully used.\n *\n * @param index - Index being built.\n * @param text - File contents.\n * @param fallbackKeyRegex - Key-extraction pattern from the caller registry.\n */\nconst foldFallbackAnalysis = (\n index: ProjectUsageIndex,\n text: string,\n fallbackKeyRegex: RegExp\n): void => {\n fallbackKeyRegex.lastIndex = 0;\n\n let match = fallbackKeyRegex.exec(text);\n\n while (match) {\n const dictionaryKey = match[1] ?? match[2];\n\n if (dictionaryKey) {\n // `formatMessage({ id: 'home.title' })` addresses a field through the key.\n const [rootKey] = dictionaryKey.split('.');\n\n getOrCreateUsage(index, rootKey ?? dictionaryKey).tracksEveryField = true;\n }\n\n match = fallbackKeyRegex.exec(text);\n }\n};\n\n/**\n * Record every module a file imports through a relative specifier.\n *\n * @param index - Index being built.\n * @param filePath - Absolute path of the importing file.\n * @param text - File contents.\n */\nconst foldImports = (\n index: ProjectUsageIndex,\n filePath: string,\n text: string\n): void => {\n RELATIVE_IMPORT_REGEX.lastIndex = 0;\n\n let match = RELATIVE_IMPORT_REGEX.exec(text);\n\n while (match) {\n const specifier = match[1];\n\n if (specifier) {\n const resolved = resolve(dirname(filePath), specifier);\n\n index.importedModulePaths.add(stripModuleExtension(resolved));\n // `import './home'` may resolve to `./home/index.ts`.\n index.importedModulePaths.add(join(resolved, 'index'));\n }\n\n match = RELATIVE_IMPORT_REGEX.exec(text);\n }\n};\n\n/**\n * Mark dictionaries pulled in by `nest()` from another content declaration.\n *\n * @param index - Index being built.\n * @param text - Content declaration source.\n */\nconst foldNestedDictionaries = (\n index: ProjectUsageIndex,\n text: string\n): void => {\n NEST_CALL_REGEX.lastIndex = 0;\n\n let match = NEST_CALL_REGEX.exec(text);\n\n while (match) {\n const dictionaryKey = match[1];\n\n if (dictionaryKey) {\n // `nest('home', 'hero.title')` still keeps the whole subtree reachable.\n getOrCreateUsage(index, dictionaryKey).tracksEveryField = true;\n }\n\n match = NEST_CALL_REGEX.exec(text);\n }\n};\n\n/**\n * Scan the project once and aggregate every dictionary reference it contains.\n *\n * @param baseDir - Project root, normally ESLint's working directory.\n * @param cacheTtlMs - Lifetime of the cached index.\n */\nexport const getProjectUsageIndex = (\n baseDir: string,\n cacheTtlMs: number\n): ProjectUsageIndex => {\n const cached = readCache(usageIndexCache, baseDir, cacheTtlMs);\n\n if (cached) return cached;\n\n const index = buildProjectUsageIndex(baseDir, cacheTtlMs);\n\n usageIndexCache.set(baseDir, { builtAt: Date.now(), value: index });\n\n return index;\n};\n\n/**\n * The uncached body of {@link getProjectUsageIndex}.\n *\n * @param baseDir - Project root.\n * @param cacheTtlMs - Lifetime of the cached configuration.\n */\nconst buildProjectUsageIndex = (\n baseDir: string,\n cacheTtlMs: number\n): ProjectUsageIndex => {\n const configuration = getProjectConfiguration(baseDir, cacheTtlMs);\n\n if (!configuration) return UNAVAILABLE_INDEX;\n\n const analyzer = requireFromProject<LspAnalyzer>(\n '@intlayer/lsp/utils',\n baseDir\n );\n // `fast-glob` is CJS, but an interop wrapper may still hand back a namespace\n // object whose callable module sits on `default`.\n const globModule = requireFromProject<Glob & { default?: Glob }>(\n 'fast-glob',\n baseDir\n );\n const glob = globModule?.sync ? globModule : globModule?.default;\n\n if (!analyzer || !glob) return UNAVAILABLE_INDEX;\n\n const targets = getScanTargets(configuration);\n const sourceFiles = listFiles(\n glob,\n targets,\n targets.includePatterns,\n targets.excludePatterns\n );\n\n if (sourceFiles.length === 0) return UNAVAILABLE_INDEX;\n\n const callerNamesAlternation = analyzer.getCallerNamesAlternation();\n const callerMentionRegex = buildCallerMentionRegex(callerNamesAlternation);\n const fallbackKeyRegex = buildFallbackKeyRegex(callerNamesAlternation);\n\n const index: ProjectUsageIndex = {\n byDictionaryKey: new Map(),\n importedModulePaths: new Set(),\n isAvailable: true,\n };\n\n for (const filePath of sourceFiles) {\n let text: string;\n\n try {\n text = readFileSync(filePath, 'utf-8');\n } catch {\n continue; // Unreadable file — nothing to learn from it.\n }\n\n foldImports(index, filePath, text);\n\n if (!callerMentionRegex.test(text)) continue;\n\n if (!ANALYZABLE_EXTENSIONS.has(extname(filePath))) {\n foldFallbackAnalysis(index, text, fallbackKeyRegex);\n continue;\n }\n\n const usages = analyzer.collectMessageUsages(text);\n\n if (usages.length === 0) {\n // The file names a caller but the parser found nothing: either the parse\n // failed or the call lives in a region the parser skipped. Stay safe.\n foldFallbackAnalysis(index, text, fallbackKeyRegex);\n continue;\n }\n\n foldFileAnalysis(index, usages, analyzer.collectCallerBindings(text));\n }\n\n for (const filePath of listFiles(\n glob,\n targets,\n targets.contentFilePatterns,\n []\n )) {\n try {\n foldNestedDictionaries(index, readFileSync(filePath, 'utf-8'));\n } catch {\n // Unreadable content declaration — nothing to learn from it.\n }\n }\n\n return index;\n};\n\n// ── Duplicate declarations ───────────────────────────────────────────────────\n\n/** One entry of an unmerged dictionary file, reduced to the fields read here. */\ntype UnmergedDictionaryEntry = {\n filePath?: string;\n location?: string;\n};\n\n/**\n * Other declarations of the same dictionary key, read from the unmerged\n * dictionaries the build writes to disk.\n *\n * Returns an empty list when the build output is missing — the rule then simply\n * has nothing to say about duplicates.\n *\n * @param configuration - The project configuration.\n * @param dictionaryKey - Key declared by the linted file.\n * @param currentFilePath - Absolute path of the linted file.\n */\nexport const getDuplicateDeclarations = (\n configuration: IntlayerProjectConfig,\n dictionaryKey: string,\n currentFilePath: string\n): DuplicateDeclaration[] => {\n const dictionaryPath = join(\n configuration.system.unmergedDictionariesDir,\n `${dictionaryKey}.json`\n );\n\n if (!existsSync(dictionaryPath)) return [];\n\n let entries: UnmergedDictionaryEntry[];\n\n try {\n const parsed: unknown = JSON.parse(readFileSync(dictionaryPath, 'utf-8'));\n\n if (!Array.isArray(parsed)) return [];\n\n entries = parsed as UnmergedDictionaryEntry[];\n } catch {\n return [];\n }\n\n const duplicates: DuplicateDeclaration[] = [];\n\n for (const entry of entries) {\n if (entry.location === 'remote') {\n duplicates.push({ filePath: 'remote', location: 'remote' });\n continue;\n }\n\n if (!entry.filePath) continue;\n\n if (\n resolve(configuration.system.baseDir, entry.filePath) === currentFilePath\n ) {\n continue;\n }\n\n duplicates.push({ filePath: entry.filePath, location: 'local' });\n }\n\n return duplicates;\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region src/utils/reactVersion.ts
|
|
5
|
+
/**
|
|
6
|
+
* Detect the installed React version for ESLint configuration.
|
|
7
|
+
*
|
|
8
|
+
* `eslint-plugin-react` versions <= 7.37.5 rely on `context.getFilename()` to detect
|
|
9
|
+
* the React version when `settings.react.version` is set to `"detect"`.
|
|
10
|
+
* Because ESLint 10 removed `context.getFilename()`, every `react/*` rule throws on load
|
|
11
|
+
* when `settings.react.version` is `"detect"`.
|
|
12
|
+
*
|
|
13
|
+
* Specifying an explicit detected React version in `settings.react.version`
|
|
14
|
+
* bypasses the broken `detectReactVersion` codepath in `eslint-plugin-react`, allowing
|
|
15
|
+
* Next.js and React projects using `eslint-config-next` / `eslint-plugin-react` to work
|
|
16
|
+
* seamlessly under both ESLint 9 and ESLint 10 without needing manual configuration hacks.
|
|
17
|
+
*/
|
|
18
|
+
const getReactVersion = (dir = process.cwd()) => {
|
|
19
|
+
try {
|
|
20
|
+
let currentDir = dir;
|
|
21
|
+
while (currentDir) {
|
|
22
|
+
const reactPkgPath = path.join(currentDir, "node_modules", "react", "package.json");
|
|
23
|
+
if (fs.existsSync(reactPkgPath)) {
|
|
24
|
+
const pkg = JSON.parse(fs.readFileSync(reactPkgPath, "utf8"));
|
|
25
|
+
if (pkg.version) return pkg.version;
|
|
26
|
+
}
|
|
27
|
+
const pkgJsonPath = path.join(currentDir, "package.json");
|
|
28
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
29
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
30
|
+
const reactDep = pkg.dependencies?.react || pkg.devDependencies?.react || pkg.peerDependencies?.react;
|
|
31
|
+
if (typeof reactDep === "string") {
|
|
32
|
+
const match = reactDep.match(/\d+(\.\d+)*/);
|
|
33
|
+
if (match) return match[0];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const parentDir = path.dirname(currentDir);
|
|
37
|
+
if (parentDir === currentDir) break;
|
|
38
|
+
currentDir = parentDir;
|
|
39
|
+
}
|
|
40
|
+
} catch {}
|
|
41
|
+
return "19.0";
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
export { getReactVersion };
|
|
46
|
+
//# sourceMappingURL=reactVersion.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reactVersion.mjs","names":[],"sources":["../../../src/utils/reactVersion.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\n/**\n * Detect the installed React version for ESLint configuration.\n *\n * `eslint-plugin-react` versions <= 7.37.5 rely on `context.getFilename()` to detect\n * the React version when `settings.react.version` is set to `\"detect\"`.\n * Because ESLint 10 removed `context.getFilename()`, every `react/*` rule throws on load\n * when `settings.react.version` is `\"detect\"`.\n *\n * Specifying an explicit detected React version in `settings.react.version`\n * bypasses the broken `detectReactVersion` codepath in `eslint-plugin-react`, allowing\n * Next.js and React projects using `eslint-config-next` / `eslint-plugin-react` to work\n * seamlessly under both ESLint 9 and ESLint 10 without needing manual configuration hacks.\n */\nexport const getReactVersion = (dir = process.cwd()): string => {\n try {\n let currentDir = dir;\n while (currentDir) {\n const reactPkgPath = path.join(\n currentDir,\n 'node_modules',\n 'react',\n 'package.json'\n );\n if (fs.existsSync(reactPkgPath)) {\n const pkg = JSON.parse(fs.readFileSync(reactPkgPath, 'utf8'));\n if (pkg.version) {\n return pkg.version;\n }\n }\n\n const pkgJsonPath = path.join(currentDir, 'package.json');\n if (fs.existsSync(pkgJsonPath)) {\n const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));\n const reactDep =\n pkg.dependencies?.react ||\n pkg.devDependencies?.react ||\n pkg.peerDependencies?.react;\n if (typeof reactDep === 'string') {\n const match = reactDep.match(/\\d+(\\.\\d+)*/);\n if (match) {\n return match[0];\n }\n }\n }\n\n const parentDir = path.dirname(currentDir);\n if (parentDir === currentDir) break;\n currentDir = parentDir;\n }\n } catch {}\n\n return '19.0';\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,MAAa,mBAAmB,MAAM,QAAQ,IAAI,MAAc;CAC9D,IAAI;EACF,IAAI,aAAa;EACjB,OAAO,YAAY;GACjB,MAAM,eAAe,KAAK,KACxB,YACA,gBACA,SACA,cACF;GACA,IAAI,GAAG,WAAW,YAAY,GAAG;IAC/B,MAAM,MAAM,KAAK,MAAM,GAAG,aAAa,cAAc,MAAM,CAAC;IAC5D,IAAI,IAAI,SACN,OAAO,IAAI;GAEf;GAEA,MAAM,cAAc,KAAK,KAAK,YAAY,cAAc;GACxD,IAAI,GAAG,WAAW,WAAW,GAAG;IAC9B,MAAM,MAAM,KAAK,MAAM,GAAG,aAAa,aAAa,MAAM,CAAC;IAC3D,MAAM,WACJ,IAAI,cAAc,SAClB,IAAI,iBAAiB,SACrB,IAAI,kBAAkB;IACxB,IAAI,OAAO,aAAa,UAAU;KAChC,MAAM,QAAQ,SAAS,MAAM,aAAa;KAC1C,IAAI,OACF,OAAO,MAAM;IAEjB;GACF;GAEA,MAAM,YAAY,KAAK,QAAQ,UAAU;GACzC,IAAI,cAAc,YAAY;GAC9B,aAAa;EACf;CACF,QAAQ,CAAC;CAET,OAAO;AACT"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { IntlayerRule } from "./utils/createRule.js";
|
|
2
|
+
import { GENERATED_OUTPUT_GLOBS, PLUGIN_NAMESPACE, PLUGIN_PACKAGE_NAME, RECOMMENDED_RULES, getRecommendedRules } from "./preset.js";
|
|
2
3
|
import { NoRawTextOptions } from "./rules/noRawText.js";
|
|
4
|
+
import { NoUnusedContentOptions } from "./rules/noUnusedContent.js";
|
|
5
|
+
import { getReactVersion } from "./utils/reactVersion.js";
|
|
3
6
|
import "./index.js";
|
|
4
7
|
import { ESLint, Linter } from "eslint";
|
|
5
8
|
//#region src/index.d.ts
|
|
6
9
|
/**
|
|
7
10
|
* Every rule this plugin ships, keyed by the name used in configuration.
|
|
8
11
|
*
|
|
9
|
-
* The rules split into
|
|
12
|
+
* The rules split into three families:
|
|
10
13
|
* - **Coverage** (`no-raw-text`) — user-facing copy that never reached a
|
|
11
14
|
* dictionary. Nothing else in the toolchain reports it.
|
|
12
15
|
* - **Compiler contract** (`static-dictionary-key`, `no-dynamic-field-access`)
|
|
13
16
|
* — call shapes that type-check and run, but that the Babel/SWC passes cannot
|
|
14
17
|
* analyse, so they silently lose the optimize pass or read a purged field.
|
|
18
|
+
* - **Dead content** (`no-unused-content`) — dictionaries and fields nothing
|
|
19
|
+
* reads. Off in every preset: it is the only rule that scans the whole
|
|
20
|
+
* project from disk, so enabling it is a cost the user should opt into.
|
|
15
21
|
*
|
|
16
22
|
* Deliberately absent: unknown dictionary keys, unknown field paths and missing
|
|
17
23
|
* required locales. The generated `__DictionaryRegistry` module augmentation
|
|
@@ -23,6 +29,7 @@ declare const rules: {
|
|
|
23
29
|
'static-dictionary-key': IntlayerRule<[], "dynamicKey" | "dynamicTemplateKey" | "dynamicJsxKey">;
|
|
24
30
|
'no-dynamic-field-access': IntlayerRule<[], "dynamicField" | "dynamicMessageId">;
|
|
25
31
|
'enforce-adapter-import': IntlayerRule<[], "useAdapterImport">;
|
|
32
|
+
'no-unused-content': IntlayerRule<[NoUnusedContentOptions?], "unusedDictionary" | "unusedField" | "duplicateDictionary">;
|
|
26
33
|
};
|
|
27
34
|
/** Rule names this plugin exposes. */
|
|
28
35
|
type IntlayerRuleName = keyof typeof rules;
|
|
@@ -38,7 +45,7 @@ type IntlayerConfigName = 'recommended' | 'strict' | 'contract-only';
|
|
|
38
45
|
* ```js
|
|
39
46
|
* import intlayer from 'eslint-plugin-intlayer';
|
|
40
47
|
*
|
|
41
|
-
* export default [intlayer.configs.recommended]
|
|
48
|
+
* export default [...intlayer]; // or [...intlayer.configs.recommended]
|
|
42
49
|
* ```
|
|
43
50
|
*
|
|
44
51
|
* - `recommended` — the compiler-contract rules as errors, plus `no-raw-text`
|
|
@@ -50,18 +57,24 @@ type IntlayerConfigName = 'recommended' | 'strict' | 'contract-only';
|
|
|
50
57
|
* `enforce-adapter-import` is opt-in: importing from the original package is
|
|
51
58
|
* perfectly valid once the bundler alias is set up, so a project that has done
|
|
52
59
|
* that should not be nagged. Only `strict` turns it on.
|
|
60
|
+
*
|
|
61
|
+
* `no-unused-content` is in no preset at all, `strict` included. It reads the
|
|
62
|
+
* Intlayer configuration and walks the project's source files from disk, which
|
|
63
|
+
* is a different order of cost from the AST-only rules and depends on build
|
|
64
|
+
* output for its duplicate report — spreading a preset should never quietly
|
|
65
|
+
* turn that on.
|
|
53
66
|
*/
|
|
54
|
-
declare const configs: Record<IntlayerConfigName, Linter.Config>;
|
|
67
|
+
declare const configs: Record<IntlayerConfigName, Linter.Config[]>;
|
|
55
68
|
/** The plugin object, as consumed by `eslint.config.js`. */
|
|
56
|
-
type IntlayerPlugin = ESLint.Plugin & {
|
|
69
|
+
type IntlayerPlugin = ESLint.Plugin & Iterable<Linter.Config> & {
|
|
57
70
|
meta: {
|
|
58
71
|
name: string;
|
|
59
72
|
version: string;
|
|
60
73
|
};
|
|
61
74
|
rules: typeof rules;
|
|
62
|
-
configs: Record<IntlayerConfigName, Linter.Config>;
|
|
75
|
+
configs: Record<IntlayerConfigName, Linter.Config[]>;
|
|
63
76
|
};
|
|
64
77
|
declare const intlayerPlugin: IntlayerPlugin;
|
|
65
78
|
//#endregion
|
|
66
|
-
export { IntlayerConfigName, IntlayerPlugin, IntlayerRuleName, type NoRawTextOptions, configs, intlayerPlugin as default, meta, rules };
|
|
79
|
+
export { GENERATED_OUTPUT_GLOBS, IntlayerConfigName, IntlayerPlugin, IntlayerRuleName, type NoRawTextOptions, type NoUnusedContentOptions, PLUGIN_NAMESPACE, PLUGIN_PACKAGE_NAME, RECOMMENDED_RULES, configs, intlayerPlugin as default, getReactVersion, getRecommendedRules, meta, rules };
|
|
67
80
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/index.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;cA+Ba;+BAAA;;;;;;;KASD,gCAAgC;cAEtC;EAAQ;EAAc;;;KAQhB;;;;;;;;;;;;;;;;;;;;;;;;;;cA4DC,SAAS,OAAO,oBAAoB,OAAO;;KA2B5C,iBAAiB,OAAO,SAClC,SAAS,OAAO;EACd;IAAQ;IAAc;;EACtB,cAAc;EACd,SAAS,OAAO,oBAAoB,OAAO;;cAGzC,gBAAgB"}
|