rhine-lint 1.0.2 → 1.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 +13 -6
- package/dist/assets/eslint.config.js +1 -6
- package/dist/cli.js +3 -1
- package/dist/config.d.ts +8 -0
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +192 -76
- package/dist/core/runner.js +97 -61
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ export default {
|
|
|
110
110
|
'react/no-unknown-property': 'off'
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
-
...
|
|
113
|
+
// ...
|
|
114
114
|
]
|
|
115
115
|
},
|
|
116
116
|
|
|
@@ -119,7 +119,7 @@ export default {
|
|
|
119
119
|
config: {
|
|
120
120
|
printWidth: 100,
|
|
121
121
|
semi: true,
|
|
122
|
-
...
|
|
122
|
+
// ...
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
} as Config;
|
|
@@ -132,6 +132,9 @@ CLI 参数优先级高于配置文件:
|
|
|
132
132
|
- `--fix`: 自动修复错误。
|
|
133
133
|
- `--config <path>`: 指定配置文件路径。
|
|
134
134
|
- `--level <level>`: 强制指定项目类型(`js`, `ts`, `frontend`, `nextjs`)。
|
|
135
|
+
- `--ignore <pattern>`: 添加忽略模式 (支持多次使用, e.g. `--ignore dist --ignore coverage`)。
|
|
136
|
+
- `--no-ignore`: 强制禁用所有忽略规则 (包括 .gitignore)。
|
|
137
|
+
- `--debug`: 打印调试信息(包括生成的配置、忽略列表等)。
|
|
135
138
|
- `--cache-dir <dir>`: 指定缓存目录(默认使用 `node_modules/.cache/rhine-lint`)。
|
|
136
139
|
|
|
137
140
|
### 缓存目录 Cache Directory
|
|
@@ -202,10 +205,14 @@ graph TD
|
|
|
202
205
|
这是项目最复杂的部分。为了实现「零配置」且不污染用户目录,我们采用 **虚拟配置 (Virtual Configuration)** 策略。
|
|
203
206
|
|
|
204
207
|
- **动态生成**: 我们不依赖用户项目里的 `.eslintrc`。相反,我们在运行时,在 `node_modules/.cache/rhine-lint/` 下生成一个真实的 `eslint.config.mjs`。
|
|
205
|
-
- **
|
|
206
|
-
-
|
|
207
|
-
-
|
|
208
|
-
-
|
|
208
|
+
- **TypeScript 配置编译 (TS Compilation)**: 如果检测到用户的配置文件是 `.ts` 格式:
|
|
209
|
+
- 会自动调用内置的 TypeScript 编译器将其转译为 `.mjs` 模块。
|
|
210
|
+
- 转译后的文件被保存在缓存目录(如 `.cache/rhine-lint/rhine-lint.user-config.mjs`)。
|
|
211
|
+
- 生成的 ESLint 配置会指向这个编译后的 JS 文件,从而解决 Node.js 原生无法加载 TS 文件的限制。
|
|
212
|
+
- **智能缓存 (Smart Caching)**: 为了提高性能(尤其是 IDE 保存自动修复时),我们实现了一套基于指纹的缓存机制:
|
|
213
|
+
- **指纹计算**: 每次运行前会计算一个 SHA-256 哈希,包含:`package.json` 版本 + CLI 参数 + 用户配置文件内容 + `.gitignore` 状态。
|
|
214
|
+
- **极速命中**: 如果指纹与缓存的 `metadata.json` 匹配,则**完全跳过**繁重的转译、合并和文件写入操作,直接复用上次的配置。
|
|
215
|
+
- **JIT 加载**: 除了上述静态编译,对于部分模块加载我们使用 `jiti` 确保兼容性。
|
|
209
216
|
- **关键点**: 这种设计使得 `rhine-lint` 内部的依赖(如 `eslint-plugin-react`)可以被正确解析,而不需要用户显式安装它们。
|
|
210
217
|
|
|
211
218
|
#### 规则资产 (`src/assets/`)
|
|
@@ -4,7 +4,6 @@ import { fileURLToPath } from 'node:url'
|
|
|
4
4
|
import nextPlugin from '@next/eslint-plugin-next'
|
|
5
5
|
import reactPlugin from 'eslint-plugin-react';
|
|
6
6
|
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
|
7
|
-
import { fixupConfigRules } from '@eslint/compat'
|
|
8
7
|
|
|
9
8
|
import css from '@eslint/css'
|
|
10
9
|
|
|
@@ -15,7 +14,6 @@ import css from '@eslint/css'
|
|
|
15
14
|
// ...
|
|
16
15
|
|
|
17
16
|
|
|
18
|
-
import { FlatCompat } from '@eslint/eslintrc'
|
|
19
17
|
import js from '@eslint/js'
|
|
20
18
|
import json from '@eslint/json'
|
|
21
19
|
import markdown from '@eslint/markdown'
|
|
@@ -29,9 +27,6 @@ import tseslint from 'typescript-eslint'
|
|
|
29
27
|
|
|
30
28
|
const __filename = fileURLToPath(import.meta.url)
|
|
31
29
|
const __dirname = path.dirname(__filename)
|
|
32
|
-
const compat = new FlatCompat({
|
|
33
|
-
baseDirectory: __dirname,
|
|
34
|
-
})
|
|
35
30
|
|
|
36
31
|
const globalConfig = defineConfig([
|
|
37
32
|
{
|
|
@@ -50,7 +45,7 @@ export default function createConfig(overrides = {}) {
|
|
|
50
45
|
const OPTIONS = {
|
|
51
46
|
ENABLE_SCRIPT: true, // Set to enable typescript javascript file features
|
|
52
47
|
ENABLE_TYPE_CHECKED: true, // Set to enable type features
|
|
53
|
-
ENABLE_PROJECT_BASE_TYPE_CHECKED:
|
|
48
|
+
ENABLE_PROJECT_BASE_TYPE_CHECKED: true, // Set to enable project-based type features
|
|
54
49
|
ENABLE_FRONTEND: true, // Set to enable JSX, React, Reacts Hooks, and other frontend features
|
|
55
50
|
ENABLE_NEXT: false, // Set to enable Next.js and other frontend features
|
|
56
51
|
ENABLE_MARKDOWN: true, // Set to enable markdown file features
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,9 @@ cli
|
|
|
14
14
|
.option("--fix", "Fix lint errors")
|
|
15
15
|
.option("--config <path>", "Path to config file")
|
|
16
16
|
.option("--level <level>", "Project level (js, ts, frontend, nextjs)")
|
|
17
|
+
.option("--no-project-type-check", "Disable project-based type checking (faster for single files)")
|
|
17
18
|
.option("--cache-dir <dir>", "Custom temporary cache directory")
|
|
19
|
+
.option("--debug", "Enable debug mode")
|
|
18
20
|
.action(async (files, options) => {
|
|
19
21
|
const cwd = process.cwd();
|
|
20
22
|
// If files is empty, default to "."
|
|
@@ -34,7 +36,7 @@ cli
|
|
|
34
36
|
}
|
|
35
37
|
console.log();
|
|
36
38
|
// 2. Generate Temp Configs
|
|
37
|
-
const temps = await generateTempConfig(cwd, userConfigResult, options.level, options.cacheDir);
|
|
39
|
+
const temps = await generateTempConfig(cwd, userConfigResult, options.level, options.cacheDir, options.debug, options.projectTypeCheck);
|
|
38
40
|
usedCachePath = temps.cachePath; // Save for cleanup
|
|
39
41
|
// 3. Run ESLint
|
|
40
42
|
const eslintResult = await runEslint(cwd, temps.eslintPath, options.fix, targetFiles);
|
package/dist/config.d.ts
CHANGED
|
@@ -6,6 +6,14 @@ export type Config = {
|
|
|
6
6
|
config?: [
|
|
7
7
|
];
|
|
8
8
|
overlay?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Enable project-based type checking with tsconfig.
|
|
11
|
+
* This enables `projectService` and `strictTypeChecked` rules.
|
|
12
|
+
* Slower but more accurate type-aware linting.
|
|
13
|
+
* Set to `false` to disable for faster single-file processing.
|
|
14
|
+
* @default true
|
|
15
|
+
*/
|
|
16
|
+
projectTypeCheck?: boolean;
|
|
9
17
|
};
|
|
10
18
|
prettier?: {
|
|
11
19
|
config?: {};
|
package/dist/core/config.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare function loadUserConfig(cwd: string): Promise<{
|
|
|
6
6
|
export declare function generateTempConfig(cwd: string, userConfigResult: {
|
|
7
7
|
config: Config;
|
|
8
8
|
path?: string;
|
|
9
|
-
}, cliLevel?: string, cliCacheDir?: string): Promise<{
|
|
9
|
+
}, cliLevel?: string, cliCacheDir?: string, debug?: boolean, cliProjectTypeCheck?: boolean): Promise<{
|
|
10
10
|
eslintPath: string;
|
|
11
11
|
prettierPath: string;
|
|
12
12
|
cachePath: string;
|
package/dist/core/config.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { createJiti } from "jiti";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import fs from "fs-extra";
|
|
3
|
-
import { logger } from "../utils/logger.js";
|
|
4
|
+
import { logger, logInfo } from "../utils/logger.js";
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
6
|
import { createRequire } from "node:module";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
6
8
|
const require = createRequire(import.meta.url);
|
|
9
|
+
const pkg = require('../../package.json');
|
|
7
10
|
function getAssetPath(filename) {
|
|
8
11
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../assets", filename);
|
|
9
12
|
}
|
|
@@ -19,80 +22,198 @@ const CONFIG_FILENAMES = [
|
|
|
19
22
|
"config/rhine-lint.config.cjs",
|
|
20
23
|
"config/rhine-lint.config.json",
|
|
21
24
|
];
|
|
25
|
+
function getCacheDir(cwd, userConfig, cliCacheDir) {
|
|
26
|
+
if (cliCacheDir)
|
|
27
|
+
return path.resolve(cwd, cliCacheDir);
|
|
28
|
+
if (userConfig?.cacheDir)
|
|
29
|
+
return path.resolve(cwd, userConfig.cacheDir);
|
|
30
|
+
const nodeModulesPath = path.join(cwd, "node_modules");
|
|
31
|
+
if (fs.existsSync(nodeModulesPath))
|
|
32
|
+
return path.join(nodeModulesPath, ".cache", "rhine-lint");
|
|
33
|
+
return path.join(cwd, ".cache", "rhine-lint");
|
|
34
|
+
}
|
|
22
35
|
export async function loadUserConfig(cwd) {
|
|
23
|
-
|
|
24
|
-
for (const
|
|
25
|
-
const
|
|
26
|
-
if (await fs.pathExists(
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
const jiti = createJiti(fileURLToPath(import.meta.url));
|
|
37
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
38
|
+
const configPath = path.join(cwd, filename);
|
|
39
|
+
if (await fs.pathExists(configPath)) {
|
|
40
|
+
try {
|
|
41
|
+
const configModule = jiti(configPath);
|
|
42
|
+
const config = configModule.default || configModule;
|
|
43
|
+
logInfo(`Using config file: ${configPath}`);
|
|
44
|
+
return { config, path: configPath };
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
logger.error(`Failed to load config file ${configPath}:`, e);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
29
50
|
}
|
|
30
51
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
logInfo("No config file found, using default configuration.");
|
|
53
|
+
return { config: {} };
|
|
54
|
+
}
|
|
55
|
+
export async function generateTempConfig(cwd, userConfigResult, cliLevel, cliCacheDir, debug, cliProjectTypeCheck) {
|
|
56
|
+
const cachePath = getCacheDir(cwd, userConfigResult.config, cliCacheDir);
|
|
57
|
+
await fs.ensureDir(cachePath);
|
|
58
|
+
const eslintTempPath = path.join(cachePath, "eslint.config.mjs");
|
|
59
|
+
const prettierTempPath = path.join(cachePath, "prettier.config.mjs");
|
|
60
|
+
const metaPath = path.join(cachePath, "metadata.json");
|
|
61
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
62
|
+
// Determine projectTypeCheck: CLI flag takes precedence over config file, default is true
|
|
63
|
+
// When --no-project-type-check is used, options.projectTypeCheck will be false
|
|
64
|
+
const projectTypeCheck = cliProjectTypeCheck ?? userConfigResult.config.eslint?.projectTypeCheck ?? true;
|
|
65
|
+
let calculatedHash = "";
|
|
34
66
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
67
|
+
const hash = createHash("sha256");
|
|
68
|
+
hash.update(pkg.version || "0.0.0");
|
|
69
|
+
hash.update(cliLevel || "default");
|
|
70
|
+
hash.update(projectTypeCheck ? "ptc-on" : "ptc-off");
|
|
71
|
+
if (userConfigResult.path && await fs.pathExists(userConfigResult.path)) {
|
|
72
|
+
const content = await fs.readFile(userConfigResult.path);
|
|
73
|
+
hash.update(content);
|
|
74
|
+
}
|
|
75
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
76
|
+
const content = await fs.readFile(gitignorePath);
|
|
77
|
+
hash.update(content);
|
|
40
78
|
}
|
|
41
79
|
else {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
80
|
+
hash.update("no-gitignore");
|
|
81
|
+
}
|
|
82
|
+
calculatedHash = hash.digest("hex");
|
|
83
|
+
if (await fs.pathExists(metaPath)) {
|
|
84
|
+
const meta = await fs.readJson(metaPath);
|
|
85
|
+
if (meta.hash === calculatedHash && await fs.pathExists(eslintTempPath) && await fs.pathExists(prettierTempPath)) {
|
|
86
|
+
logger.debug(`Cache hit! Configs reused from ${cachePath}`);
|
|
87
|
+
return { eslintPath: eslintTempPath, prettierPath: prettierTempPath, cachePath };
|
|
88
|
+
}
|
|
45
89
|
}
|
|
46
|
-
return {
|
|
47
|
-
config: loaded || {},
|
|
48
|
-
path: configPath
|
|
49
|
-
};
|
|
50
90
|
}
|
|
51
91
|
catch (e) {
|
|
52
|
-
logger.debug("
|
|
53
|
-
logger.warn(`Found config file at ${configPath} but failed to load it.`);
|
|
54
|
-
return { config: {} };
|
|
92
|
+
logger.debug("Cache check failed, regenerating...", e);
|
|
55
93
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
94
|
+
let ignoredPatterns = [];
|
|
95
|
+
// Parse .gitignore file and convert to ESLint ignore patterns
|
|
96
|
+
// ESLint ignores are relative to the cwd where ESLint runs (which is the project root)
|
|
97
|
+
// NOT relative to the config file location
|
|
98
|
+
const parseGitignore = (content) => {
|
|
99
|
+
const patterns = [];
|
|
100
|
+
const lines = content.split('\n');
|
|
101
|
+
for (let line of lines) {
|
|
102
|
+
// Remove Windows line endings and trim
|
|
103
|
+
line = line.replace(/\r$/, '').trim();
|
|
104
|
+
// Skip empty lines and comments
|
|
105
|
+
if (!line || line.startsWith('#'))
|
|
106
|
+
continue;
|
|
107
|
+
// Handle negation (un-ignore)
|
|
108
|
+
const isNegation = line.startsWith('!');
|
|
109
|
+
if (isNegation) {
|
|
110
|
+
line = line.slice(1);
|
|
111
|
+
}
|
|
112
|
+
// Determine if pattern is rooted (starts with /)
|
|
113
|
+
const isRooted = line.startsWith('/');
|
|
114
|
+
if (isRooted) {
|
|
115
|
+
line = line.slice(1);
|
|
116
|
+
}
|
|
117
|
+
// Normalize path separators
|
|
118
|
+
line = line.replace(/\\/g, '/');
|
|
119
|
+
// Handle directory patterns (ends with /)
|
|
120
|
+
const isDir = line.endsWith('/');
|
|
121
|
+
if (isDir) {
|
|
122
|
+
line = line.slice(0, -1);
|
|
123
|
+
}
|
|
124
|
+
// Build the ESLint ignore pattern
|
|
125
|
+
// Patterns are relative to the cwd where ESLint runs (project root)
|
|
126
|
+
let pattern;
|
|
127
|
+
if (isRooted) {
|
|
128
|
+
// Rooted patterns: only match at project root
|
|
129
|
+
// e.g., /node_modules -> node_modules/**
|
|
130
|
+
pattern = line;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// Non-rooted patterns: match anywhere in the tree
|
|
134
|
+
// e.g., .next -> **/.next/**
|
|
135
|
+
pattern = `**/${line}`;
|
|
136
|
+
}
|
|
137
|
+
// Add /** suffix for directories and patterns that should match all contents
|
|
138
|
+
if (isDir || !line.includes('*')) {
|
|
139
|
+
// Check if it's likely a directory (no extension or common directory names)
|
|
140
|
+
const shouldMatchContents = isDir ||
|
|
141
|
+
!path.extname(line) ||
|
|
142
|
+
line.endsWith('.next') ||
|
|
143
|
+
line.endsWith('.git') ||
|
|
144
|
+
line.endsWith('.cache');
|
|
145
|
+
if (shouldMatchContents && !pattern.endsWith('/**')) {
|
|
146
|
+
pattern += '/**';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Apply negation prefix for ESLint if this was an un-ignore pattern
|
|
150
|
+
if (isNegation) {
|
|
151
|
+
pattern = '!' + pattern;
|
|
152
|
+
}
|
|
153
|
+
patterns.push(pattern);
|
|
154
|
+
}
|
|
155
|
+
return patterns;
|
|
156
|
+
};
|
|
157
|
+
// Default directories to always ignore (relative to project root)
|
|
158
|
+
const defaultIgnores = [
|
|
159
|
+
'node_modules', 'dist', '.next', '.git', '.output', '.nuxt', 'coverage', '.cache'
|
|
160
|
+
].map(dir => `**/${dir}/**`);
|
|
161
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
162
|
+
try {
|
|
163
|
+
const gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
164
|
+
if (debug) {
|
|
165
|
+
console.log("-----------------------------------------");
|
|
166
|
+
console.log("DEBUG: .gitignore content preview:");
|
|
167
|
+
console.log(gitignoreContent.substring(0, 500));
|
|
168
|
+
console.log("-----------------------------------------");
|
|
169
|
+
}
|
|
170
|
+
const parsedPatterns = parseGitignore(gitignoreContent);
|
|
171
|
+
if (debug) {
|
|
172
|
+
console.log("-----------------------------------------");
|
|
173
|
+
console.log("DEBUG: Parsed gitignore patterns:");
|
|
174
|
+
parsedPatterns.forEach((p, i) => console.log(` [${i}] "${p}"`));
|
|
175
|
+
console.log("-----------------------------------------");
|
|
176
|
+
}
|
|
177
|
+
// Merge defaults with parsed patterns, removing duplicates
|
|
178
|
+
const allPatterns = [...defaultIgnores, ...parsedPatterns];
|
|
179
|
+
ignoredPatterns = [...new Set(allPatterns)];
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
logger.debug("Failed to parse .gitignore", e);
|
|
183
|
+
ignoredPatterns = defaultIgnores;
|
|
184
|
+
}
|
|
61
185
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return path.resolve(cwd, userConfig.cacheDir);
|
|
186
|
+
else {
|
|
187
|
+
ignoredPatterns = defaultIgnores;
|
|
65
188
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
189
|
+
if (debug) {
|
|
190
|
+
console.log("-----------------------------------------");
|
|
191
|
+
console.log("DEBUG: Final Ignores List:");
|
|
192
|
+
ignoredPatterns.forEach(p => console.log(p));
|
|
193
|
+
console.log("-----------------------------------------");
|
|
70
194
|
}
|
|
71
|
-
// 4. Fallback: .cache/rhine-lint in root
|
|
72
|
-
return path.join(cwd, ".cache", "rhine-lint");
|
|
73
|
-
}
|
|
74
|
-
export async function generateTempConfig(cwd, userConfigResult, cliLevel, cliCacheDir) {
|
|
75
|
-
const cachePath = getCacheDir(cwd, userConfigResult.config, cliCacheDir);
|
|
76
|
-
await fs.ensureDir(cachePath);
|
|
77
|
-
const eslintTempPath = path.join(cachePath, "eslint.config.mjs");
|
|
78
|
-
const prettierTempPath = path.join(cachePath, "prettier.config.mjs");
|
|
79
195
|
const defaultEslintPath = pathToFileURL(getAssetPath("eslint.config.js")).href;
|
|
80
196
|
const defaultPrettierPath = pathToFileURL(getAssetPath("prettier.config.js")).href;
|
|
81
197
|
const userConfigUrl = userConfigResult.path ? pathToFileURL(userConfigResult.path).href : null;
|
|
82
198
|
const defuUrl = pathToFileURL(require.resolve("defu")).href;
|
|
83
199
|
const jitiUrl = pathToFileURL(require.resolve("jiti")).href;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
200
|
+
let finalUserConfigUrl = userConfigUrl;
|
|
201
|
+
if (userConfigResult.path && (userConfigResult.path.endsWith('.ts') || userConfigResult.path.endsWith('.mts') || userConfigResult.path.endsWith('.cts'))) {
|
|
202
|
+
try {
|
|
203
|
+
const ts = await import('typescript');
|
|
204
|
+
const fileContent = await fs.readFile(userConfigResult.path, 'utf-8');
|
|
205
|
+
const transpiled = ts.transpileModule(fileContent, {
|
|
206
|
+
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ESNext }
|
|
207
|
+
});
|
|
208
|
+
const compiledName = 'rhine-lint.user-config.mjs';
|
|
209
|
+
const compiledPath = path.join(cachePath, compiledName);
|
|
210
|
+
await fs.writeFile(compiledPath, transpiled.outputText);
|
|
211
|
+
finalUserConfigUrl = pathToFileURL(compiledPath).href;
|
|
212
|
+
}
|
|
213
|
+
catch (e) {
|
|
214
|
+
logger.debug("Failed to transpile user config.", e);
|
|
215
|
+
}
|
|
92
216
|
}
|
|
93
|
-
const gitignorePath = path.join(cwd, ".gitignore");
|
|
94
|
-
const hasGitignore = await fs.pathExists(gitignorePath);
|
|
95
|
-
const gitignoreUrl = hasGitignore ? pathToFileURL(gitignorePath).href : null;
|
|
96
217
|
const isEslintOverlay = userConfigResult.config.eslint?.overlay ?? false;
|
|
97
218
|
const isPrettierOverlay = userConfigResult.config.prettier?.overlay ?? false;
|
|
98
219
|
const eslintContent = `
|
|
@@ -101,36 +222,35 @@ import { fileURLToPath } from "node:url";
|
|
|
101
222
|
import path from "node:path";
|
|
102
223
|
import createConfig from "${defaultEslintPath}";
|
|
103
224
|
import { defu } from "${defuUrl}";
|
|
104
|
-
${compatUrl && hasGitignore ? `import { includeIgnoreFile } from "${compatUrl}";` : ''}
|
|
105
|
-
|
|
106
225
|
const __filename = fileURLToPath(import.meta.url);
|
|
107
226
|
const __dirname = path.dirname(__filename);
|
|
108
227
|
const jiti = createJiti(__filename);
|
|
109
228
|
|
|
110
|
-
${
|
|
111
|
-
const loaded = await jiti.import("${
|
|
229
|
+
${finalUserConfigUrl ? `
|
|
230
|
+
const loaded = await jiti.import("${finalUserConfigUrl.replace('file:///', '').replace(/%20/g, ' ')}", { default: true });
|
|
112
231
|
const userOne = loaded.default || loaded;
|
|
113
232
|
` : 'const userOne = {};'}
|
|
114
233
|
|
|
115
234
|
const userEslint = userOne.eslint || {};
|
|
116
235
|
const level = "${cliLevel || ''}" || userOne.level || "frontend";
|
|
117
236
|
|
|
118
|
-
|
|
237
|
+
// Project-based type checking: CLI flag (${projectTypeCheck}) takes precedence over config file
|
|
238
|
+
const projectTypeCheck = ${projectTypeCheck} || userEslint.projectTypeCheck || false;
|
|
239
|
+
|
|
240
|
+
let overrides = { ENABLE_PROJECT_BASE_TYPE_CHECKED: projectTypeCheck };
|
|
119
241
|
switch (level) {
|
|
120
|
-
case "nextjs": overrides = { ENABLE_NEXT: true }; break;
|
|
121
|
-
case "frontend": overrides = { ENABLE_FRONTEND: true, ENABLE_NEXT: false }; break;
|
|
122
|
-
case "ts": overrides = { ENABLE_FRONTEND: false }; break;
|
|
123
|
-
case "js": overrides = { ENABLE_FRONTEND: false, ENABLE_TYPE_CHECKED: false }; break;
|
|
242
|
+
case "nextjs": overrides = { ...overrides, ENABLE_NEXT: true }; break;
|
|
243
|
+
case "frontend": overrides = { ...overrides, ENABLE_FRONTEND: true, ENABLE_NEXT: false }; break;
|
|
244
|
+
case "ts": overrides = { ...overrides, ENABLE_FRONTEND: false }; break;
|
|
245
|
+
case "js": overrides = { ...overrides, ENABLE_FRONTEND: false, ENABLE_TYPE_CHECKED: false }; break;
|
|
124
246
|
}
|
|
125
247
|
|
|
126
248
|
const defaultEslint = createConfig(overrides);
|
|
127
|
-
|
|
128
249
|
let finalConfig;
|
|
129
|
-
|
|
130
|
-
// Prepend ignore file if exists
|
|
131
250
|
const prefixConfig = [];
|
|
132
|
-
|
|
133
|
-
|
|
251
|
+
|
|
252
|
+
${ignoredPatterns.length > 0 ? `
|
|
253
|
+
prefixConfig.push({ ignores: ${JSON.stringify(ignoredPatterns)} });
|
|
134
254
|
` : ''}
|
|
135
255
|
|
|
136
256
|
if (${isEslintOverlay} || userEslint.overlay) {
|
|
@@ -138,7 +258,6 @@ if (${isEslintOverlay} || userEslint.overlay) {
|
|
|
138
258
|
finalConfig = [...prefixConfig, ...defaultEslint, ...userEslint.config];
|
|
139
259
|
} else {
|
|
140
260
|
finalConfig = defu(userEslint, defaultEslint);
|
|
141
|
-
// Note: merging object into array-based config is weird, assume flat config array concatenation for robustness
|
|
142
261
|
if (!Array.isArray(finalConfig)) finalConfig = [...prefixConfig, finalConfig];
|
|
143
262
|
}
|
|
144
263
|
} else {
|
|
@@ -175,14 +294,11 @@ export default finalConfig;
|
|
|
175
294
|
`;
|
|
176
295
|
await fs.writeFile(eslintTempPath, eslintContent);
|
|
177
296
|
await fs.writeFile(prettierTempPath, prettierContent);
|
|
297
|
+
await fs.writeJson(metaPath, { hash: calculatedHash, timestamp: Date.now() });
|
|
178
298
|
return { eslintPath: eslintTempPath, prettierPath: prettierTempPath, cachePath };
|
|
179
299
|
}
|
|
180
300
|
export async function cleanup(cachePath) {
|
|
181
301
|
if (cachePath && await fs.pathExists(cachePath)) {
|
|
182
|
-
//
|
|
183
|
-
// If it's node_modules/.cache/rhine-lint, delete it
|
|
184
|
-
// If it's custom, delete it (careful?)
|
|
185
|
-
// Generally standard to remove temp config dir.
|
|
186
|
-
await fs.remove(cachePath);
|
|
302
|
+
// await fs.remove(cachePath);
|
|
187
303
|
}
|
|
188
304
|
}
|
package/dist/core/runner.js
CHANGED
|
@@ -1,38 +1,82 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "fs-extra";
|
|
2
5
|
import { logger, logInfo, logError } from "../utils/logger.js";
|
|
3
|
-
const
|
|
4
|
-
const EXECUTOR = IS_BUN ? "bunx" : "npx";
|
|
5
|
-
// Helper to strip ANSI codes for easier regex matching
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
6
7
|
function stripAnsi(str) {
|
|
7
8
|
return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
|
8
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Robustly resolve a binary path from a package, handling exports restrictions.
|
|
12
|
+
*/
|
|
13
|
+
function resolveBin(pkgName, binPathRelative) {
|
|
14
|
+
// 1. Try strict resolve (fastest, but might be blocked by exports)
|
|
15
|
+
try {
|
|
16
|
+
return require.resolve(`${pkgName}/${binPathRelative}`);
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
if (e.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED' && e.code !== 'MODULE_NOT_FOUND') {
|
|
20
|
+
logger.debug(`Resolve error for ${pkgName}/${binPathRelative}:`, e);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// 2. Fallback: Resolve main entry point, then traverse up to find package root
|
|
24
|
+
try {
|
|
25
|
+
const mainPath = require.resolve(pkgName);
|
|
26
|
+
let currentDir = path.dirname(mainPath);
|
|
27
|
+
// Traverse up (max 5 levels) to find package.json
|
|
28
|
+
for (let i = 0; i < 5; i++) {
|
|
29
|
+
const pkgJsonPath = path.join(currentDir, "package.json");
|
|
30
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const pkg = fs.readJsonSync(pkgJsonPath);
|
|
33
|
+
if (pkg.name === pkgName) {
|
|
34
|
+
// Found it! Construct bin path
|
|
35
|
+
const binPath = path.join(currentDir, binPathRelative);
|
|
36
|
+
if (fs.existsSync(binPath)) {
|
|
37
|
+
return binPath;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Ignore parsing errors
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (currentDir === path.dirname(currentDir))
|
|
46
|
+
break; // Root reached
|
|
47
|
+
currentDir = path.dirname(currentDir);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
logger.debug(`Deep resolve failed for ${pkgName}:`, e);
|
|
52
|
+
}
|
|
53
|
+
// 3. Fallback to system PATH (bare command)
|
|
54
|
+
return pkgName;
|
|
55
|
+
}
|
|
9
56
|
export async function runCommandWithOutput(command, args, cwd) {
|
|
10
57
|
return new Promise((resolve, reject) => {
|
|
11
58
|
logger.debug(`Executing: ${command} ${args.join(" ")}`);
|
|
12
59
|
const proc = spawn(command, args, {
|
|
13
60
|
cwd,
|
|
14
|
-
stdio: ["inherit", "pipe", "pipe"],
|
|
15
|
-
shell:
|
|
61
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
62
|
+
shell: false,
|
|
16
63
|
});
|
|
17
64
|
let output = "";
|
|
18
65
|
if (proc.stdout) {
|
|
19
66
|
proc.stdout.on("data", (data) => {
|
|
20
|
-
process.stdout.write(data);
|
|
67
|
+
process.stdout.write(data);
|
|
21
68
|
output += data.toString();
|
|
22
69
|
});
|
|
23
70
|
}
|
|
24
71
|
if (proc.stderr) {
|
|
25
72
|
proc.stderr.on("data", (data) => {
|
|
26
|
-
process.stderr.write(data);
|
|
73
|
+
process.stderr.write(data);
|
|
27
74
|
output += data.toString();
|
|
28
75
|
});
|
|
29
76
|
}
|
|
30
77
|
proc.on("close", (code) => {
|
|
31
|
-
// Resolve all exit codes (even 1 or 2 or others) so we can parse output.
|
|
32
|
-
// But we might want to differentiate "crash" vs "lint failure".
|
|
33
|
-
// ESLint exit code 1 = user lint error. code 2 = config/crash error.
|
|
34
78
|
if (code === null)
|
|
35
|
-
code = 1;
|
|
79
|
+
code = 1;
|
|
36
80
|
resolve({ output, code });
|
|
37
81
|
});
|
|
38
82
|
proc.on("error", (err) => {
|
|
@@ -40,83 +84,75 @@ export async function runCommandWithOutput(command, args, cwd) {
|
|
|
40
84
|
});
|
|
41
85
|
});
|
|
42
86
|
}
|
|
43
|
-
// Return type: null means success (no errors), string means summary of errors/warnings
|
|
44
87
|
export async function runEslint(cwd, configPath, fix, files = ["."]) {
|
|
45
88
|
logInfo("Running ESLint...");
|
|
46
89
|
console.log();
|
|
90
|
+
const eslintBin = resolveBin("eslint", "bin/eslint.js");
|
|
47
91
|
const args = [
|
|
48
|
-
|
|
92
|
+
eslintBin,
|
|
49
93
|
...files,
|
|
50
94
|
"--config", configPath,
|
|
51
95
|
...(fix ? ["--fix"] : []),
|
|
52
96
|
];
|
|
53
97
|
try {
|
|
54
|
-
const { output, code } = await runCommandWithOutput(
|
|
55
|
-
if (
|
|
56
|
-
|
|
98
|
+
const { output, code } = await runCommandWithOutput(process.execPath, args, cwd);
|
|
99
|
+
if (code !== 0 && code !== 1) {
|
|
100
|
+
return `Process failed with exit code ${code}`;
|
|
57
101
|
}
|
|
58
102
|
const cleanOutput = stripAnsi(output);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return `${match[1]} problems (${match[2]} errors, ${match[3]} warnings)`;
|
|
63
|
-
}
|
|
64
|
-
// Check if there are errors but no summary line
|
|
65
|
-
if (cleanOutput.includes("error") || cleanOutput.includes("warning")) {
|
|
66
|
-
// Maybe custom format or specific error
|
|
67
|
-
// Try to count occurences of "error"
|
|
68
|
-
const errorCount = (cleanOutput.match(/error/gi) || []).length;
|
|
69
|
-
if (errorCount > 0)
|
|
70
|
-
return `${errorCount} issues found`;
|
|
71
|
-
return "Issues found";
|
|
103
|
+
const problemMatch = cleanOutput.match(/(\d+) problems? \((\d+) errors?, (\d+) warnings?\)/);
|
|
104
|
+
if (problemMatch) {
|
|
105
|
+
return problemMatch[0];
|
|
72
106
|
}
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
return
|
|
107
|
+
const errorMatch = cleanOutput.match(/(\d+) error/);
|
|
108
|
+
if (errorMatch) {
|
|
109
|
+
return `${errorMatch[0]} found`;
|
|
76
110
|
}
|
|
77
|
-
|
|
111
|
+
if (code === 0)
|
|
112
|
+
return null;
|
|
113
|
+
return "Issues found";
|
|
78
114
|
}
|
|
79
115
|
catch (e) {
|
|
80
|
-
logError("
|
|
81
|
-
|
|
116
|
+
logError("Failed to run ESLint", e);
|
|
117
|
+
return e.message || "Unknown error";
|
|
82
118
|
}
|
|
83
119
|
}
|
|
84
120
|
export async function runPrettier(cwd, configPath, fix, files = ["."]) {
|
|
85
121
|
logInfo("Running Prettier...");
|
|
86
|
-
|
|
122
|
+
// Try Prettier v3 path first, then fallback (v3: bin/prettier.cjs, v2: bin-prettier.js)
|
|
123
|
+
let prettierBin = resolveBin("prettier", "bin/prettier.cjs");
|
|
124
|
+
// If resolved to "prettier" (PATH) or logic failed, check if file exists, if not try legacy
|
|
125
|
+
// Actually resolveBin returns bare "prettier" if not found.
|
|
126
|
+
// If it is just "prettier", we might want to try legacy path too before giving up.
|
|
127
|
+
if (prettierBin === "prettier" || !fs.existsSync(prettierBin)) {
|
|
128
|
+
const legacy = resolveBin("prettier", "bin-prettier.js");
|
|
129
|
+
if (legacy !== "prettier" && fs.existsSync(legacy)) {
|
|
130
|
+
prettierBin = legacy;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
87
133
|
const args = [
|
|
88
|
-
|
|
89
|
-
...files,
|
|
90
|
-
"--config", configPath,
|
|
134
|
+
prettierBin,
|
|
91
135
|
...(fix ? ["--write"] : ["--check"]),
|
|
136
|
+
"--config", configPath,
|
|
137
|
+
"--ignore-path", ".gitignore",
|
|
138
|
+
...files
|
|
92
139
|
];
|
|
93
140
|
try {
|
|
94
|
-
const { output, code } = await runCommandWithOutput(
|
|
95
|
-
if (!output.endsWith('\n')) {
|
|
96
|
-
console.log();
|
|
97
|
-
}
|
|
98
|
-
const cleanOutput = stripAnsi(output);
|
|
99
|
-
if (!fix) {
|
|
100
|
-
// In check mode with issues: "Code style issues found in 2 files"
|
|
101
|
-
const match = cleanOutput.match(/Code style issues found in (\d+) files?/);
|
|
102
|
-
if (match) {
|
|
103
|
-
return `${match[1]} unformatted files`;
|
|
104
|
-
}
|
|
105
|
-
if (cleanOutput.includes("[warn]")) {
|
|
106
|
-
return "Style issues found";
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// Prettier specific: exit code 2 usually means error/crash. code 1 (in check mode) means unformatted.
|
|
141
|
+
const { output, code } = await runCommandWithOutput(process.execPath, args, cwd);
|
|
110
142
|
if (code !== 0) {
|
|
111
|
-
if (fix
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
143
|
+
if (!fix) {
|
|
144
|
+
if (stripAnsi(output).includes("Code style issues found")) {
|
|
145
|
+
return "Code style issues found";
|
|
146
|
+
}
|
|
147
|
+
if (code === 1)
|
|
148
|
+
return "Formatting issues found";
|
|
149
|
+
}
|
|
150
|
+
return `Process failed with exit code ${code}`;
|
|
115
151
|
}
|
|
116
152
|
return null;
|
|
117
153
|
}
|
|
118
154
|
catch (e) {
|
|
119
|
-
logError("
|
|
120
|
-
|
|
155
|
+
logError("Failed to run Prettier", e);
|
|
156
|
+
return e.message || "Unknown error";
|
|
121
157
|
}
|
|
122
158
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rhine-lint",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"module": "./dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,17 +34,18 @@
|
|
|
34
34
|
"prepublishOnly": "npm run build"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@eslint/compat": "^1.3.2",
|
|
38
37
|
"@eslint/css": "^0.10.0",
|
|
39
38
|
"@eslint/eslintrc": "^3.3.1",
|
|
40
39
|
"@eslint/json": "^0.13.1",
|
|
41
40
|
"@eslint/markdown": "^7.1.0",
|
|
42
41
|
"@next/eslint-plugin-next": "^16.1.1",
|
|
42
|
+
"ajv": "^8.12.0",
|
|
43
43
|
"cac": "^6.7.14",
|
|
44
44
|
"consola": "^3.4.2",
|
|
45
45
|
"defu": "^6.1.4",
|
|
46
46
|
"eslint": "^9.39.2",
|
|
47
47
|
"eslint-config-prettier": "^10.1.8",
|
|
48
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
48
49
|
"eslint-plugin-import-x": "^4.16.1",
|
|
49
50
|
"eslint-plugin-react": "^7.37.5",
|
|
50
51
|
"eslint-plugin-react-hooks": "^7.0.1",
|