Skip to content

Commit

Permalink
Add Karma plugin (#871)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidlj95 authored Dec 13, 2024
1 parent e43304b commit 3d29854
Show file tree
Hide file tree
Showing 20 changed files with 343 additions and 0 deletions.
54 changes: 54 additions & 0 deletions packages/knip/fixtures/plugins/karma/karma.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Karma configuration
// Generated on Fri Dec 06 2024 13:19:12 GMT+0100 (Central European Standard Time)
// With command `bunx [email protected] init`

module.exports = config => {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',

// frameworks to use
// available frameworks: https://www.npmjs.com/search?q=keywords:karma-adapter
frameworks: ['jasmine'],

// list of files / patterns to load in the browser
files: [],

// list of files / patterns to exclude
exclude: [],

// preprocess matching files before serving them to the browser
// available preprocessors: https://www.npmjs.com/search?q=keywords:karma-preprocessor
preprocessors: {},

// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://www.npmjs.com/search?q=keywords:karma-reporter
reporters: ['progress'],

// web server port
port: 9876,

// enable / disable colors in the output (reporters and logs)
colors: true,

// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,

// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,

// start these browsers
// available browser launchers: https://www.npmjs.com/search?q=keywords:karma-launcher
browsers: ['Chrome'],

// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,

// Concurrency level
// how many browser instances should be started simultaneously
concurrency: Number.POSITIVE_INFINITY,
});
};
8 changes: 8 additions & 0 deletions packages/knip/fixtures/plugins/karma/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@fixtures/karma",
"scripts": {},
"devDependencies": {
"karma": "*",
"karma-coverage": "*"
}
}
7 changes: 7 additions & 0 deletions packages/knip/fixtures/plugins/karma2/karma.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = config => {
config.set({
basePath: 'src',
files: ['**/*.spec.js'],
exclude: ['**/*excluded*.js'],
});
};
Empty file.
7 changes: 7 additions & 0 deletions packages/knip/fixtures/plugins/karma2/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@fixtures/karma2",
"scripts": {},
"devDependencies": {
"karma": "*"
}
}
Empty file.
Empty file.
Empty file.
Empty file.
5 changes: 5 additions & 0 deletions packages/knip/fixtures/plugins/karma3/karma.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = config => {
config.set({
plugins: ['karma-jasmine', 'karma-coverage', './karma-plugin'],
});
};
7 changes: 7 additions & 0 deletions packages/knip/fixtures/plugins/karma3/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@fixtures/karma3",
"scripts": {},
"devDependencies": {
"karma": "*"
}
}
4 changes: 4 additions & 0 deletions packages/knip/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,10 @@
"title": "Jest plugin configuration (https://knip.dev/reference/plugins/jest)",
"$ref": "#/definitions/plugin"
},
"karma": {
"title": "karma plugin configuration (https://knip.dev/reference/plugins/karma)",
"$ref": "#/definitions/plugin"
},
"ladle": {
"title": "ladle plugin configuration (https://knip.dev/reference/plugins/ladle)",
"$ref": "#/definitions/plugin"
Expand Down
2 changes: 2 additions & 0 deletions packages/knip/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { default as glob } from './glob/index.js';
import { default as graphqlCodegen } from './graphql-codegen/index.js';
import { default as husky } from './husky/index.js';
import { default as jest } from './jest/index.js';
import { default as karma } from './karma/index.js';
import { default as ladle } from './ladle/index.js';
import { default as lefthook } from './lefthook/index.js';
import { default as lintStaged } from './lint-staged/index.js';
Expand Down Expand Up @@ -110,6 +111,7 @@ export const Plugins = {
'graphql-codegen': graphqlCodegen,
husky,
jest,
karma,
ladle,
lefthook,
'lint-staged': lintStaged,
Expand Down
106 changes: 106 additions & 0 deletions packages/knip/src/plugins/karma/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { IsPluginEnabled, Plugin, ResolveConfig, ResolveEntryPaths } from '../../types/config.js';
import { type Input, toDeferResolveEntry, toDevDependency, toEntry } from '../../util/input.js';
import { isInternal, join } from '../../util/path.js';
import { hasDependency } from '../../util/plugin.js';
import type { Config, ConfigOptions } from './types.js';

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

const title = 'Karma';

const enablers = ['karma'];

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

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

const entry: string[] = [];

type ConfigFile = (config: Config) => void;

const resolveConfig: ResolveConfig<ConfigFile> = async (localConfig, options) => {
const inputs = new Set<Input>();

const config = loadConfig(localConfig);

if (config.frameworks) {
for (const framework of config.frameworks) {
inputs.add(toDevDependency(devDepForFramework(framework)));
}
}
if (config.plugins) {
for (const plugin of config.plugins) {
if (typeof plugin !== 'string') continue;
if (isInternal(plugin)) {
inputs.add(toDeferResolveEntry(plugin));
} else {
inputs.add(toDevDependency(plugin));
}
}
} else {
const karmaPluginDevDeps = Object.keys(options.manifest.devDependencies ?? {}).filter(name =>
name.startsWith('karma-')
);
for (const karmaPluginDevDep of karmaPluginDevDeps) {
inputs.add(toDevDependency(karmaPluginDevDep));
}
}

return Array.from(inputs);
};

const devDepForFramework = (framework: string): string => (framework === 'jasmine' ? 'jasmine-core' : framework);

const resolveEntryPaths: ResolveEntryPaths<ConfigFile> = (localConfig, options) => {
const inputs = new Set<Input>();

const config = loadConfig(localConfig);

const basePath = config.basePath ?? '';
if (config.files) {
for (const fileOrPatternObj of config.files) {
const fileOrPattern = typeof fileOrPatternObj === 'string' ? fileOrPatternObj : fileOrPatternObj.pattern;
inputs.add(toEntry(join(options.configFileDir, basePath, fileOrPattern)));
}
}
if (config.exclude) {
for (const fileOrPattern of config.exclude) {
inputs.add(toEntry(`!${join(options.configFileDir, basePath, fileOrPattern)}`));
}
}

return Array.from(inputs);
};

const loadConfig = (configFile: ConfigFile): ConfigOptions => {
const inMemoryConfig = new InMemoryConfig();
configFile(inMemoryConfig);
return inMemoryConfig.config ?? {};
};

/**
* Dummy configuration class with no default config options
* Relevant config options' defaults are empty, so that's good enough
* Real class: https://github.com/karma-runner/karma/blob/v6.4.4/lib/config.js#L275
*/
class InMemoryConfig implements Config {
config?: ConfigOptions;
/**
* Real method merges configurations with Lodash's `mergeWith`
* https://github.com/karma-runner/karma/blob/v6.4.4/lib/config.js#L343
*/
set(config: ConfigOptions) {
this.config = config;
}
}

export default {
title,
enablers,
isEnabled,
config,
entry,
resolveConfig,
resolveEntryPaths,
} satisfies Plugin;
62 changes: 62 additions & 0 deletions packages/knip/src/plugins/karma/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/6dfee7b81f699209d0b25b06018c633e0c67d38c/types/karma/index.d.ts#L202-L579
// Just relevant parts are copy/pasted because:
// - Some types depend on other deps (`log4js`)
// - Needs manual fixing of some linter issues

//👇 Partial copy/paste with relevant options for Knip
export interface Config {
set: (config: ConfigOptions) => void;
}

//👇 Partial copy/paste with relevant options for Knip
export interface ConfigOptions {
/**
* @default ''
* @description The root path location that will be used to resolve all relative paths defined in <code>files</code> and <code>exclude</code>.
* If the basePath configuration is a relative path then it will be resolved to
* the <code>__dirname</code> of the configuration file.
*/
basePath?: string | undefined;
/**
* @default []
* @description List of files/patterns to exclude from loaded files.
*/
exclude?: string[] | undefined;
/**
* @default []
* @description List of files/patterns to load in the browser.
*/
files?: Array<FilePattern | string> | undefined;
/**
* @default []
* @description List of test frameworks you want to use. Typically, you will set this to ['jasmine'], ['mocha'] or ['qunit']...
* Please note just about all frameworks in Karma require an additional plugin/framework library to be installed (via NPM).
*/
frameworks?: string[] | undefined;
/**
* @default ['karma-*']
* @description List of plugins to load. A plugin can be a string (in which case it will be required
* by Karma) or an inlined plugin - Object.
* By default, Karma loads all sibling NPM modules which have a name starting with karma-*.
* Note: Just about all plugins in Karma require an additional library to be installed (via NPM).
*/
plugins?: Array<PluginName | InlinePluginDef> | undefined;
}

type PluginName = string;
type InlinePluginDef = Record<PluginName, InlinePluginType>;
type InlinePluginType = FactoryFnType | ConstructorFnType | ValueType;
type FactoryFnType = ['factory', FactoryFn];
type FactoryFn = (...params: any[]) => any;
type ConstructorFnType = ['type', ConstructorFn];
// biome-ignore lint/complexity/noBannedTypes: copy/pasted
type ConstructorFn = Function | (new (...params: any[]) => any);
type ValueType = ['value', any];

//👇 Partial extraction of relevant options for Knip
interface FilePattern {
/**
* The pattern to use for matching.
*/
pattern: string;
}
1 change: 1 addition & 0 deletions packages/knip/src/schema/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const pluginsSchema = z.object({
'graphql-codegen': pluginSchema,
husky: pluginSchema,
jest: pluginSchema,
karma: pluginSchema,
ladle: pluginSchema,
lefthook: pluginSchema,
'lint-staged': pluginSchema,
Expand Down
2 changes: 2 additions & 0 deletions packages/knip/src/types/PluginNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type PluginName =
| 'graphql-codegen'
| 'husky'
| 'jest'
| 'karma'
| 'ladle'
| 'lefthook'
| 'lint-staged'
Expand Down Expand Up @@ -111,6 +112,7 @@ export const pluginNames = [
'graphql-codegen',
'husky',
'jest',
'karma',
'ladle',
'lefthook',
'lint-staged',
Expand Down
26 changes: 26 additions & 0 deletions packages/knip/test/plugins/karma.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { test } from 'bun:test';
import assert from 'node:assert/strict';
import { main } from '../../src/index.js';
import { resolve } from '../../src/util/path.js';
import baseArguments from '../helpers/baseArguments.js';
import baseCounters from '../helpers/baseCounters.js';

const cwd = resolve('fixtures/plugins/karma');

test('Find dependencies with the Karma plugin (initial config)', async () => {
const { issues, counters } = await main({
...baseArguments,
cwd,
});

assert(issues.unlisted['karma.conf.js']['jasmine-core']);

assert.deepEqual(counters, {
...baseCounters,
//👇 Not 2, as `karma-coverage` should be loaded as plugin by default
devDependencies: 1,
unlisted: 1,
processed: 1,
total: 1,
});
});
26 changes: 26 additions & 0 deletions packages/knip/test/plugins/karma2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { test } from 'bun:test';
import assert from 'node:assert/strict';
import { main } from '../../src/index.js';
import { join, resolve } from '../../src/util/path.js';
import baseArguments from '../helpers/baseArguments.js';
import baseCounters from '../helpers/baseCounters.js';

const cwd = resolve('fixtures/plugins/karma2');

test('Find dependencies with the Karma plugin (test files)', async () => {
const { issues, counters } = await main({
...baseArguments,
cwd,
});

assert.ok(issues.files.has(join(cwd, 'out-of-base-path', 'example.spec.js')));
assert.ok(issues.files.has(join(cwd, 'src', 'excluded.spec.js')));

assert.deepEqual(counters, {
...baseCounters,
devDependencies: 1,
files: 2,
processed: 5,
total: 5,
});
});
26 changes: 26 additions & 0 deletions packages/knip/test/plugins/karma3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { test } from 'bun:test';
import assert from 'node:assert/strict';
import { main } from '../../src/index.js';
import { resolve } from '../../src/util/path.js';
import baseArguments from '../helpers/baseArguments.js';
import baseCounters from '../helpers/baseCounters.js';

const cwd = resolve('fixtures/plugins/karma3');

test('Find dependencies with the Karma plugin (plugin dependencies)', async () => {
const { issues, counters } = await main({
...baseArguments,
cwd,
});

assert.ok(issues.unlisted['karma.conf.js']['karma-jasmine']);
assert.ok(issues.unlisted['karma.conf.js']['karma-coverage']);

assert.deepEqual(counters, {
...baseCounters,
devDependencies: 1,
unlisted: 2,
processed: 2,
total: 2,
});
});

0 comments on commit 3d29854

Please sign in to comment.