Skip to content

Commit

Permalink
Resolve config file paths and parse recursively
Browse files Browse the repository at this point in the history
  • Loading branch information
webpro committed Oct 16, 2024
1 parent d288779 commit c03f963
Show file tree
Hide file tree
Showing 30 changed files with 166 additions and 158 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"test": "bun run --cwd packages/knip test",
"format": "biome format .",
"lint": "biome lint .",
"ci": "biome ci . && installed-check --no-include-workspace-root --ignore-dev"
"ci": "biome ci . && installed-check --no-include-workspace-root --ignore-dev",
"waitDB": "./wait-for-postgres.sh -h localhost -p 5433 -U dev -r 10"
},
"devDependencies": {
"@biomejs/biome": "1.9.3",
Expand Down
107 changes: 82 additions & 25 deletions packages/knip/src/WorkspaceWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,27 @@ import type {
Configuration,
EnsuredPluginConfiguration,
GetDependenciesFromScriptsP,
Plugin,
WorkspaceConfiguration,
} from './types/config.js';
import type { PackageJson } from './types/package-json.js';
import type { DependencySet } from './types/workspace.js';
import { compact } from './util/array.js';
import { debugLogArray, debugLogObject } from './util/debug.js';
import { isFile } from './util/fs.js';
import { _glob, hasNoProductionSuffix, hasProductionSuffix, negate, prependDirToPattern } from './util/glob.js';
import { getPackageNameFromModuleSpecifier } from './util/modules.js';
import { getKeysByValue } from './util/object.js';
import { basename, dirname, join } from './util/path.js';
import { basename, dirname, isAbsolute, isInternal, join } from './util/path.js';
import { getFinalEntryPaths, loadConfigForPlugin } from './util/plugin.js';
import { type Dependency, isConfigPattern, toDebugString, toEntry } from './util/protocols.js';
import {
type ConfigDependencyW,
type Dependency,
isConfigPattern,
toDebugString,
toDependency,
toEntry,
} from './util/protocols.js';
import { _resolveSync } from './util/resolve.js';

type WorkspaceManagerOptions = {
name: string;
Expand All @@ -27,6 +36,7 @@ type WorkspaceManagerOptions = {
config: WorkspaceConfiguration;
manifest: PackageJson;
dependencies: DependencySet;
workspacePkgNames: DependencySet;
rootIgnore: Configuration['ignore'];
negatedWorkspacePatterns: string[];
ignoredWorkspacePatterns: string[];
Expand All @@ -48,6 +58,13 @@ const initEnabledPluginsMap = () =>
{} as Record<PluginName, boolean>
);

const resolveConfigFilePath = (dependency: ConfigDependencyW) => {
const dir = dirname(dependency.containingFilePath);
const filePath = join(dir, dependency.specifier);
const r = isAbsolute(filePath) && isFile(filePath) ? filePath : _resolveSync(dependency.specifier, dir);
return r;
};

/**
* - Determines enabled plugins
* - Hands out workspace and plugin glob patterns
Expand All @@ -61,6 +78,7 @@ export class WorkspaceWorker {
manifest: PackageJson;
manifestScriptNames: Set<string>;
dependencies: DependencySet;
workspacePkgNames: DependencySet;
isProduction;
isStrict;
rootIgnore: Configuration['ignore'];
Expand All @@ -80,6 +98,7 @@ export class WorkspaceWorker {
config,
manifest,
dependencies,
workspacePkgNames,
isProduction,
isStrict,
rootIgnore,
Expand All @@ -96,6 +115,7 @@ export class WorkspaceWorker {
this.manifest = manifest;
this.manifestScriptNames = new Set(Object.keys(manifest.scripts ?? {}));
this.dependencies = dependencies;
this.workspacePkgNames = workspacePkgNames;
this.isProduction = isProduction;
this.isStrict = isStrict;
this.rootIgnore = rootIgnore;
Expand Down Expand Up @@ -241,7 +261,7 @@ export class WorkspaceWorker {
const pluginDependencies: Dependency[] = [];

const add = (id: Dependency, containingFilePath: string) => {
pluginDependencies.push({ ...id, containingFilePath });
pluginDependencies.push({ ...id, containingFilePath: containingFilePath ?? id.containingFilePath });
};

// Get dependencies from package.json#scripts
Expand All @@ -266,31 +286,55 @@ export class WorkspaceWorker {
const getDependenciesFromScripts: GetDependenciesFromScriptsP = (scripts, options) =>
_getDependenciesFromScripts(scripts, { ...baseOptions, ...options });

const configFiles = new Map<PluginName, Set<string>>();
const configFiles2 = new Map<PluginName, Set<string>>();
const remainingPlugins = new Set(this.enabledPlugins);
const configFiles = new Map<PluginName, Set<ConfigDependencyW>>();

const addC = (pluginName: PluginName, dependency: Dependency) => {
if (!configFiles.has(pluginName)) configFiles.set(pluginName, new Set());
configFiles.get(pluginName)?.add(dependency.specifier);
};
const addC = (pluginName: PluginName, dependency: ConfigDependencyW) => {
const packageName = getPackageNameFromModuleSpecifier(dependency.specifier);

if (packageName && this.workspacePkgNames.has(packageName)) {
if (!configFiles.has(pluginName)) configFiles.set(pluginName, new Set());
configFiles.get(pluginName)?.add(dependency);
add(toEntry(dependency.specifier), dependency.containingFilePath);
pluginDependencies.push({ ...dependency, ...toDependency(dependency.specifier) });
return;
}

if (packageName && this.dependencies.has(packageName)) {
pluginDependencies.push({ ...dependency, ...toDependency(dependency.specifier) });
return;
}

const addC2 = (pluginName: PluginName, dependency: Dependency) => {
if (!configFiles2.has(pluginName)) configFiles2.set(pluginName, new Set());
configFiles2.get(pluginName)?.add(dependency.specifier);
const s = dependency.specifier;
if (isInternal(s)) {
if (!configFiles.has(pluginName)) configFiles.set(pluginName, new Set());
configFiles.get(pluginName)?.add(dependency);
add(toEntry(dependency.specifier), dependency.containingFilePath);
return;
}

const r = resolveConfigFilePath(dependency);
if (r && isInternal(r)) {
if (!configFiles.has(pluginName)) configFiles.set(pluginName, new Set());
configFiles.get(pluginName)?.add(dependency);
add(toEntry(dependency.specifier), dependency.containingFilePath);
return;
}

pluginDependencies.push({ ...dependency, ...toDependency(dependency.specifier) });
};

for (const dependency of [...dependenciesFromManifest, ...dependenciesFromManifest1]) {
if (isConfigPattern(dependency)) {
const pluginName = dependency.pluginName as PluginName;
addC(pluginName, dependency);
add(toEntry(dependency.specifier), manifestPath);
addC(dependency.pluginName, { ...dependency, containingFilePath: manifestPath });
} else {
if (!this.isProduction) add(dependency, manifestPath);
else if (this.isProduction && (dependency.production || has(dependency))) add(dependency, manifestPath);
}
}

const fn = async (pluginName: PluginName, plugin: Plugin, patterns: string[]) => {
const fn = async (pluginName: PluginName, patterns: string[]) => {
const plugin = Plugins[pluginName];
const hasResolveEntryPaths = typeof plugin.resolveEntryPaths === 'function';
const hasResolveConfig = typeof plugin.resolveConfig === 'function';
const shouldRunConfigResolver =
Expand All @@ -309,6 +353,7 @@ export class WorkspaceWorker {
const options = {
...baseOptions,
config: pluginConfig,
configFilePath: manifestPath,
configFileDir: cwd,
configFileName: '',
getDependenciesFromScripts,
Expand All @@ -317,7 +362,12 @@ export class WorkspaceWorker {
const configEntryPaths: Dependency[] = [];

for (const configFilePath of configFilePaths) {
const opts = { ...options, configFileDir: dirname(configFilePath), configFileName: basename(configFilePath) };
const opts = {
...options,
configFilePath,
configFileDir: dirname(configFilePath),
configFileName: basename(configFilePath),
};
if (hasResolveEntryPaths || shouldRunConfigResolver) {
const isManifest = basename(configFilePath) === 'package.json';
const fd = isManifest ? undefined : this.cache.getFileDescriptor(configFilePath);
Expand All @@ -338,7 +388,7 @@ export class WorkspaceWorker {
if (shouldRunConfigResolver) {
const dependencies = (await plugin.resolveConfig?.(config, opts)) ?? [];
for (const id of dependencies) {
if (isConfigPattern(id)) addC2(id.pluginName, id);
if (isConfigPattern(id)) addC(id.pluginName, { ...id, containingFilePath: configFilePath });
add(id, configFilePath);
}
data.resolveConfig = dependencies;
Expand All @@ -358,16 +408,23 @@ export class WorkspaceWorker {
}
};

for (const [pluginName, plugin] of PluginEntries) {
for (const [pluginName] of PluginEntries) {
if (this.enabledPluginsMap[pluginName]) {
const patterns = [...this.getConfigurationFilePatterns(pluginName), ...(configFiles.get(pluginName) ?? [])];
await fn(pluginName, plugin, patterns);
const p = Array.from(configFiles.get(pluginName) ?? []).map(resolveConfigFilePath);
const patterns = [...this.getConfigurationFilePatterns(pluginName), ...compact(p)];
configFiles.delete(pluginName);
await fn(pluginName, patterns);
remainingPlugins.delete(pluginName);
}
}

for (const [pluginName, configFilePaths] of configFiles2.entries()) {
await fn(pluginName, Plugins[pluginName], [...configFilePaths]);
}
do {
for (const [pluginName, dependencies] of configFiles.entries()) {
const patterns = Array.from(dependencies).map(resolveConfigFilePath);
configFiles.delete(pluginName);
await fn(pluginName, compact(patterns));
}
} while (remainingPlugins.size > 0 && configFiles.size > 0);

debugLogArray(name, 'Plugin dependencies', () => compact(pluginDependencies.map(toDebugString)));

Expand Down
2 changes: 1 addition & 1 deletion packages/knip/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export const IGNORED_GLOBAL_BINARIES = new Set([

export const IGNORED_DEPENDENCIES = new Set(['knip', 'typescript']);

export const IGNORED_RUNTIME_DEPENDENCIES = new Set(['bun', 'deno']);
export const IGNORED_RUNTIME_DEPENDENCIES = new Set(['node', 'bun', 'deno']);

export const FOREIGN_FILE_EXTENSIONS = new Set([
'.avif',
Expand Down
5 changes: 4 additions & 1 deletion packages/knip/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export const main = async (unresolvedConfiguration: CommandLineOptions) => {
config,
manifest,
dependencies,
workspacePkgNames: chief.availableWorkspacePkgNames,
isProduction,
isStrict,
rootIgnore: chief.config.ignore,
Expand Down Expand Up @@ -199,7 +200,9 @@ export const main = async (unresolvedConfiguration: CommandLineOptions) => {
} else if (isProductionEntry(dependency)) {
productionEntryFilePatterns.add(isAbsolute(s) ? relative(dir, s) : s);
} else {
const specifierFilePath = handleReferencedDependency(dependency, workspace);
const ws =
(dependency.containingFilePath && chief.findWorkspaceByFilePath(dependency.containingFilePath)) || workspace;
const specifierFilePath = handleReferencedDependency(dependency, ws);
if (specifierFilePath) principal.addEntryPath(specifierFilePath);
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/knip/src/plugins/astro/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ const entry = ['astro.config.{js,cjs,mjs,ts}', 'src/content/config.ts'];
const production = ['src/pages/**/*.{astro,mdx,js,ts}', 'src/content/**/*.mdx'];

const resolve: Resolve = options => {
const { manifest } = options;
const { manifest, isProduction } = options;
const dependencies = [];

if (
!isProduction &&
manifest.scripts &&
Object.values(manifest.scripts).some(script => /(?<=^|\s)astro(\s|\s.+\s)check(?=\s|$)/.test(script))
) {
Expand Down
53 changes: 10 additions & 43 deletions packages/knip/src/plugins/eslint/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import type { PluginOptions } from '../../types/config.js';
import { compact } from '../../util/array.js';
import { getPackageNameFromFilePath, getPackageNameFromModuleSpecifier } from '../../util/modules.js';
import { basename, dirname, isAbsolute, isInternal, toAbsolute } from '../../util/path.js';
import { load, resolve } from '../../util/plugin.js';
import { type Dependency, toDeferResolve, toEntry } from '../../util/protocols.js';
import { isAbsolute, isInternal } from '../../util/path.js';
import { type ConfigDependency, type Dependency, toConfig, toDeferResolve } from '../../util/protocols.js';
import { getDependenciesFromConfig } from '../babel/index.js';
import type { ESLintConfig, OverrideConfig } from './types.js';

const getDependencies = (config: ESLintConfig | OverrideConfig): Dependency[] => {
const extendsSpecifiers = config.extends ? [config.extends].flat().map(resolveExtendSpecifier) : [];
export const getDependencies = (
config: ESLintConfig | OverrideConfig,
options: PluginOptions
): (Dependency | ConfigDependency)[] => {
const extendsSpecifiers = config.extends ? compact([config.extends].flat().map(resolveExtendSpecifier)) : [];
// 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));
const plugins = config.plugins ? config.plugins.map(resolvePluginSpecifier) : [];
const parser = config.parser ?? config.parserOptions?.parser;
const babelDependencies = config.parserOptions?.babelOptions
Expand All @@ -21,44 +23,9 @@ const getDependencies = (config: ESLintConfig | OverrideConfig): Dependency[] =>
const settings = config.settings ? getDependenciesFromSettings(config.settings) : [];
// const rules = getDependenciesFromRules(config.rules); // TODO enable in next major? Unexpected/breaking in certain cases w/ eslint v8
const rules = getDependenciesFromRules({});
const overrides: Dependency[] = config.overrides ? [config.overrides].flat().flatMap(getDependencies) : [];

const overrides = config.overrides ? [config.overrides].flat().flatMap(d => getDependencies(d, options)) : [];
const x = compact([...extendsSpecifiers, ...plugins, parser, ...settings, ...rules]).map(toDeferResolve);

return [...x, ...babelDependencies, ...overrides];
};

type GetDependenciesDeep = (
localConfig: ESLintConfig,
options: PluginOptions,
dependencies?: Set<Dependency>
) => Promise<Set<Dependency>>;

export const getDependenciesDeep: GetDependenciesDeep = async (localConfig, options, dependencies = new Set()) => {
const { configFileDir } = options;
const addAll = (deps: Dependency[] | Set<Dependency>) => {
for (const dependency of deps) dependencies.add(dependency);
};

if (localConfig) {
if (localConfig.extends) {
for (const extend of [localConfig.extends].flat()) {
if (isInternal(extend)) {
const filePath = resolve(toAbsolute(extend, configFileDir), configFileDir);
if (filePath) {
dependencies.add(toEntry(filePath));
const localConfig: ESLintConfig = await load(filePath);
const opts = { ...options, configFileDir: dirname(filePath), configFileName: basename(filePath) };
addAll(await getDependenciesDeep(localConfig, opts, dependencies));
}
}
}
}

addAll(getDependencies(localConfig));
}

return dependencies;
return [...extendConfigs, ...x, ...babelDependencies, ...overrides];
};

const isQualifiedSpecifier = (specifier: string) =>
Expand Down
6 changes: 3 additions & 3 deletions packages/knip/src/plugins/eslint/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IsPluginEnabled, Plugin, ResolveConfig } from '../../types/config.js';
import { hasDependency } from '../../util/plugin.js';
import { getDependenciesDeep } from './helpers.js';
import { getDependencies } from './helpers.js';
import type { ESLintConfig } from './types.js';

// New: https://eslint.org/docs/latest/use/configure/configuration-files
Expand All @@ -24,8 +24,8 @@ const entry = ['eslint.config.{js,cjs,mjs}'];

const config = ['.eslintrc', '.eslintrc.{js,json,cjs}', '.eslintrc.{yml,yaml}', 'package.json'];

const resolveConfig: ResolveConfig<ESLintConfig> = async (localConfig, options) => {
const dependencies = await getDependenciesDeep(localConfig, options);
const resolveConfig: ResolveConfig<ESLintConfig> = (localConfig, options) => {
const dependencies = getDependencies(localConfig, options);
return Array.from(dependencies);
};

Expand Down
Loading

0 comments on commit c03f963

Please sign in to comment.