Skip to content

Commit 8f99f41

Browse files
committed
refactor: wip
1 parent 2266a0b commit 8f99f41

File tree

5 files changed

+175
-155
lines changed

5 files changed

+175
-155
lines changed

packages/utils/src/lib/import-module.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ type JitiOptions = Exclude<Parameters<typeof createJitiSource>[1], undefined>;
1717
* causing errors in packages that use new URL(..., import.meta.url).
1818
*/
1919
export const JITI_NATIVE_MODULES = [
20-
'@vitest/eslint-plugin',
21-
'@code-pushup/eslint-config',
22-
'lighthouse',
20+
//'@vitest/eslint-plugin',
21+
//'@code-pushup/eslint-config',
22+
//'lighthouse',
2323
] as const;
2424

2525
export type ImportModuleOptions = JitiOptions & {

tools/jiti/README_JITI_ISSUE.md

Lines changed: 125 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ When encountering a module loading error with jiti/tsx, follow this systematic a
88

99
- Open `code-pushup.config.ts`
1010
- Comment out all plugin configurations
11-
- Run: `nx code-pushup --print-config --output out.json`
11+
- Run: `nx reset && nx code-pushup -- print-config --output out.json`
1212
- Verify the command succeeds without errors
1313

1414
2. **Identify the problematic plugin**
@@ -61,12 +61,6 @@ Location of the file: `tools/jiti/<library-name>/fix-<library-name>.ts`
6161

6262
## List of Issues
6363

64-
### My Issue
65-
66-
...
67-
68-
## Old Issuess for reference
69-
7064
### axe-core
7165

7266
**Library** (required)
@@ -113,19 +107,33 @@ import axe from 'axe-core';
113107
**Preliminary fix** (optional)
114108
https://github.com/code-pushup/cli/pull/1228/commits/b7109fb6c78803adae7f11605446ef2b6de950ff
115109

116-
### vitest
110+
### @vitest/eslint-plugin (via ESLint plugin)
117111

118112
**Library** (required)
119113
[@vitest/eslint-plugin](https://github.com/vitest-dev/vitest/tree/main/packages/eslint-plugin)
120114

121115
**Problem** (required)
122-
`@vitest/eslint-plugin` cannot be executed with tsx and jiti internally
116+
`@vitest/eslint-plugin` fails with Invalid URL error when ESLint configs are loaded through `configureEslintPlugin()`. The issue occurs because tsx intercepts nested imports from files loaded by jiti, transforming `@vitest/eslint-plugin` before jiti can handle it.
123117

124118
**Description** (required)
125-
The Vitest ESLint plugin is used for linting Vitest test files in our codebase. When running ESLint with tsx module loading (as used in `nx code-pushup`), the plugin fails due to URL parsing issues in its CommonJS distribution. When tsx transforms `@vitest/eslint-plugin`, `import.meta.url` becomes `'about:blank'`, causing `new URL('index.cjs', 'about:blank')` to fail with Invalid URL error.
119+
The ESLint plugin (`configureEslintPlugin()`) loads ESLint configuration files using `loadConfigByPath()`. When ESLint configs (like `eslint.config.js`) import `@code-pushup/eslint-config/vitest.js`, which in turn imports `@vitest/eslint-plugin`, tsx transforms the CommonJS distribution. This causes `import.meta.url` to become `'about:blank'`, leading to `new URL('index.cjs', 'about:blank')` failing with Invalid URL error.
126120

127-
**Reproduction** (required)
128-
`TSX_TSCONFIG_PATH=tsconfig.base.json node --import tsx tools/jiti/vitest/issue-vitest.ts`
121+
**Root Cause:**
122+
123+
- The CLI runs with `NODE_OPTIONS="--import tsx"`, registering tsx as a Node.js loader
124+
- When `eslint.config.js` executes with static imports, Node.js uses tsx for module resolution
125+
- tsx transforms `@vitest/eslint-plugin` before jiti can handle it
126+
- jiti's `nativeModules` configuration only prevents jiti from transforming modules, not tsx
127+
- This is an architectural limitation when both jiti and tsx are used together
128+
129+
**Reproduction** (required)
130+
`nx reset && nx code-pushup -- print-config --output out.json` (with `configureEslintPlugin()` enabled in `code-pushup.config.ts`)
131+
132+
Or using the reproduction script:
133+
`TSX_TSCONFIG_PATH=tsconfig.base.json node --import tsx tools/jiti/eslint-vitest/issue-eslint-vitest.ts`
134+
135+
Or using the old reproduction script (now with shorter output):
136+
`TSX_TSCONFIG_PATH=tsconfig.base.json node --import tsx tools/jiti/old-vitest/issue-vitest.ts`
129137

130138
**Error** (required)
131139

@@ -148,30 +156,118 @@ TypeError: Invalid URL
148156
```
149157

150158
**Issues** (optional)
151-
[tools/jiti/vitest/issue-vitest.ts](tools/jiti/vitest/issue-vitest.ts)
159+
[tools/jiti/eslint-vitest/issue-eslint-vitest.ts](tools/jiti/eslint-vitest/issue-eslint-vitest.ts)
152160

153-
**Source Code** (required)
154-
[packages/utils/src/lib/import-module.ts](packages/utils/src/lib/import-module.ts#L19-L23) - `JITI_NATIVE_MODULES` list that includes `@vitest/eslint-plugin`
161+
**Status** (required)
162+
Resolved - Fixed by temporarily removing `--import tsx` from `NODE_OPTIONS` when loading ESLint configs, allowing jiti to handle all module loading without tsx interference.
155163

156-
```typescript
157-
export const JITI_NATIVE_MODULES = ['@vitest/eslint-plugin', '@code-pushup/eslint-config', 'lighthouse'] as const;
158-
```
164+
**Source Code** (required)
165+
[packages/plugin-eslint/src/lib/meta/versions/flat.ts](packages/plugin-eslint/src/lib/meta/versions/flat.ts#L64-L73) - `loadConfigByPath` function that delegates to child process loading
159166

160-
[packages/plugin-eslint/src/lib/meta/versions/flat.ts](packages/plugin-eslint/src/lib/meta/versions/flat.ts#L62-L69) - `loadConfigByPath` function that loads ESLint config files
167+
[packages/plugin-eslint/src/lib/meta/versions/flat.ts](packages/plugin-eslint/src/lib/meta/versions/flat.ts#L75-L210) - `loadConfigInChildProcess` function that spawns a child process without tsx
161168

162169
```typescript
163170
async function loadConfigByPath(configPath: string): Promise<FlatConfig> {
164-
const absolutePath = path.isAbsolute(configPath) ? configPath : path.join(process.cwd(), configPath);
165-
// Use jiti's importModule instead of dynamic import to ensure nativeModules
166-
// (like @vitest/eslint-plugin) are loaded without transformation.
167-
const mod = await importModule<FlatConfig | { default: FlatConfig }>({
168-
filepath: absolutePath,
169-
});
170-
return 'default' in mod ? mod.default : mod;
171+
// Temporarily remove --import tsx from NODE_OPTIONS to prevent tsx from intercepting
172+
// nested imports. This allows jiti to handle all module loading and respect its
173+
// nativeModules configuration (like @vitest/eslint-plugin).
174+
const originalNodeOptions = process.env.NODE_OPTIONS;
175+
176+
// Remove --import tsx if present, preserving other options
177+
const nodeOptions =
178+
originalNodeOptions
179+
?.split(/\s+/)
180+
.filter(opt => {
181+
if (opt.includes('--import')) {
182+
return !opt.includes('tsx');
183+
}
184+
return true;
185+
})
186+
.join(' ') || undefined;
187+
188+
// Temporarily set modified NODE_OPTIONS if it changed
189+
if (nodeOptions !== originalNodeOptions) {
190+
if (nodeOptions) {
191+
process.env.NODE_OPTIONS = nodeOptions;
192+
} else {
193+
delete process.env.NODE_OPTIONS;
194+
}
195+
}
196+
197+
try {
198+
// Load config - jiti will handle imports without tsx interference
199+
const mod = await importModule<FlatConfig | { default: FlatConfig }>({
200+
filepath: configPath,
201+
});
202+
return 'default' in mod ? mod.default : mod;
203+
} finally {
204+
// Always restore original NODE_OPTIONS
205+
if (originalNodeOptions !== undefined) {
206+
process.env.NODE_OPTIONS = originalNodeOptions;
207+
} else {
208+
delete process.env.NODE_OPTIONS;
209+
}
210+
}
171211
}
172212
```
173213

174-
**Preliminary fix** (optional)
175-
[tools/jiti/vitest/fix-vitest.ts](tools/jiti/vitest/fix-vitest.ts) - Fix implementation using jiti's `importModule` instead of dynamic `import()` to ensure `@vitest/eslint-plugin` is loaded natively without transformation.
214+
[code-pushup.config.ts](code-pushup.config.ts#L20) - `configureEslintPlugin()` call that triggers ESLint config loading
215+
216+
[eslint.config.js](eslint.config.js#L8) - ESLint config that imports `@code-pushup/eslint-config/vitest.js`
217+
218+
[packages/utils/src/lib/import-module.ts](packages/utils/src/lib/import-module.ts#L19-L23) - `JITI_NATIVE_MODULES` list that includes `@vitest/eslint-plugin` and `@code-pushup/eslint-config`
219+
220+
**Solution** (required)
221+
The fix loads ESLint configs in a child process without tsx to prevent tsx from intercepting nested imports. This allows jiti to handle all module loading and respect its `nativeModules` configuration.
222+
223+
**How it works:**
224+
225+
1. Create a temporary loader script that uses jiti to load the ESLint config
226+
2. Spawn a child Node.js process with `NODE_OPTIONS` that excludes `--import tsx`
227+
3. The child process loads the config using jiti, which handles all imports without tsx interference
228+
4. Serialize the config (handling circular references) and write it to a temporary file
229+
5. Read the serialized config from the parent process and return it
230+
6. Clean up temporary files
231+
232+
**Why this works:**
233+
234+
- Child processes don't inherit already-registered loaders from the parent process
235+
- By removing `--import tsx` from the child's `NODE_OPTIONS`, tsx is never registered as a loader
236+
- jiti can then handle all module loading and respect its `nativeModules` configuration
237+
- `@vitest/eslint-plugin` is loaded natively without transformation, preserving `import.meta.url`
238+
- The parent process continues to use tsx normally for other operations
239+
240+
**Verification** (required)
241+
Run the following command to verify the fix:
242+
243+
```bash
244+
nx reset && nx code-pushup -- print-config --output out.json
245+
```
246+
247+
**Expected result:** The original `TypeError: Invalid URL` error with `@vitest/eslint-plugin` should be resolved. The command should no longer fail with the tsx transformation error.
248+
249+
**Note on optional dependencies:** If your ESLint config references optional peer dependencies (like `eslint-plugin-jsx-a11y`) that aren't installed, the child process may fail with a "Cannot find module" error. This is expected behavior and separate from the original JITI/tsx issue. To resolve:
250+
251+
1. Install the missing optional dependency: `npm install eslint-plugin-jsx-a11y`
252+
2. Or remove the reference to the optional plugin from your ESLint config
253+
254+
The reproduction script should also run without the original Invalid URL error:
255+
256+
```bash
257+
TSX_TSCONFIG_PATH=tsconfig.base.json node --import tsx tools/jiti/eslint-vitest/issue-eslint-vitest.ts
258+
```
259+
260+
**Known limitations:**
261+
262+
- The child process approach requires all ESLint config dependencies to be installed. Optional peer dependencies that are referenced in the config must be installed, even if they're marked as optional in package.json.
263+
- This is a limitation of loading ESLint configs programmatically - ESLint itself may handle missing optional dependencies more gracefully during actual linting, but config parsing requires all referenced modules to be available.
264+
- When a missing optional dependency is encountered, the error message now provides clear guidance on which package needs to be installed and how to install it.
265+
266+
**Error handling:**
267+
The implementation includes improved error messages for missing ESLint dependencies. When a module like `eslint-plugin-jsx-a11y` is missing, the error will clearly indicate:
268+
269+
- Which dependency is missing
270+
- That it's referenced in the ESLint config but not installed
271+
- How to fix it (e.g., `npm install eslint-plugin-jsx-a11y`)
176272

177-
[packages/plugin-eslint/src/lib/meta/versions/flat.ts](packages/plugin-eslint/src/lib/meta/versions/flat.ts#L62-L69) - Updated `loadConfigByPath` to use `importModule` instead of dynamic import.
273+
This makes it easier to diagnose and resolve missing optional dependency issues.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Test script for @vitest/eslint-plugin TypeError: Invalid URL issue fix
3+
* when using tsx loader.
4+
*
5+
* This script tests that the fix works by directly loading ESLint configs:
6+
* - Direct import loads eslint.config.js (mimics loadConfigByPath behavior)
7+
* - ESLint config imports @code-pushup/eslint-config/vitest.js
8+
* - That package imports @vitest/eslint-plugin
9+
* - With the fix: config loads successfully without Invalid URL error
10+
* - Without the fix: would fail with "new URL('index.cjs', 'about:blank')" error
11+
*
12+
* Run with:
13+
* TSX_TSCONFIG_PATH=tsconfig.base.json node --import tsx tools/jiti/eslint-vitest/issue-eslint-vitest.ts
14+
*
15+
* Flow:
16+
* -> TSX_TSCONFIG_PATH=tsconfig.base.json node --import tsx node packages/cli/src/index.ts
17+
* -> core: loadConfigByPath('code-pushup.config.ts')
18+
* -> utils:importModule('code-pushup.config.ts')
19+
* -> preset: configureEslintPlugin()
20+
* -> nx: eslintConfigFromAllNxProjects()
21+
* -> eslint: eslintPlugin(targets)
22+
* -> eslint: listAuditsAndGroups(targets)
23+
* -> eslint: listRules(targets)
24+
* -> eslint: loadRulesForFlatConfig(target)
25+
* -> eslint: loadConfigByPath(eslintrc)
26+
* 🔎 🔥 -> loads 'eslint.config.js'
27+
* -> eslint.config.js imports @code-pushup/eslint-config/vitest.js
28+
* -> @code-pushup/eslint-config/vitest.js imports @vitest/eslint-plugin
29+
* -> tsx transforms @vitest/eslint-plugin -> 🔥 Invalid URL error (before fix)
30+
* -> child process prevents tsx interference (after fix)
31+
*
32+
*/
33+
import path from 'node:path';
34+
import { pathToFileURL } from 'node:url';
35+
36+
// Mimic hot path of loading ESLint config 🔎 🔥
37+
const configPath = path.join(process.cwd(), 'code-pushup.config.ts');
38+
const configUrl = pathToFileURL(configPath).toString();
39+
const config = await import(configUrl);
40+
41+
console.log('Reproduction Failed: ESLint config loaded without errors');
42+
console.log(
43+
'Config type:',
44+
Array.isArray(config.default)
45+
? `array (${config.default.length} items)`
46+
: typeof config.default,
47+
);

tools/jiti/vitest/fix-vitest.ts

Lines changed: 0 additions & 57 deletions
This file was deleted.

tools/jiti/vitest/issue-vitest.ts

Lines changed: 0 additions & 66 deletions
This file was deleted.

0 commit comments

Comments
 (0)