@ai-i18n/eslint-plugin 1.0.0-alpha.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yliu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # @ai-i18n/eslint-plugin
2
+
3
+ 用于提前报告无法被 Vite/Yuku 静态提取的 `t()` 参数。规则检查解析到
4
+ `virtual:ai-i18n` 的 `t` binding,以及 Vue/React 模式下 `useI18n()` 解构或对象成员得到的
5
+ `t`。其他库或局部同名函数不受影响。
6
+
7
+ ## 按模式配置
8
+
9
+ 显式 import 的 Vanilla 项目可以使用 `recommended`。启用 ai-i18n 按需导入时,选择与
10
+ Vite `framework` 一致的 preset;它会声明对应只读全局并启用同一条静态参数规则:
11
+
12
+ ```js
13
+ import aiI18n from '@ai-i18n/eslint-plugin';
14
+
15
+ export default [
16
+ ...aiI18n.configs.vanilla, // 或 .vue / .react
17
+ ];
18
+ ```
19
+
20
+ 这些 preset 只负责 ai-i18n 的 `t`/`useI18n`。宿主 Auto Import 插件管理的其他 API 仍由
21
+ 它自己的 ESLint 集成负责。
22
+
23
+ ## Vue SFC
24
+
25
+ `.vue` 是可选文件格式。Vue 项目应先通过 `eslint-plugin-vue` 配置
26
+ `vue-eslint-parser`,再展开 `configs.vue`:
27
+
28
+ ```js
29
+ import aiI18n from '@ai-i18n/eslint-plugin';
30
+ import pluginVue from 'eslint-plugin-vue';
31
+ import tseslint from 'typescript-eslint';
32
+
33
+ export default [
34
+ ...pluginVue.configs['flat/recommended'],
35
+ {
36
+ files: ['**/*.vue'],
37
+ languageOptions: {
38
+ parserOptions: { parser: tseslint.parser },
39
+ },
40
+ },
41
+ ...aiI18n.configs.vue,
42
+ ];
43
+ ```
44
+
45
+ `configs.vue` 复用宿主的 Vue parser,只启用同一条 `t-static-args` 规则。语义分析使用
46
+ Vue 项目已有的 `@vue/compiler-sfc`,与 Vite 提取器共享编译结果和 source map,覆盖
47
+ `<script>`、`<script setup>`、模板插值和指令表达式。两个 Vue 依赖均为可选 peer,
48
+ 不会安装到 React/Vanilla 项目。
49
+
50
+ Vue preset 同时覆盖 Vue JSX/TSX,但宿主仍需用 `@vitejs/plugin-vue-jsx` 编译这些文件。
51
+ React 项目使用 React preset;同一个 Vite build 不支持两种框架模式混用。
52
+
53
+ 规则与 Vite 共用静态参数语义,包括 `i18n.t()`、`i18n['t']()` 和省略式
54
+ `t('source', undefined)`。未绑定到 ai-i18n 的 template-only `t()` 不参与检查。
55
+
56
+ 需要解析 `tsconfig` 路径别名时,可以显式配置规则:
57
+
58
+ ```js
59
+ import aiI18n from '@ai-i18n/eslint-plugin';
60
+
61
+ export default [
62
+ {
63
+ plugins: { 'ai-i18n': aiI18n },
64
+ rules: {
65
+ 'ai-i18n/t-static-args': [
66
+ 'error',
67
+ {
68
+ tsconfigPath: './tsconfig.json',
69
+ },
70
+ ],
71
+ },
72
+ },
73
+ ];
74
+ ```
75
+
76
+ 插件不会自动修改宿主 ESLint 配置。
@@ -0,0 +1,9 @@
1
+ import { ESLint, Rule } from "eslint";
2
+ //#region src/rules/t-static-args.d.ts
3
+ declare const tStaticArgs: Rule.RuleModule;
4
+ //#endregion
5
+ //#region src/index.d.ts
6
+ declare const plugin: ESLint.Plugin;
7
+ //#endregion
8
+ export { plugin as default, tStaticArgs };
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/rules/t-static-args.ts","../src/index.ts"],"mappings":";;cASa,aAAa,KAAK;;;cCDzB,QAAQ,OAAO"}
package/dist/index.js ADDED
@@ -0,0 +1,282 @@
1
+ import { createRequire } from "node:module";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { AI_I18N_VIRTUAL_MODULE_ID, Analyzer, extractMessages, findUnboundCalls } from "@ai-i18n/analyzer";
5
+ import { analyzeVueSource } from "@ai-i18n/analyzer/vue";
6
+ //#region src/resolve-import.ts
7
+ const SOURCE_EXTENSIONS = [
8
+ "",
9
+ ".ts",
10
+ ".tsx",
11
+ ".js",
12
+ ".jsx",
13
+ ".mts",
14
+ ".cts",
15
+ ".mjs",
16
+ ".cjs"
17
+ ];
18
+ function createImportResolver(tsconfigPath) {
19
+ const aliases = [];
20
+ let baseUrl = process.cwd();
21
+ if (tsconfigPath) {
22
+ const absoluteConfig = path.resolve(tsconfigPath);
23
+ const config = readTsConfig(absoluteConfig);
24
+ baseUrl = path.resolve(path.dirname(absoluteConfig), config.compilerOptions?.baseUrl ?? ".");
25
+ for (const [pattern, targets] of Object.entries(config.compilerOptions?.paths ?? {})) {
26
+ const star = pattern.indexOf("*");
27
+ aliases.push({
28
+ prefix: star < 0 ? pattern : pattern.slice(0, star),
29
+ suffix: star < 0 ? "" : pattern.slice(star + 1),
30
+ targets
31
+ });
32
+ }
33
+ }
34
+ return (specifier, importer) => {
35
+ if (specifier === "virtual:ai-i18n") return specifier;
36
+ if (specifier.startsWith(".")) return probeSource(path.resolve(path.dirname(importer), specifier));
37
+ for (const alias of aliases) {
38
+ if (!specifier.startsWith(alias.prefix) || !specifier.endsWith(alias.suffix)) continue;
39
+ const value = specifier.slice(alias.prefix.length, specifier.length - alias.suffix.length || void 0);
40
+ for (const target of alias.targets) {
41
+ const candidate = target.includes("*") ? target.replace("*", value) : target;
42
+ const resolved = probeSource(path.resolve(baseUrl, candidate));
43
+ if (resolved) return resolved;
44
+ }
45
+ }
46
+ return null;
47
+ };
48
+ }
49
+ function probeSource(candidate) {
50
+ for (const extension of SOURCE_EXTENSIONS) {
51
+ const file = `${candidate}${extension}`;
52
+ if (isFile(file)) return path.normalize(file);
53
+ }
54
+ for (const extension of SOURCE_EXTENSIONS.slice(1)) {
55
+ const file = path.join(candidate, `index${extension}`);
56
+ if (isFile(file)) return path.normalize(file);
57
+ }
58
+ return null;
59
+ }
60
+ function isFile(file) {
61
+ try {
62
+ return fs.statSync(file).isFile();
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+ function readTsConfig(filename) {
68
+ try {
69
+ return JSON.parse(stripJsonComments(fs.readFileSync(filename, "utf8")));
70
+ } catch {
71
+ return {};
72
+ }
73
+ }
74
+ function stripJsonComments(source) {
75
+ let result = "";
76
+ let quote = "";
77
+ let escaped = false;
78
+ for (let index = 0; index < source.length; index += 1) {
79
+ const current = source[index];
80
+ const next = source[index + 1];
81
+ if (quote) {
82
+ result += current;
83
+ if (escaped) escaped = false;
84
+ else if (current === "\\") escaped = true;
85
+ else if (current === quote) quote = "";
86
+ continue;
87
+ }
88
+ if (current === "\"" || current === "'") {
89
+ quote = current;
90
+ result += current;
91
+ continue;
92
+ }
93
+ if (current === "/" && next === "/") {
94
+ while (index < source.length && source[index] !== "\n") index += 1;
95
+ result += "\n";
96
+ continue;
97
+ }
98
+ if (current === "/" && next === "*") {
99
+ index += 2;
100
+ while (index < source.length && !(source[index] === "*" && source[index + 1] === "/")) {
101
+ if (source[index] === "\n") result += "\n";
102
+ index += 1;
103
+ }
104
+ index += 1;
105
+ continue;
106
+ }
107
+ result += current;
108
+ }
109
+ return result.replace(/,\s*([}\]])/g, "$1");
110
+ }
111
+ //#endregion
112
+ //#region src/analyze.ts
113
+ function analyzeStaticArgs(code, filename, tsconfigPath, lang, autoImport = false) {
114
+ const resolve = createImportResolver(tsconfigPath);
115
+ const analyzer = new Analyzer({ resolve });
116
+ analyzer.addFile(AI_I18N_VIRTUAL_MODULE_ID, "export function t(source) { return source }");
117
+ const entryPath = normalizeFilename(filename);
118
+ const entry = analyzer.addFile(entryPath, code, lang ? { lang } : void 0);
119
+ if (!hasTranslationCandidate(entry, autoImport)) return [];
120
+ loadDependencies(analyzer, entry, resolve);
121
+ analyzer.link();
122
+ return extractMessages(entry, AI_I18N_VIRTUAL_MODULE_ID, translationHooks(autoImport), autoImport).warnings.map(({ code: warningCode, line, column, message }) => ({
123
+ code: warningCode,
124
+ line,
125
+ column,
126
+ message
127
+ }));
128
+ }
129
+ function hasTranslationCandidate(module, autoImport) {
130
+ return module.imports.some((item) => !item.typeOnly && (item.name === "t" || item.name === "useI18n")) || autoImport && findUnboundCalls(module, /* @__PURE__ */ new Set(["t", "useI18n"])).length > 0;
131
+ }
132
+ function translationHooks(autoImport) {
133
+ return [{
134
+ module: AI_I18N_VIRTUAL_MODULE_ID,
135
+ hook: "useI18n",
136
+ property: "t",
137
+ autoImport
138
+ }];
139
+ }
140
+ function loadDependencies(analyzer, entry, resolve) {
141
+ const queue = [entry];
142
+ const visited = /* @__PURE__ */ new Set();
143
+ while (queue.length) {
144
+ const module = queue.shift();
145
+ if (visited.has(module.path)) continue;
146
+ visited.add(module.path);
147
+ for (const item of module.imports) {
148
+ const resolved = resolve(item.specifier, module.path);
149
+ if (!resolved || resolved === AI_I18N_VIRTUAL_MODULE_ID) continue;
150
+ let dependency = analyzer.module(resolved);
151
+ if (!dependency) try {
152
+ dependency = analyzer.addFile(resolved, fs.readFileSync(resolved, "utf8"));
153
+ } catch {
154
+ continue;
155
+ }
156
+ queue.push(dependency);
157
+ }
158
+ }
159
+ }
160
+ function normalizeFilename(filename) {
161
+ return filename.startsWith("<") ? path.resolve("eslint-input.ts") : path.resolve(filename);
162
+ }
163
+ //#endregion
164
+ //#region src/vue-sfc.ts
165
+ const require = createRequire(import.meta.url);
166
+ function createVueAnalysisSource(source, filename, parserServices) {
167
+ if (!parserServices.getDocumentFragment?.()) throw new Error("检查 .vue 文件需要使用 vue-eslint-parser");
168
+ try {
169
+ return analyzeVueSource(source, filename, require("@vue/compiler-sfc"));
170
+ } catch (error) {
171
+ if (isMissingVueCompiler(error)) throw new Error("检查 .vue 文件需要安装 @vue/compiler-sfc");
172
+ throw error;
173
+ }
174
+ }
175
+ function isMissingVueCompiler(error) {
176
+ return error instanceof Error && "code" in error && error.code === "MODULE_NOT_FOUND";
177
+ }
178
+ //#endregion
179
+ //#region src/rules/t-static-args.ts
180
+ const tStaticArgs = {
181
+ meta: {
182
+ type: "problem",
183
+ docs: { description: "要求 virtual:ai-i18n 的 t() 参数可被静态提取" },
184
+ schema: [{
185
+ type: "object",
186
+ properties: {
187
+ tsconfigPath: { type: "string" },
188
+ autoImport: { type: "boolean" }
189
+ },
190
+ additionalProperties: false
191
+ }],
192
+ messages: {
193
+ analysisFailed: "ai-i18n 静态分析失败:{{reason}}",
194
+ dynamicArg: "t() 参数无法静态提取,请使用字符串字面量、静态模板、条件表达式或可解析的 const 字符串。"
195
+ }
196
+ },
197
+ create(context) {
198
+ const options = context.options[0] ?? {};
199
+ return { "Program:exit"(node) {
200
+ let warnings;
201
+ try {
202
+ const source = context.filename.endsWith(".vue") ? createVueAnalysisSource(context.sourceCode.text, context.filename, context.sourceCode.parserServices) : {
203
+ code: context.sourceCode.text,
204
+ lang: void 0,
205
+ mapLocation: (location) => location
206
+ };
207
+ warnings = analyzeStaticArgs(source.code, context.filename, options.tsconfigPath, source.lang, options.autoImport);
208
+ warnings = warnings.map((warning) => ({
209
+ ...warning,
210
+ ...source.mapLocation(warning)
211
+ }));
212
+ } catch (error) {
213
+ context.report({
214
+ node,
215
+ messageId: "analysisFailed",
216
+ data: { reason: error instanceof Error ? error.message : String(error) }
217
+ });
218
+ return;
219
+ }
220
+ for (const warning of warnings) {
221
+ const analysisFailed = warning.code === "parse-error";
222
+ context.report({
223
+ node,
224
+ loc: {
225
+ start: warning,
226
+ end: {
227
+ line: warning.line,
228
+ column: warning.column + 1
229
+ }
230
+ },
231
+ messageId: analysisFailed ? "analysisFailed" : "dynamicArg",
232
+ ...analysisFailed ? { data: { reason: warning.message } } : {}
233
+ });
234
+ }
235
+ } };
236
+ }
237
+ };
238
+ //#endregion
239
+ //#region src/index.ts
240
+ const { version } = createRequire(import.meta.url)("../package.json");
241
+ const plugin = {
242
+ meta: {
243
+ name: "@ai-i18n/eslint-plugin",
244
+ version,
245
+ namespace: "ai-i18n"
246
+ },
247
+ rules: { "t-static-args": tStaticArgs },
248
+ configs: {}
249
+ };
250
+ plugin.configs.recommended = [{
251
+ ignores: ["**/*.vue"],
252
+ plugins: { "ai-i18n": plugin },
253
+ rules: { "ai-i18n/t-static-args": "error" }
254
+ }];
255
+ plugin.configs.vue = [{
256
+ files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx,vue}"],
257
+ plugins: { "ai-i18n": plugin },
258
+ languageOptions: { globals: { useI18n: "readonly" } },
259
+ rules: { "ai-i18n/t-static-args": ["error", { autoImport: true }] }
260
+ }];
261
+ plugin.configs.react = [{
262
+ files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
263
+ plugins: { "ai-i18n": plugin },
264
+ languageOptions: { globals: { useI18n: "readonly" } },
265
+ rules: { "ai-i18n/t-static-args": ["error", { autoImport: true }] }
266
+ }];
267
+ plugin.configs.vanilla = [{
268
+ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
269
+ plugins: { "ai-i18n": plugin },
270
+ languageOptions: { globals: {
271
+ t: "readonly",
272
+ setLang: "readonly",
273
+ getLang: "readonly",
274
+ getLangs: "readonly",
275
+ subscribe: "readonly"
276
+ } },
277
+ rules: { "ai-i18n/t-static-args": ["error", { autoImport: true }] }
278
+ }];
279
+ //#endregion
280
+ export { plugin as default, tStaticArgs };
281
+
282
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/resolve-import.ts","../src/analyze.ts","../src/vue-sfc.ts","../src/rules/t-static-args.ts","../src/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nconst SOURCE_EXTENSIONS = [\n '',\n '.ts',\n '.tsx',\n '.js',\n '.jsx',\n '.mts',\n '.cts',\n '.mjs',\n '.cjs',\n] as const;\n\ninterface TsConfig {\n compilerOptions?: {\n baseUrl?: string;\n paths?: Record<string, string[]>;\n };\n}\n\ninterface Alias {\n prefix: string;\n suffix: string;\n targets: string[];\n}\n\nexport function createImportResolver(tsconfigPath?: string) {\n const aliases: Alias[] = [];\n let baseUrl = process.cwd();\n if (tsconfigPath) {\n const absoluteConfig = path.resolve(tsconfigPath);\n const config = readTsConfig(absoluteConfig);\n baseUrl = path.resolve(\n path.dirname(absoluteConfig),\n config.compilerOptions?.baseUrl ?? '.',\n );\n for (const [pattern, targets] of Object.entries(\n config.compilerOptions?.paths ?? {},\n )) {\n const star = pattern.indexOf('*');\n aliases.push({\n prefix: star < 0 ? pattern : pattern.slice(0, star),\n suffix: star < 0 ? '' : pattern.slice(star + 1),\n targets,\n });\n }\n }\n\n return (specifier: string, importer: string): string | null => {\n if (specifier === 'virtual:ai-i18n') return specifier;\n if (specifier.startsWith('.')) {\n return probeSource(path.resolve(path.dirname(importer), specifier));\n }\n for (const alias of aliases) {\n if (\n !specifier.startsWith(alias.prefix) ||\n !specifier.endsWith(alias.suffix)\n ) {\n continue;\n }\n const value = specifier.slice(\n alias.prefix.length,\n specifier.length - alias.suffix.length || undefined,\n );\n for (const target of alias.targets) {\n const candidate = target.includes('*')\n ? target.replace('*', value)\n : target;\n const resolved = probeSource(path.resolve(baseUrl, candidate));\n if (resolved) return resolved;\n }\n }\n return null;\n };\n}\n\nfunction probeSource(candidate: string): string | null {\n for (const extension of SOURCE_EXTENSIONS) {\n const file = `${candidate}${extension}`;\n if (isFile(file)) return path.normalize(file);\n }\n for (const extension of SOURCE_EXTENSIONS.slice(1)) {\n const file = path.join(candidate, `index${extension}`);\n if (isFile(file)) return path.normalize(file);\n }\n return null;\n}\n\nfunction isFile(file: string) {\n try {\n return fs.statSync(file).isFile();\n } catch {\n return false;\n }\n}\n\nfunction readTsConfig(filename: string): TsConfig {\n try {\n return JSON.parse(stripJsonComments(fs.readFileSync(filename, 'utf8'))) as TsConfig;\n } catch {\n return {};\n }\n}\n\nfunction stripJsonComments(source: string): string {\n let result = '';\n let quote = '';\n let escaped = false;\n for (let index = 0; index < source.length; index += 1) {\n const current = source[index]!;\n const next = source[index + 1];\n if (quote) {\n result += current;\n if (escaped) escaped = false;\n else if (current === '\\\\') escaped = true;\n else if (current === quote) quote = '';\n continue;\n }\n if (current === '\"' || current === \"'\") {\n quote = current;\n result += current;\n continue;\n }\n if (current === '/' && next === '/') {\n while (index < source.length && source[index] !== '\\n') index += 1;\n result += '\\n';\n continue;\n }\n if (current === '/' && next === '*') {\n index += 2;\n while (\n index < source.length &&\n !(source[index] === '*' && source[index + 1] === '/')\n ) {\n if (source[index] === '\\n') result += '\\n';\n index += 1;\n }\n index += 1;\n continue;\n }\n result += current;\n }\n return result.replace(/,\\s*([}\\]])/g, '$1');\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport {\n AI_I18N_VIRTUAL_MODULE_ID,\n Analyzer,\n extractMessages,\n findUnboundCalls,\n type Module,\n type ExtractWarningCode,\n type AnalysisLanguage,\n type TranslationHookBinding,\n} from '@ai-i18n/analyzer';\nimport { createImportResolver } from './resolve-import.js';\n\nexport interface StaticArgsWarning {\n code: ExtractWarningCode;\n line: number;\n column: number;\n message: string;\n}\n\nexport function analyzeStaticArgs(\n code: string,\n filename: string,\n tsconfigPath?: string,\n lang?: AnalysisLanguage,\n autoImport = false,\n): StaticArgsWarning[] {\n const resolve = createImportResolver(tsconfigPath);\n const analyzer = new Analyzer({ resolve });\n analyzer.addFile(\n AI_I18N_VIRTUAL_MODULE_ID,\n 'export function t(source) { return source }',\n );\n const entryPath = normalizeFilename(filename);\n const entry = analyzer.addFile(entryPath, code, lang ? { lang } : undefined);\n if (!hasTranslationCandidate(entry, autoImport)) return [];\n loadDependencies(analyzer, entry, resolve);\n analyzer.link();\n return extractMessages(\n entry,\n AI_I18N_VIRTUAL_MODULE_ID,\n translationHooks(autoImport),\n autoImport,\n ).warnings.map(({ code: warningCode, line, column, message }) => ({\n code: warningCode,\n line,\n column,\n message,\n }));\n}\n\nfunction hasTranslationCandidate(module: Module, autoImport: boolean): boolean {\n return (\n module.imports.some(\n (item) =>\n !item.typeOnly && (item.name === 't' || item.name === 'useI18n'),\n ) ||\n (autoImport &&\n findUnboundCalls(module, new Set(['t', 'useI18n'])).length > 0)\n );\n}\n\nfunction translationHooks(\n autoImport: boolean,\n): readonly TranslationHookBinding[] {\n return [\n {\n module: AI_I18N_VIRTUAL_MODULE_ID,\n hook: 'useI18n',\n property: 't',\n autoImport,\n },\n ];\n}\n\nfunction loadDependencies(\n analyzer: Analyzer,\n entry: Module,\n resolve: (specifier: string, importer: string) => string | null,\n) {\n const queue = [entry];\n const visited = new Set<string>();\n while (queue.length) {\n const module = queue.shift()!;\n if (visited.has(module.path)) continue;\n visited.add(module.path);\n for (const item of module.imports) {\n const resolved = resolve(item.specifier, module.path);\n if (!resolved || resolved === AI_I18N_VIRTUAL_MODULE_ID) continue;\n let dependency = analyzer.module(resolved);\n if (!dependency) {\n try {\n dependency = analyzer.addFile(\n resolved,\n fs.readFileSync(resolved, 'utf8'),\n );\n } catch {\n continue;\n }\n }\n queue.push(dependency);\n }\n }\n}\n\nfunction normalizeFilename(filename: string) {\n return filename.startsWith('<')\n ? path.resolve('eslint-input.ts')\n : path.resolve(filename);\n}\n","import { createRequire } from 'node:module';\nimport {\n analyzeVueSource,\n type VueAnalysisSource,\n type VueCompiler,\n} from '@ai-i18n/analyzer/vue';\n\ninterface VueParserServices {\n getDocumentFragment?: () => unknown;\n}\n\nconst require = createRequire(import.meta.url);\n\nexport function createVueAnalysisSource(\n source: string,\n filename: string,\n parserServices: VueParserServices,\n): VueAnalysisSource {\n if (!parserServices.getDocumentFragment?.()) {\n throw new Error('检查 .vue 文件需要使用 vue-eslint-parser');\n }\n\n try {\n // Vue 编译器仅在 configs.vue 真正处理 SFC 时加载,不影响 React/Vanilla 项目。\n const compiler = require('@vue/compiler-sfc') as VueCompiler;\n return analyzeVueSource(source, filename, compiler);\n } catch (error) {\n if (isMissingVueCompiler(error)) {\n throw new Error('检查 .vue 文件需要安装 @vue/compiler-sfc');\n }\n throw error;\n }\n}\n\nfunction isMissingVueCompiler(error: unknown): boolean {\n return (\n error instanceof Error &&\n 'code' in error &&\n error.code === 'MODULE_NOT_FOUND'\n );\n}\n","import type { Rule } from 'eslint';\nimport { analyzeStaticArgs } from '../analyze.js';\nimport { createVueAnalysisSource } from '../vue-sfc.js';\n\ninterface RuleOptions {\n tsconfigPath?: string;\n autoImport?: boolean;\n}\n\nexport const tStaticArgs: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: '要求 virtual:ai-i18n 的 t() 参数可被静态提取',\n },\n schema: [\n {\n type: 'object',\n properties: {\n tsconfigPath: { type: 'string' },\n autoImport: { type: 'boolean' },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n analysisFailed: 'ai-i18n 静态分析失败:{{reason}}',\n dynamicArg:\n 't() 参数无法静态提取,请使用字符串字面量、静态模板、条件表达式或可解析的 const 字符串。',\n },\n },\n create(context) {\n const options = (context.options[0] ?? {}) as RuleOptions;\n return {\n 'Program:exit'(node) {\n let warnings;\n try {\n const source = context.filename.endsWith('.vue')\n ? createVueAnalysisSource(\n context.sourceCode.text,\n context.filename,\n context.sourceCode.parserServices,\n )\n : {\n code: context.sourceCode.text,\n lang: undefined,\n mapLocation: (location: { line: number; column: number }) =>\n location,\n };\n warnings = analyzeStaticArgs(\n source.code,\n context.filename,\n options.tsconfigPath,\n source.lang,\n options.autoImport,\n );\n warnings = warnings.map((warning) => ({\n ...warning,\n ...source.mapLocation(warning),\n }));\n } catch (error) {\n context.report({\n node,\n messageId: 'analysisFailed',\n data: {\n reason: error instanceof Error ? error.message : String(error),\n },\n });\n return;\n }\n for (const warning of warnings) {\n const analysisFailed = warning.code === 'parse-error';\n context.report({\n node,\n loc: {\n start: warning,\n end: { line: warning.line, column: warning.column + 1 },\n },\n messageId: analysisFailed ? 'analysisFailed' : 'dynamicArg',\n ...(analysisFailed ? { data: { reason: warning.message } } : {}),\n });\n }\n },\n };\n },\n};\n","import { createRequire } from 'node:module';\nimport type { ESLint } from 'eslint';\nimport { tStaticArgs } from './rules/t-static-args.js';\n\nconst { version } = createRequire(import.meta.url)('../package.json') as {\n version: string;\n};\n\nconst plugin: ESLint.Plugin = {\n meta: {\n name: '@ai-i18n/eslint-plugin',\n version,\n namespace: 'ai-i18n',\n },\n rules: {\n 't-static-args': tStaticArgs,\n },\n configs: {},\n};\n\n// 规则集必须由使用者显式引入,不会修改宿主项目的 ESLint 配置。\nplugin.configs!.recommended = [\n {\n ignores: ['**/*.vue'],\n plugins: { 'ai-i18n': plugin },\n rules: { 'ai-i18n/t-static-args': 'error' },\n },\n];\n\nplugin.configs!.vue = [\n {\n files: ['**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx,vue}'],\n plugins: { 'ai-i18n': plugin },\n languageOptions: { globals: { useI18n: 'readonly' } },\n rules: {\n 'ai-i18n/t-static-args': ['error', { autoImport: true }],\n },\n },\n];\n\nplugin.configs!.react = [\n {\n files: ['**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],\n plugins: { 'ai-i18n': plugin },\n languageOptions: { globals: { useI18n: 'readonly' } },\n rules: {\n 'ai-i18n/t-static-args': ['error', { autoImport: true }],\n },\n },\n];\n\nplugin.configs!.vanilla = [\n {\n files: ['**/*.{js,mjs,cjs,ts,mts,cts}'],\n plugins: { 'ai-i18n': plugin },\n languageOptions: {\n globals: {\n t: 'readonly',\n setLang: 'readonly',\n getLang: 'readonly',\n getLangs: 'readonly',\n subscribe: 'readonly',\n },\n },\n rules: {\n 'ai-i18n/t-static-args': ['error', { autoImport: true }],\n },\n },\n];\n\nexport { tStaticArgs };\nexport default plugin;\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@ai-i18n/eslint-plugin",
3
+ "version": "1.0.0-alpha.0",
4
+ "description": "ESLint 9 static-argument checks aligned with ai-i18n extraction semantics.",
5
+ "keywords": [
6
+ "eslint",
7
+ "eslintplugin",
8
+ "i18n",
9
+ "translation"
10
+ ],
11
+ "license": "MIT",
12
+ "author": "yliu",
13
+ "homepage": "https://github.com/bosens-China/ai-i18n#readme",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/bosens-China/ai-i18n.git",
17
+ "directory": "packages/eslint"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/bosens-China/ai-i18n/issues"
21
+ },
22
+ "type": "module",
23
+ "sideEffects": false,
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "main": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js"
33
+ }
34
+ },
35
+ "dependencies": {
36
+ "@ai-i18n/analyzer": "^1.0.0-alpha.0"
37
+ },
38
+ "peerDependencies": {
39
+ "@vue/compiler-sfc": "^3.5.0",
40
+ "eslint": "^9.0.0",
41
+ "vue-eslint-parser": "^10.0.0"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "@vue/compiler-sfc": {
45
+ "optional": true
46
+ },
47
+ "vue-eslint-parser": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "devDependencies": {
55
+ "@vue/compiler-sfc": "^3.5.34",
56
+ "vue-eslint-parser": "^10.4.1"
57
+ },
58
+ "scripts": {
59
+ "lint": "eslint .",
60
+ "check": "tsc -p tsconfig.json --noEmit && pnpm lint",
61
+ "test": "pnpm --dir ../.. exec vitest run packages/eslint/test --config vitest.config.ts"
62
+ }
63
+ }