Skip to content

Commit

Permalink
Improve cross-workspace config & binary handling
Browse files Browse the repository at this point in the history
  • Loading branch information
webpro committed Feb 25, 2025
1 parent b49b753 commit 7ea7c34
Show file tree
Hide file tree
Showing 9 changed files with 43 additions and 36 deletions.
5 changes: 3 additions & 2 deletions packages/knip/src/binaries/bash-parser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import parse, { type Assignment, type ExpansionNode, type Node, type Prefix } from '../../vendor/bash-parser/index.js';
import { Plugins, pluginArgsMap } from '../plugins.js';
import type { GetInputsFromScriptsOptions } from '../types/config.js';
import type { FromArgs, GetInputsFromScriptsOptions } from '../types/config.js';
import { debugLogObject } from '../util/debug.js';
import { type Input, toBinary, toDeferResolve } from '../util/input.js';
import { extractBinary } from '../util/modules.js';
Expand All @@ -26,9 +26,10 @@ export const getDependenciesFromScript = (script: string, options: GetInputsFrom
if (!script) return [];

// Helper for recursive calls
const fromArgs = (args: string[]) => {
const fromArgs: FromArgs = (args, opts): Input[] => {
return getDependenciesFromScript(args.filter(arg => arg !== '--').join(' '), {
...options,
...opts,
knownBinsOnly: false,
});
};
Expand Down
6 changes: 3 additions & 3 deletions packages/knip/src/binaries/package-manager/yarn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { BinaryResolver, BinaryResolverOptions } from '../../types/config.j
import { isBinary, isDependency, toBinary, toDependency } from '../../util/input.js';
import { stripVersionFromSpecifier } from '../../util/modules.js';
import { join } from '../../util/path.js';
import { argsFrom } from '../util.js';

// https://yarnpkg.com/cli

Expand Down Expand Up @@ -76,7 +77,6 @@ export const resolve: BinaryResolver = (_binary, args, options) => {

if ((!dir && manifestScriptNames.has(command)) || commands.includes(command)) return [];

const bin = command === 'exec' ? toBinary(binary) : toBinary(command);
if (dir) Object.assign(bin, { dir });
return [bin];
const opts = dir ? { cwd: dir } : {};
return fromArgs(argsFrom(args, command === 'exec' ? binary : command), opts);
};
42 changes: 22 additions & 20 deletions packages/knip/src/binaries/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { BinaryResolver } from '../types/config.js';
import { compact } from '../util/array.js';
import { toBinary, toConfig, toDeferResolve, toDeferResolveEntry, toEntry } from '../util/input.js';
import { extractBinary } from '../util/modules.js';
import { dirname } from '../util/path.js';
import { resolve as fallbackResolve } from './fallback.js';

const isGlobLikeMatch = /(^!|[*+\\(|{^$])/;
Expand All @@ -17,26 +18,27 @@ export const resolve: BinaryResolver = (binary, _args, options) => {

if (!pluginArgs) return fallbackResolve(binary, _args, options);

const opts = pluginArgs;
const inputOpts = {};
if (options.cwd && dirname(containingFilePath) !== options.cwd) Object.assign(inputOpts, { dir: options.cwd });

const args = typeof opts.args === 'function' ? opts.args(_args) : _args;
const args = typeof pluginArgs.args === 'function' ? pluginArgs.args(_args) : _args;

const parsed = parseArgs(args, {
string: [
...(opts.nodeImportArgs ? ['import'] : []),
...(opts.config === true ? ['config'] : []),
...(opts.string ?? []),
...(pluginArgs.nodeImportArgs ? ['import'] : []),
...(pluginArgs.config === true ? ['config'] : []),
...(pluginArgs.string ?? []),
],
boolean: ['quiet', 'verbose', 'watch', ...(opts.boolean ?? [])],
boolean: ['quiet', 'verbose', 'watch', ...(pluginArgs.boolean ?? [])],
alias: {
...(opts.nodeImportArgs ? nodeLoadersArgs : {}),
...(opts.config === true ? { config: ['c'] } : {}),
...opts.alias,
...(pluginArgs.nodeImportArgs ? nodeLoadersArgs : {}),
...(pluginArgs.config === true ? { config: ['c'] } : {}),
...pluginArgs.alias,
},
});

const positionals = [];
if (opts.positional && parsed._[0]) {
if (pluginArgs.positional && parsed._[0]) {
const id = parsed._[0]; // let's start out safe, but sometimes we'll want more
if (isGlobLike(id)) positionals.push(toEntry(id));
else {
Expand All @@ -46,28 +48,28 @@ export const resolve: BinaryResolver = (binary, _args, options) => {
}

const mapToParsedKey = (id: string) => parsed[id];
const resolved = compact(opts.resolve ? opts.resolve.flatMap(mapToParsedKey) : []);
const resolved = compact(pluginArgs.resolve ? pluginArgs.resolve.flatMap(mapToParsedKey) : []);

const resolvedImports = opts.nodeImportArgs && parsed.import ? [parsed.import].flat() : [];
const resolvedImports = pluginArgs.nodeImportArgs && parsed.import ? [parsed.import].flat() : [];

const resolvedFromArgs =
typeof opts.fromArgs === 'function'
? fromArgs(opts.fromArgs(parsed, args))
: Array.isArray(opts.fromArgs)
? fromArgs(opts.fromArgs.flatMap(mapToParsedKey))
typeof pluginArgs.fromArgs === 'function'
? fromArgs(pluginArgs.fromArgs(parsed, args))
: Array.isArray(pluginArgs.fromArgs)
? fromArgs(pluginArgs.fromArgs.flatMap(mapToParsedKey))
: [];

const config = opts.config === true ? ['config'] : opts.config || [];
const config = pluginArgs.config === true ? ['config'] : pluginArgs.config || [];
const mapToConfigPattern = (value: string | [string, (value: string) => string]) => {
if (typeof value === 'string')
return parsed[value] && pluginName ? [toConfig(pluginName, parsed[value], containingFilePath)] : [];
return parsed[value] && pluginName ? [toConfig(pluginName, parsed[value], inputOpts)] : [];
const [id, fn] = value;
return parsed[id] && pluginName ? [toConfig(pluginName, fn(parsed[id]), containingFilePath)] : [];
return parsed[id] && pluginName ? [toConfig(pluginName, fn(parsed[id]), inputOpts)] : [];
};
const configFilePaths = config.flatMap(mapToConfigPattern);

return [
toBinary(binary),
toBinary(binary, inputOpts),
...positionals,
...resolved.map(toDeferResolve),
...resolvedImports.map(toDeferResolve),
Expand Down
4 changes: 2 additions & 2 deletions packages/knip/src/plugins/angular/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const resolveConfig: ResolveConfig<AngularCLIWorkspaceConfiguration> = async (co
if (typeof packageName === 'string') inputs.add(toDependency(packageName));
if (opts) {
if ('tsConfig' in opts && typeof opts.tsConfig === 'string') {
inputs.add(toConfig('typescript', opts.tsConfig, configFilePath));
inputs.add(toConfig('typescript', opts.tsConfig, { containingFilePath: configFilePath }));
}
}
const defaultEntriesByOption: EntriesByOption = opts ? entriesByOption(opts) : new Map();
Expand Down Expand Up @@ -104,7 +104,7 @@ const resolveConfig: ResolveConfig<AngularCLIWorkspaceConfiguration> = async (co
karma.inputsFromFrameworks(['jasmine']).forEach(inputs.add, inputs);
}
if (karmaConfig && !karma.configFiles.includes(karmaConfig)) {
inputs.add(toConfig('karma', karmaConfig, options.configFilePath));
inputs.add(toConfig('karma', karmaConfig, { containingFilePath: options.configFilePath }));
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion packages/knip/src/plugins/eslint/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export const getDependencies = (
// https://github.com/prettier/eslint-plugin-prettier#recommended-configuration
if (extendsSpecifiers.some(specifier => specifier?.startsWith('eslint-plugin-prettier')))
extendsSpecifiers.push('eslint-config-prettier');
const extendConfigs = extendsSpecifiers.map(specifier => toConfig('eslint', specifier, options.configFilePath));
const extendConfigs = extendsSpecifiers.map(specifier =>
toConfig('eslint', specifier, { containingFilePath: options.configFilePath })
);
const plugins = config.plugins ? config.plugins.map(resolvePluginSpecifier) : [];
const parser = config.parser ?? config.parserOptions?.parser;
const babelDependencies = config.parserOptions?.babelOptions
Expand Down
6 changes: 4 additions & 2 deletions packages/knip/src/plugins/typescript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ const resolveConfig: ResolveConfig<TsConfigJson> = async (localConfig, options)
const { compilerOptions } = localConfig;

const extend = localConfig.extends
? [localConfig.extends].flat().map(specifier => toConfig('typescript', specifier, options.configFilePath))
? [localConfig.extends]
.flat()
.map(specifier => toConfig('typescript', specifier, { containingFilePath: options.configFilePath }))
: [];

const references =
localConfig.references
?.filter(reference => reference.path.endsWith('.json'))
.map(reference => toConfig('typescript', reference.path, options.configFilePath)) ?? [];
.map(reference => toConfig('typescript', reference.path, { containingFilePath: options.configFilePath })) ?? [];

if (!(compilerOptions && localConfig)) return compact([...extend, ...references]);

Expand Down
2 changes: 1 addition & 1 deletion packages/knip/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type GetInputsFromScriptsPartial = (
options?: Partial<GetInputsFromScriptsOptions>
) => Input[];

type FromArgs = (args: string[]) => Input[];
export type FromArgs = (args: string[], options?: Partial<GetInputsFromScriptsOptions>) => Input[];

export interface BinaryResolverOptions extends GetInputsFromScriptsOptions {
fromArgs: FromArgs;
Expand Down
4 changes: 2 additions & 2 deletions packages/knip/src/util/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ export const toProductionEntry = (specifier: string, options: Options = {}): Inp

export const isProductionEntry = (input: Input) => input.type === 'entry' && input.production === true;

export const toConfig = (pluginName: PluginName, specifier: string, containingFilePath?: string): ConfigInput => ({
export const toConfig = (pluginName: PluginName, specifier: string, options: Options = {}): ConfigInput => ({
type: 'config',
specifier,
pluginName,
containingFilePath,
...options,
});

export const isConfig = (input: Input): input is ConfigInput => input.type === 'config';
Expand Down
6 changes: 3 additions & 3 deletions packages/knip/test/util/get-inputs-from-scripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ test('getInputsFromScripts (nx)', () => {
test('getInputsFromScripts (npm)', () => {
t('npm run script', []);
t('npm run publish:latest -- --npm-tag=debug --no-push', []);
t('npm exec -- vitest -c vitest.e2e.config.mts', [toBinary('vitest'), toConfig('vitest', 'vitest.e2e.config.mts', containingFilePath)]);
t('npm exec -- vitest -c vitest.e2e.config.mts', [toBinary('vitest'), toConfig('vitest', 'vitest.e2e.config.mts')]);
});

test('getInputsFromScripts (npx)', () => {
Expand Down Expand Up @@ -267,6 +267,6 @@ test('getInputsFromScripts (ignore parse error)', () => {
});

test('getInputsFromScripts (config)', () => {
t('tsc -p tsconfig.app.json', [toBinary('tsc'), toConfig('typescript', 'tsconfig.app.json', containingFilePath)]);
t('tsup -c tsup.server.json', [toBinary('tsup'), toConfig('tsup', 'tsup.server.json', containingFilePath)]);
t('tsc -p tsconfig.app.json', [toBinary('tsc'), toConfig('typescript', 'tsconfig.app.json')]);
t('tsup -c tsup.server.json', [toBinary('tsup'), toConfig('tsup', 'tsup.server.json')]);
});

0 comments on commit 7ea7c34

Please sign in to comment.