Skip to content

Commit e1ba447

Browse files
authored
Add Karma configuration from Angular plugin configuration (#885)
1 parent 4fc91eb commit e1ba447

File tree

12 files changed

+110
-60
lines changed

12 files changed

+110
-60
lines changed

packages/knip/fixtures/plugins/angular/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
},
1111
"devDependencies": {
1212
"@angular/cli": "*",
13+
"karma": "*",
14+
"karma-chrome-launcher": "*",
15+
"karma-coverage": "*",
16+
"karma-jasmine": "*",
17+
"karma-jasmine-html-reporter": "*",
18+
"jasmine-core": "*",
1319
"typescript": "*"
1420
}
1521
}

packages/knip/fixtures/plugins/angular/src/app/app.component.spec.ts

Whitespace-only changes.

packages/knip/fixtures/plugins/angular2/angular.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@
9898
}
9999
],
100100
"styles": ["src/styles.css"],
101-
"scripts": []
101+
"scripts": [],
102+
"karmaConfig": "another-karma.conf.js"
102103
}
103104
}
104105
}

packages/knip/fixtures/plugins/angular2/another-karma.conf.js

Whitespace-only changes.

packages/knip/fixtures/plugins/angular2/karma.conf.js

Whitespace-only changes.

packages/knip/fixtures/plugins/angular2/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"@angular/cli": "*",
88
"@angular/ssr": "*",
99
"@angular-builders/custom-esbuild": "*",
10-
"@angular-devkit/build-angular": "*"
10+
"@angular-devkit/build-angular": "*",
11+
"karma": "*"
1112
}
1213
}

packages/knip/src/plugins/angular/index.ts

+30-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import type { IsPluginEnabled, Plugin, ResolveConfig } from '../../types/config.
22
import { type Input, toConfig, toDependency, toEntry, toProductionEntry } from '../../util/input.js';
33
import { join } from '../../util/path.js';
44
import { hasDependency } from '../../util/plugin.js';
5-
import type { AngularCLIWorkspaceConfiguration, WebpackBrowserSchemaForBuildFacade } from './types.js';
5+
import * as karma from '../karma/helpers.js';
6+
import type { AngularCLIWorkspaceConfiguration, KarmaTarget, WebpackBrowserSchemaForBuildFacade } from './types.js';
67

78
// https://angular.io/guide/workspace-config
89

@@ -61,6 +62,34 @@ const resolveConfig: ResolveConfig<AngularCLIWorkspaceConfiguration> = async (co
6162
}
6263
}
6364
}
65+
if (target.builder === '@angular-devkit/build-angular:karma' && opts) {
66+
const karmaBuilderOptions = opts as KarmaTarget;
67+
// https://github.com/angular/angular-cli/blob/19.0.6/packages/angular_devkit/build_angular/src/builders/karma/schema.json#L143
68+
const testFilePatterns = karmaBuilderOptions.include ?? ['**/*.spec.ts'];
69+
for (const testFilePattern of testFilePatterns) {
70+
inputs.add(toEntry(testFilePattern));
71+
}
72+
// https://github.com/angular/angular-cli/blob/19.0.6/packages/angular_devkit/build_angular/src/builders/karma/schema.json#L146
73+
const excludedTestFilePatterns = karmaBuilderOptions.exclude ?? [];
74+
for (const excludedTestFilePattern of excludedTestFilePatterns) {
75+
inputs.add(toEntry(`!${excludedTestFilePattern}`));
76+
}
77+
const karmaConfig = karmaBuilderOptions.karmaConfig;
78+
if (!karmaConfig) {
79+
// Hardcoded default Karma config from Angular builder
80+
// https://github.com/angular/angular-cli/blob/19.0.6/packages/angular_devkit/build_angular/src/builders/karma/index.ts#L115
81+
karma
82+
.inputsFromPlugins(
83+
['karma-jasmine', 'karma-chrome-launcher', 'karma-jasmine-html-reporter', 'karma-coverage'],
84+
options.manifest.devDependencies
85+
)
86+
.forEach(inputs.add, inputs);
87+
karma.inputsFromFrameworks(['jasmine']).forEach(inputs.add, inputs);
88+
}
89+
if (karmaConfig && !karma.configFiles.includes(karmaConfig)) {
90+
inputs.add(toConfig('karma', karmaConfig, options.configFilePath));
91+
}
92+
}
6493
}
6594
}
6695

