Skip to content

Commit 0914bd3

Browse files
committed
Don't exit on config file load error
1 parent 2c6d8a0 commit 0914bd3

7 files changed

Lines changed: 71 additions & 11 deletions

File tree

packages/docs/src/content/docs/reference/known-issues.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ See [handling issues][1] to learn more about dealing with lint issues.
88

99
## Exceptions from config files
1010

11-
An exception may be thrown when a Knip plugin loads a JavaScript or TypeScript
12-
configuration file such as `webpack.config.js` or `vite.config.ts`:
11+
Knip plugins may fail to load a JavaScript or TypeScript configuration file
12+
such as `webpack.config.js` or `vite.config.ts`:
1313

1414
```sh
1515
$ knip
16-
Error loading .../vite.config.ts
16+
ERROR: Error loading vite.config.ts
1717
```
1818

1919
Knip may load such files differently, in a different environment, with missing
@@ -27,6 +27,7 @@ Potential workarounds:
2727
- Use a helper package like [dotenvx][3]
2828
- `KEY=VAL knip`
2929
- `node --env-file .env $(which knip)`
30+
- Run the build script to generate required files.
3031
- Disable loading the file by overriding the default `config` for that plugin.
3132
- Example: `vite: { config: [] }`
3233
- In a monorepo, be more specific like so:
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "@fixtures/plugin-config-load-error",
3+
"main": "./src/index.ts",
4+
"scripts": {
5+
"build": "vite build"
6+
},
7+
"devDependencies": {
8+
"vite": "*"
9+
}
10+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const greet = (name: string) => `hello ${name}`;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import './missing-bootstrap-output.js';
2+
3+
export default { build: { outDir: 'dist' } };

packages/knip/src/WorkspaceWorker.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ import {
4040
import { getPackageNameFromSpecifier } from './util/modules.ts';
4141
import { getKeysByValue } from './util/object.ts';
4242
import { timerify } from './util/Performance.ts';
43-
import { basename, dirname, isInternal, join } from './util/path.ts';
43+
import { basename, dirname, isInternal, join, toRelative } from './util/path.ts';
4444
import { extractPatternExtensions } from './util/pattern-extensions.ts';
45+
import { formatCauseMessage } from './util/errors.ts';
46+
import { logError } from './util/log.ts';
4547
import { loadConfigForPlugin } from './util/plugin.ts';
4648
import { ELLIPSIS } from './util/string.ts';
4749

@@ -428,14 +430,24 @@ export class WorkspaceWorker {
428430
if (!cache.resolveConfig) {
429431
const isLoad =
430432
typeof plugin.isLoadConfig === 'function' ? plugin.isLoadConfig(resolveOpts, this.dependencies) : true;
431-
const localConfig = isLoad && (await loadConfigForPlugin(configFilePath, plugin, resolveOpts, pluginName));
432-
if (localConfig) {
433-
const inputs = await plugin.resolveConfig(localConfig, resolveOpts);
434-
if (plugin.isFilterTransitiveDependencies && !isManifest) {
435-
this.filterTransitiveDependencies(inputs, configFilePath);
433+
if (isLoad) {
434+
try {
435+
const localConfig = await loadConfigForPlugin(configFilePath, plugin, resolveOpts, pluginName);
436+
if (localConfig) {
437+
const inputs = await plugin.resolveConfig(localConfig, resolveOpts);
438+
if (plugin.isFilterTransitiveDependencies && !isManifest) {
439+
this.filterTransitiveDependencies(inputs, configFilePath);
440+
}
441+
for (const input of inputs) addInput(input, configFilePath);
442+
cache.resolveConfig = inputs;
443+
}
444+
} catch (error) {
445+
if (!(error instanceof Error)) throw error;
446+
const relPath = toRelative(configFilePath, this.options.cwd);
447+
const cause = formatCauseMessage(error, this.options.cwd);
448+
logError(`Error loading ${relPath} (${cause})`);
449+
logError('Please fix or visit https://knip.dev/reference/known-issues');
436450
}
437-
for (const input of inputs) addInput(input, configFilePath);
438-
cache.resolveConfig = inputs;
439451
}
440452
}
441453
}

packages/knip/src/util/errors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export const isModuleNotFoundError = (error: Error): boolean => 'code' in error
2222

2323
export const isLoaderError = (error: Error): error is LoaderError => error instanceof LoaderError;
2424

25+
export const formatCauseMessage = (error: Error, cwd: string) => {
26+
let root: Error = error;
27+
while (root.cause instanceof Error) root = root.cause;
28+
return root.message.split('\n', 1)[0].replace(`${cwd}/`, '');
29+
};
30+
2531
export const getKnownErrors = (error: Error) => {
2632
if (isZodErrorLike(error))
2733
return [...error.issues].map(error => {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import assert from 'node:assert/strict';
2+
import test from 'node:test';
3+
import { main } from '../src/index.ts';
4+
import { createOptions } from './helpers/create-options.ts';
5+
import { exec } from './helpers/exec.ts';
6+
import { resolve } from './helpers/resolve.ts';
7+
8+
const cwd = resolve('fixtures/plugin-config-load-error');
9+
10+
test('Plugin config that throws on load should not be fatal', async () => {
11+
const options = await createOptions({ cwd });
12+
await assert.doesNotReject(main(options));
13+
const { counters } = await main(options);
14+
assert.equal(counters.processed, 2);
15+
});
16+
17+
test('Plugin config that throws on load exits non-zero via CLI', () => {
18+
const result = exec('knip --no-progress', { cwd });
19+
assert.match(result.stderr, /^ERROR: Error loading vite\.config\.ts /m);
20+
assert.equal(result.status, 1);
21+
});
22+
23+
test('Plugin config that throws on load exits zero with --no-exit-code', () => {
24+
const result = exec('knip --no-progress --no-exit-code', { cwd });
25+
assert.match(result.stderr, /^ERROR: Error loading vite\.config\.ts /m);
26+
assert.equal(result.status, 0);
27+
});

0 commit comments

Comments
 (0)