packages/knip/src/plugins/angular/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2533,7 +2533,7 @@ interface ExtractI18NTarget1 {
25332533
/**
25342534
* Karma target options for Build Facade.
25352535
*/
2536-
interface KarmaTarget {
2536+
export interface KarmaTarget {
25372537
/**
25382538
* The name of the main entry-point file.
25392539
*/
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { type Input, toDeferResolveEntry, toDevDependency } from '../../util/input.js';
2+
import { isInternal } from '../../util/path.js';
3+
import type { Config, ConfigOptions } from './types.js';
4+
5+
//👇 All but CoffeeScript ones. Low usage nowadays compared to the effort to implement support for those files
6+
export const configFiles = ['karma.conf.js', 'karma.conf.ts', '.config/karma.conf.js', '.config/karma.conf.ts'];
7+
8+
export const inputsFromFrameworks = (frameworks: readonly string[]): readonly Input[] =>
9+
frameworks.map(framework => {
10+
return toDevDependency(framework === 'jasmine' ? 'jasmine-core' : framework);
11+
});
12+
13+
export const inputsFromPlugins = (
14+
plugins: ConfigOptions['plugins'],
15+
devDependencies: Record<string, string> | undefined
16+
): readonly Input[] => {
17+
if (!plugins) {
18+
const karmaPluginDevDeps = Object.keys(devDependencies ?? {}).filter(name => name.startsWith('karma-'));
19+
return karmaPluginDevDeps.map(karmaPluginDevDep => toDevDependency(karmaPluginDevDep));
20+
}
21+
return plugins
22+
.map(plugin => {
23+
if (typeof plugin !== 'string') return;
24+
return isInternal(plugin) ? toDeferResolveEntry(plugin) : toDevDependency(plugin);
25+
})
26+
.filter(input => !!input);
27+
};
28+
29+
export type ConfigFile = (config: Config) => void;
30+
export const loadConfig = (configFile: ConfigFile): ConfigOptions | undefined => {
31+
if (typeof configFile !== 'function') return;
32+
const inMemoryConfig = new InMemoryConfig();
33+
configFile(inMemoryConfig);
34+
return inMemoryConfig.config ?? {};
35+
};
36+
37+
/**
38+
* Dummy configuration class with no default config options
39+
* Relevant config options' defaults are empty, so that's good enough
40+
* Real class: https://github.com/karma-runner/karma/blob/v6.4.4/lib/config.js#L275
41+
*/
42+
class InMemoryConfig implements Config {
43+
config?: ConfigOptions;
44+
/**
45+
* Real method merges configurations with Lodash's `mergeWith`
46+
* https://github.com/karma-runner/karma/blob/v6.4.4/lib/config.js#L343
47+
*/
48+
set(config: ConfigOptions) {
49+
this.config = config;
50+
}
51+
}

packages/knip/src/plugins/karma/index.ts

+8-51
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { IsPluginEnabled, Plugin, ResolveConfig, ResolveEntryPaths } from '../../types/config.js';
2-
import { type Input, toDeferResolveEntry, toDevDependency, toEntry } from '../../util/input.js';
3-
import { isInternal, join } from '../../util/path.js';
2+
import { type Input, toEntry } from '../../util/input.js';
3+
import { join } from '../../util/path.js';
44
import { hasDependency } from '../../util/plugin.js';
5-
import type { Config, ConfigOptions } from './types.js';
5+
import { type ConfigFile, configFiles, inputsFromFrameworks, inputsFromPlugins, loadConfig } from './helpers.js';
66

77
// https://karma-runner.github.io/latest/config/configuration-file.html
88

@@ -12,50 +12,29 @@ const enablers = ['karma'];
1212

1313
const isEnabled: IsPluginEnabled = ({ dependencies }) => hasDependency(dependencies, enablers);
1414

15-
//👇 All but CoffeeScript ones. Low usage nowadays compared to the effort to implement support for those files
16-
const config = ['karma.conf.js', 'karma.conf.ts', '.config/karma.conf.js', '.config/karma.conf.ts'];
15+
const config = configFiles;
1716

1817
const entry: string[] = [];
1918

20-
type ConfigFile = (config: Config) => void;
21-
2219
const resolveConfig: ResolveConfig<ConfigFile> = async (localConfig, options) => {
2320
const inputs = new Set<Input>();
2421

2522
const config = loadConfig(localConfig);
23+
if (!config) return [];
2624

2725
if (config.frameworks) {
28-
for (const framework of config.frameworks) {
29-
inputs.add(toDevDependency(devDepForFramework(framework)));
30-
}
31-
}
32-
if (config.plugins) {
33-
for (const plugin of config.plugins) {
34-
if (typeof plugin !== 'string') continue;
35-
if (isInternal(plugin)) {
36-
inputs.add(toDeferResolveEntry(plugin));
37-
} else {
38-
inputs.add(toDevDependency(plugin));
39-
}
40-
}
41-
} else {
42-
const karmaPluginDevDeps = Object.keys(options.manifest.devDependencies ?? {}).filter(name =>
43-
name.startsWith('karma-')
44-
);
45-
for (const karmaPluginDevDep of karmaPluginDevDeps) {
46-
inputs.add(toDevDependency(karmaPluginDevDep));
47-
}
26+
inputsFromFrameworks(config.frameworks).forEach(inputs.add, inputs);
4827
}
28+
inputsFromPlugins(config.plugins, options.manifest.devDependencies).forEach(inputs.add, inputs);
4929

5030
return Array.from(inputs);
5131
};
5232

53-
const devDepForFramework = (framework: string): string => (framework === 'jasmine' ? 'jasmine-core' : framework);
54-
5533
const resolveEntryPaths: ResolveEntryPaths<ConfigFile> = (localConfig, options) => {
5634
const inputs = new Set<Input>();
5735

5836
const config = loadConfig(localConfig);
37+
if (!config) return [];
5938

6039
const basePath = config.basePath ?? '';
6140
if (config.files) {
@@ -73,28 +52,6 @@ const resolveEntryPaths: ResolveEntryPaths<ConfigFile> = (localConfig, options)
7352
return Array.from(inputs);
7453
};
7554

76-
const loadConfig = (configFile: ConfigFile): ConfigOptions => {
77-
const inMemoryConfig = new InMemoryConfig();
78-
configFile(inMemoryConfig);
79-
return inMemoryConfig.config ?? {};
80-
};
81-
82-
/**
83-
* Dummy configuration class with no default config options
84-
* Relevant config options' defaults are empty, so that's good enough
85-
* Real class: https://github.com/karma-runner/karma/blob/v6.4.4/lib/config.js#L275
86-
*/
87-
class InMemoryConfig implements Config {
88-
config?: ConfigOptions;
89-
/**
90-
* Real method merges configurations with Lodash's `mergeWith`
91-
* https://github.com/karma-runner/karma/blob/v6.4.4/lib/config.js#L343
92-
*/
93-
set(config: ConfigOptions) {
94-
this.config = config;
95-
}
96-
}
97-
9855
export default {
9956
title,
10057
enablers,

packages/knip/test/plugins/angular.test.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ test('Find dependencies with the Angular plugin', async () => {
1515

1616
assert(issues.unlisted['angular.json']['@angular-devkit/build-angular']);
1717
assert(issues.unresolved['tsconfig.spec.json']['jasmine']);
18+
assert(issues.devDependencies['package.json']['karma']);
1819

1920
assert.deepEqual(counters, {
2021
...baseCounters,
22+
devDependencies: 1,
2123
unlisted: 1,
2224
unresolved: 1,
23-
processed: 1,
24-
total: 1,
25+
processed: 2,
26+
total: 2,
2527
});
2628
});

packages/knip/test/plugins/angular2.test.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ import baseCounters from '../helpers/baseCounters.js';
88
const cwd = resolve('fixtures/plugins/angular2');
99

1010
test('Find dependencies with the Angular plugin (2)', async () => {
11-
const { counters } = await main({
11+
const { issues, counters } = await main({
1212
...baseArguments,
1313
cwd,
1414
});
1515

16+
assert(issues.devDependencies['package.json']['karma']);
17+
1618
assert.deepEqual(counters, {
1719
...baseCounters,
18-
processed: 5,
19-
total: 5,
20+
devDependencies: 1,
21+
processed: 7,
22+
total: 7,
2023
});
2124
});

0 commit comments

Comments
 (0)