From b7f1874aa22c78a7d23eb8f5a35cf8e9e09ed39f Mon Sep 17 00:00:00 2001 From: Lars Kappert Date: Wed, 16 Oct 2024 20:44:02 +0200 Subject: [PATCH] Resolve config file paths and parse recursively --- package.json | 3 +- packages/knip/src/WorkspaceWorker.ts | 107 ++++++++++++++---- packages/knip/src/constants.ts | 2 +- packages/knip/src/index.ts | 5 +- packages/knip/src/plugins/astro/index.ts | 3 +- packages/knip/src/plugins/eslint/helpers.ts | 53 ++------- packages/knip/src/plugins/eslint/index.ts | 6 +- packages/knip/src/plugins/typescript/index.ts | 43 ++----- packages/knip/src/plugins/xo/index.ts | 4 +- packages/knip/src/types/config.ts | 1 + .../src/util/handle-referenced-dependency.ts | 56 +++++---- packages/knip/src/util/plugin.ts | 1 - packages/knip/src/util/protocols.ts | 8 +- packages/knip/test/npm-scripts.test.ts | 1 + packages/knip/test/plugins/angular2.test.ts | 5 +- packages/knip/test/plugins/babel.test.ts | 2 +- packages/knip/test/plugins/cucumber.test.ts | 1 - .../plugins/cypress-multi-reporter.test.ts | 1 - packages/knip/test/plugins/eslint.test.ts | 6 +- packages/knip/test/plugins/gatsby.test.ts | 1 - packages/knip/test/plugins/jest.test.ts | 2 +- packages/knip/test/plugins/linthtml.test.ts | 1 - packages/knip/test/plugins/nest.test.ts | 1 - packages/knip/test/plugins/storybook.test.ts | 1 - packages/knip/test/plugins/typescript.test.ts | 6 +- packages/knip/test/plugins/vike.test.ts | 1 - .../knip/test/plugins/vue-webpack.test.ts | 1 - packages/knip/test/plugins/webpack.test.ts | 1 - .../test/workspaces-plugin-config.test.ts | 1 - packages/knip/test/workspaces-tooling.test.ts | 1 - 30 files changed, 167 insertions(+), 158 deletions(-) diff --git a/package.json b/package.json index 692afe2cf..b2bd205cf 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/knip/src/WorkspaceWorker.ts b/packages/knip/src/WorkspaceWorker.ts index 3ee8ed7ab..6c6ca473e 100644 --- a/packages/knip/src/WorkspaceWorker.ts +++ b/packages/knip/src/WorkspaceWorker.ts @@ -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; @@ -27,6 +36,7 @@ type WorkspaceManagerOptions = { config: WorkspaceConfiguration; manifest: PackageJson; dependencies: DependencySet; + workspacePkgNames: DependencySet; rootIgnore: Configuration['ignore']; negatedWorkspacePatterns: string[]; ignoredWorkspacePatterns: string[]; @@ -48,6 +58,13 @@ const initEnabledPluginsMap = () => {} as Record ); +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 @@ -61,6 +78,7 @@ export class WorkspaceWorker { manifest: PackageJson; manifestScriptNames: Set; dependencies: DependencySet; + workspacePkgNames: DependencySet; isProduction; isStrict; rootIgnore: Configuration['ignore']; @@ -80,6 +98,7 @@ export class WorkspaceWorker { config, manifest, dependencies, + workspacePkgNames, isProduction, isStrict, rootIgnore, @@ -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; @@ -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 @@ -266,31 +286,55 @@ export class WorkspaceWorker { const getDependenciesFromScripts: GetDependenciesFromScriptsP = (scripts, options) => _getDependenciesFromScripts(scripts, { ...baseOptions, ...options }); - const configFiles = new Map>(); - const configFiles2 = new Map>(); + const remainingPlugins = new Set(this.enabledPlugins); + const configFiles = new Map>(); - 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 = @@ -309,6 +353,7 @@ export class WorkspaceWorker { const options = { ...baseOptions, config: pluginConfig, + configFilePath: manifestPath, configFileDir: cwd, configFileName: '', getDependenciesFromScripts, @@ -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); @@ -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; @@ -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))); diff --git a/packages/knip/src/constants.ts b/packages/knip/src/constants.ts index b53640f34..aa7684eb2 100644 --- a/packages/knip/src/constants.ts +++ b/packages/knip/src/constants.ts @@ -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', diff --git a/packages/knip/src/index.ts b/packages/knip/src/index.ts index cc27be7bb..e150a6733 100644 --- a/packages/knip/src/index.ts +++ b/packages/knip/src/index.ts @@ -140,6 +140,7 @@ export const main = async (unresolvedConfiguration: CommandLineOptions) => { config, manifest, dependencies, + workspacePkgNames: chief.availableWorkspacePkgNames, isProduction, isStrict, rootIgnore: chief.config.ignore, @@ -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); } } diff --git a/packages/knip/src/plugins/astro/index.ts b/packages/knip/src/plugins/astro/index.ts index cea65ec58..d8c2b041a 100644 --- a/packages/knip/src/plugins/astro/index.ts +++ b/packages/knip/src/plugins/astro/index.ts @@ -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)) ) { diff --git a/packages/knip/src/plugins/eslint/helpers.ts b/packages/knip/src/plugins/eslint/helpers.ts index f4be1a91b..c70402376 100644 --- a/packages/knip/src/plugins/eslint/helpers.ts +++ b/packages/knip/src/plugins/eslint/helpers.ts @@ -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 @@ -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 -) => Promise>; - -export const getDependenciesDeep: GetDependenciesDeep = async (localConfig, options, dependencies = new Set()) => { - const { configFileDir } = options; - const addAll = (deps: Dependency[] | Set) => { - 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) => diff --git a/packages/knip/src/plugins/eslint/index.ts b/packages/knip/src/plugins/eslint/index.ts index 5eab60698..06ae20563 100644 --- a/packages/knip/src/plugins/eslint/index.ts +++ b/packages/knip/src/plugins/eslint/index.ts @@ -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 @@ -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 = async (localConfig, options) => { - const dependencies = await getDependenciesDeep(localConfig, options); +const resolveConfig: ResolveConfig = (localConfig, options) => { + const dependencies = getDependencies(localConfig, options); return Array.from(dependencies); }; diff --git a/packages/knip/src/plugins/typescript/index.ts b/packages/knip/src/plugins/typescript/index.ts index dbc9ca4df..bef626925 100644 --- a/packages/knip/src/plugins/typescript/index.ts +++ b/packages/knip/src/plugins/typescript/index.ts @@ -1,10 +1,8 @@ import type { TsConfigJson } from 'type-fest'; import type { IsPluginEnabled, Plugin, ResolveConfig } from '../../types/config.js'; import { compact } from '../../util/array.js'; -import { dirname, isInternal, join, toAbsolute } from '../../util/path.js'; -import { hasDependency, loadJSON } from '../../util/plugin.js'; -import { type Dependency, toConfig, toDependency, toProductionDependency } from '../../util/protocols.js'; -import { loadTSConfig } from '../../util/tsconfig-loader.js'; +import { hasDependency } from '../../util/plugin.js'; +import { toConfig, toDeferResolve, toProductionDependency } from '../../util/protocols.js'; // https://www.typescriptlang.org/tsconfig @@ -18,37 +16,14 @@ const config = ['tsconfig.json']; const production: string[] = []; -const getExtends = async (configFilePath: string, extendSet = new Set()) => { - const filePath = configFilePath.replace(/(\.json)?$/, '.json'); - const localConfig: TsConfigJson | undefined = await loadJSON(filePath); +const resolveConfig: ResolveConfig = async localConfig => { + const { compilerOptions } = localConfig; - if (!localConfig) return extendSet; - - const extends_ = localConfig.extends ? [localConfig.extends].flat() : []; - for (const extend of extends_) { - if (isInternal(extend)) { - const presetConfigPath = toAbsolute(extend, dirname(configFilePath)); - await getExtends(presetConfigPath, extendSet); - } - } - - for (const extend of extends_) { - if (isInternal(extend)) extendSet.add(toConfig('typescript', toAbsolute(extend, dirname(configFilePath)))); - else extendSet.add(toDependency(extend)); - } - - return extendSet; -}; - -const resolveConfig: ResolveConfig = async (localConfig, options) => { - const { configFileDir, configFileName } = options; - - const configFilePath = join(configFileDir, configFileName); - const { compilerOptions } = await loadTSConfig(configFilePath); - - const extend = await getExtends(configFilePath); + const extend = localConfig.extends + ? [localConfig.extends].flat().map(specifier => toConfig('typescript', specifier)) + : []; - if (!(compilerOptions && localConfig)) return []; + if (!(compilerOptions && localConfig)) return extend; const jsx = (compilerOptions?.jsxImportSource ? [compilerOptions.jsxImportSource] : []).map(toProductionDependency); @@ -58,7 +33,7 @@ const resolveConfig: ResolveConfig = async (localConfig, options) => { : []; const importHelpers = compilerOptions?.importHelpers ? ['tslib'] : []; - return compact([...extend, ...[...types, ...plugins, ...importHelpers].map(toDependency), ...jsx]); + return compact([...extend, ...[...types, ...plugins, ...importHelpers].map(toDeferResolve), ...jsx]); }; const args = { diff --git a/packages/knip/src/plugins/xo/index.ts b/packages/knip/src/plugins/xo/index.ts index 19f799d21..749c732f2 100644 --- a/packages/knip/src/plugins/xo/index.ts +++ b/packages/knip/src/plugins/xo/index.ts @@ -1,6 +1,6 @@ import type { IsPluginEnabled, Plugin, ResolveConfig } from '../../types/config.js'; import { hasDependency } from '../../util/plugin.js'; -import { getDependenciesDeep } from '../eslint/helpers.js'; +import { getDependencies } from '../eslint/helpers.js'; import type { XOConfig } from './types.js'; // link to xo docs: https://github.com/xojs/xo#config @@ -20,7 +20,7 @@ const config = ['package.json', '.xo-config', '.xo-config.{js,cjs,json}', 'xo.co const entry: string[] = ['.xo-config.{js,cjs}', 'xo.config.{js,cjs}']; const resolveConfig: ResolveConfig = async (config, options) => { - const dependencies = await getDependenciesDeep(config, options); + const dependencies = getDependencies(config, options); return [...dependencies]; }; diff --git a/packages/knip/src/types/config.ts b/packages/knip/src/types/config.ts index f3a07cbae..64ef561bb 100644 --- a/packages/knip/src/types/config.ts +++ b/packages/knip/src/types/config.ts @@ -110,6 +110,7 @@ export interface PluginOptions extends BaseOptions { config: EnsuredPluginConfiguration; configFileDir: string; configFileName: string; + configFilePath: string; isProduction: boolean; enabledPlugins: string[]; getDependenciesFromScripts: GetDependenciesFromScriptsP; diff --git a/packages/knip/src/util/handle-referenced-dependency.ts b/packages/knip/src/util/handle-referenced-dependency.ts index 8999ce672..2315d1ab9 100644 --- a/packages/knip/src/util/handle-referenced-dependency.ts +++ b/packages/knip/src/util/handle-referenced-dependency.ts @@ -1,6 +1,7 @@ import type { ConfigurationChief, Workspace } from '../ConfigurationChief.js'; import type { DependencyDeputy } from '../DependencyDeputy.js'; import type { IssueCollector } from '../IssueCollector.js'; +import { IGNORED_RUNTIME_DEPENDENCIES, ROOT_WORKSPACE_NAME } from '../constants.js'; import { isFile } from './fs.js'; import { getPackageNameFromFilePath, getPackageNameFromModuleSpecifier } from './modules.js'; import { dirname, isAbsolute, isInNodeModules, isInternal, join } from './path.js'; @@ -8,6 +9,7 @@ import { type Dependency, fromBinary, isBinary, + isConfigPattern, isDeferResolve, isDeferResolveEntry, isDependency, @@ -27,7 +29,19 @@ export const getReferencedDependencyHandler = ) => (dependency: Dependency, workspace: Workspace) => { const { specifier, containingFilePath } = dependency; - if (!containingFilePath) throw new Error(`Missing cont for ${specifier}`); + if (!containingFilePath || IGNORED_RUNTIME_DEPENDENCIES.has(specifier)) return; + + if (isConfigPattern(dependency)) { + if (!isInternal(specifier)) { + const packageName = isInNodeModules(specifier) + ? getPackageNameFromFilePath(specifier) + : getPackageNameFromModuleSpecifier(specifier); + const isHandled = packageName && deputy.maybeAddReferencedExternalDependency(workspace, packageName); + // if(isHandled) return; + return; + } + return dependency.specifier; + } if (isBinary(dependency)) { const binaryName = fromBinary(dependency); @@ -44,22 +58,24 @@ export const getReferencedDependencyHandler = return; } - if (isDependency(dependency) && !deputy.isProduction) { - const id = getPackageNameFromModuleSpecifier(specifier); - const isHandled = id && deputy.maybeAddReferencedExternalDependency(workspace, id); - if (!isHandled) - collector.addIssue({ - type: 'unlisted', - filePath: containingFilePath, - workspace: workspace.name, - symbol: specifier, - }); - return; - } + const packageName = isInNodeModules(specifier) + ? getPackageNameFromFilePath(specifier) // Pattern: /abs/path/to/repo/node_modules/package/index.js + : getPackageNameFromModuleSpecifier(specifier); // Patterns: package, @any/package, @local/package, self-ref - if (isProductionDependency(dependency) && deputy.isProduction) { - const id = getPackageNameFromModuleSpecifier(specifier); - const isHandled = id && deputy.maybeAddReferencedExternalDependency(workspace, id); + const isWs = packageName && chief.availableWorkspacePkgNames.has(packageName); + const isInDeps = packageName && deputy._manifests.get(workspace.name)?.allDependencies.has(packageName); + const isInRootDeps = + packageName && + workspace.name !== ROOT_WORKSPACE_NAME && + deputy._manifests.get(ROOT_WORKSPACE_NAME)?.allDependencies.has(packageName); + + // Needs work but cheap early bail-out + if ( + (deputy.isProduction && isProductionDependency(dependency)) || + (!deputy.isProduction && + (isDependency(dependency) || (!deputy.isProduction && (isInDeps || isInRootDeps) && !isWs))) + ) { + const isHandled = packageName && deputy.maybeAddReferencedExternalDependency(workspace, packageName); if (!isHandled) { collector.addIssue({ type: 'unlisted', @@ -71,15 +87,13 @@ export const getReferencedDependencyHandler = return; } + if (deputy.isProduction && !dependency.production) return; + const baseDir = dependency.dir ?? dirname(containingFilePath); const filePath = join(baseDir, specifier); const relPath = isDeferResolveEntry(dependency) && !specifier.startsWith('.') ? `./${specifier}` : specifier; const resolvedFilePath = isAbsolute(filePath) && isFile(filePath) ? filePath : _resolveSync(relPath, baseDir); - const packageName = isInNodeModules(specifier) - ? getPackageNameFromFilePath(specifier) // Pattern: /abs/path/to/repo/node_modules/package/index.js - : getPackageNameFromModuleSpecifier(specifier); // Patterns: package, @any/package, @local/package, self-ref - if (resolvedFilePath) { if (isInternal(resolvedFilePath)) { if (packageName) { @@ -161,6 +175,4 @@ export const getReferencedDependencyHandler = if (isEntry(dependency) || isProductionEntry(dependency)) { return _resolveSync(specifier, dependency.dir ?? dirname(containingFilePath)); } - - // throw new Error(`Unhandled dependency: ${dependency.specifier} ${JSON.stringify(dependency)}`); }; diff --git a/packages/knip/src/util/plugin.ts b/packages/knip/src/util/plugin.ts index 6c5e682ac..b7da94730 100644 --- a/packages/knip/src/util/plugin.ts +++ b/packages/knip/src/util/plugin.ts @@ -1,6 +1,5 @@ export { _loadJSON as loadJSON } from './fs.js'; export { _load as load } from './loader.js'; -export { _resolveSync as resolve } from './resolve.js'; import type { Plugin, PluginOptions, RawPluginConfiguration } from '../types/config.js'; import { arrayify } from './array.js'; import { _load as load } from './loader.js'; diff --git a/packages/knip/src/util/protocols.ts b/packages/knip/src/util/protocols.ts index 9bfcb4d14..85600f32a 100644 --- a/packages/knip/src/util/protocols.ts +++ b/packages/knip/src/util/protocols.ts @@ -1,4 +1,5 @@ import type { PluginName } from '../types/PluginNames.js'; +import { toRelative } from './path.js'; type Type = 'binary' | 'entry' | 'config' | 'dependency' | 'deferResolve' | 'deferResolveEntry'; @@ -14,6 +15,10 @@ export interface ConfigDependency extends Dependency { pluginName: PluginName; } +export interface ConfigDependencyW extends ConfigDependency { + containingFilePath: string; +} + type Options = { production?: boolean; dir?: string; @@ -75,4 +80,5 @@ export const toDeferResolveEntry = (specifier: string): Dependency => ({ type: ' export const isDeferResolveEntry = (dependency: Dependency) => dependency.type === 'deferResolveEntry'; -export const toDebugString = (dependency: Dependency) => `${dependency.type}:${dependency.specifier}`; +export const toDebugString = (dependency: Dependency) => + `${dependency.type}:${dependency.specifier} ${dependency.containingFilePath ? `(${toRelative(dependency.containingFilePath)})` : ''}`; diff --git a/packages/knip/test/npm-scripts.test.ts b/packages/knip/test/npm-scripts.test.ts index 55b085502..537df8707 100644 --- a/packages/knip/test/npm-scripts.test.ts +++ b/packages/knip/test/npm-scripts.test.ts @@ -72,6 +72,7 @@ test('Unused dependencies in npm scripts', async () => { devDependencies: 1, binaries: 2, unresolved: 0, + unlisted: 1, processed: 2, total: 2, }); diff --git a/packages/knip/test/plugins/angular2.test.ts b/packages/knip/test/plugins/angular2.test.ts index abddda0a1..b3f51b990 100644 --- a/packages/knip/test/plugins/angular2.test.ts +++ b/packages/knip/test/plugins/angular2.test.ts @@ -8,13 +8,16 @@ import baseCounters from '../helpers/baseCounters.js'; const cwd = resolve('fixtures/plugins/angular2'); test('Find dependencies with the Angular plugin (2)', async () => { - const { counters } = await main({ + const { issues, counters } = await main({ ...baseArguments, cwd, }); + assert(issues.unlisted['angular.json']['tsconfig.spec.json']); + assert.deepEqual(counters, { ...baseCounters, + unlisted: 1, processed: 2, total: 2, }); diff --git a/packages/knip/test/plugins/babel.test.ts b/packages/knip/test/plugins/babel.test.ts index 7d0085c83..56469db61 100644 --- a/packages/knip/test/plugins/babel.test.ts +++ b/packages/knip/test/plugins/babel.test.ts @@ -68,7 +68,7 @@ test('Find dependencies with the Babel plugin (1)', async () => { ...baseCounters, devDependencies: 2, unlisted: 39, - unresolved: 16, + unresolved: 4, processed: 3, total: 3, }); diff --git a/packages/knip/test/plugins/cucumber.test.ts b/packages/knip/test/plugins/cucumber.test.ts index 0e1c88091..7e30f1de1 100644 --- a/packages/knip/test/plugins/cucumber.test.ts +++ b/packages/knip/test/plugins/cucumber.test.ts @@ -15,7 +15,6 @@ test('Find dependencies with the cucumber plugin', async () => { assert.deepEqual(counters, { ...baseCounters, - unresolved: 1, processed: 2, total: 2, }); diff --git a/packages/knip/test/plugins/cypress-multi-reporter.test.ts b/packages/knip/test/plugins/cypress-multi-reporter.test.ts index 5420a52ef..584b749cf 100644 --- a/packages/knip/test/plugins/cypress-multi-reporter.test.ts +++ b/packages/knip/test/plugins/cypress-multi-reporter.test.ts @@ -22,7 +22,6 @@ test('Find dependencies with the cypress-multi-reporter plugin', async () => { ...baseCounters, devDependencies: 0, unlisted: 4, - unresolved: 3, processed: 3, total: 3, }); diff --git a/packages/knip/test/plugins/eslint.test.ts b/packages/knip/test/plugins/eslint.test.ts index 075ae4f11..490e4472d 100644 --- a/packages/knip/test/plugins/eslint.test.ts +++ b/packages/knip/test/plugins/eslint.test.ts @@ -32,21 +32,17 @@ test('Find dependencies with the ESLint plugin', async () => { assert(issues.unlisted['.eslintrc.js']['eslint-plugin-cypress']); assert(issues.unlisted['.eslintrc.js']['eslint-plugin-eslint-comments']); assert(issues.unlisted['.eslintrc.js']['eslint-plugin-eslint-plugin']); - assert(issues.unlisted['.eslintrc.js']['eslint-plugin-import']); assert(issues.unlisted['.eslintrc.js']['@org/eslint-plugin-name/typescript']); - // assert(issues.unlisted['.eslintrc.js']['@other-org/eslint-plugin']); assert(issues.unlisted['.eslintrc.json']['@babel/plugin-syntax-import-assertions']); assert(issues.unlisted['.eslintrc.json']['eslint-config-airbnb']); - assert(issues.unlisted['.eslintrc.json']['eslint-plugin-import']); assert(issues.unlisted['.eslintrc.yml']['@sinonjs/eslint-config']); assert(issues.unlisted['.eslintrc.yml']['@sinonjs/eslint-plugin-no-prototype-methods']); assert.deepEqual(counters, { ...baseCounters, - unlisted: 25, - unresolved: 3, + unlisted: 23, processed: 4, total: 4, }); diff --git a/packages/knip/test/plugins/gatsby.test.ts b/packages/knip/test/plugins/gatsby.test.ts index e1f25d93f..51c41bb3c 100644 --- a/packages/knip/test/plugins/gatsby.test.ts +++ b/packages/knip/test/plugins/gatsby.test.ts @@ -30,7 +30,6 @@ test('Find dependencies with the Gatsby plugin', async () => { binaries: 1, devDependencies: 4, unlisted: 5, - unresolved: 7, processed: 2, total: 2, }); diff --git a/packages/knip/test/plugins/jest.test.ts b/packages/knip/test/plugins/jest.test.ts index dcb34a53e..0ed31dc82 100644 --- a/packages/knip/test/plugins/jest.test.ts +++ b/packages/knip/test/plugins/jest.test.ts @@ -30,7 +30,7 @@ test('Find dependencies with the Jest plugin', async () => { ...baseCounters, devDependencies: 1, unlisted: 10, - unresolved: 2, + unresolved: 1, processed: 6, total: 6, }); diff --git a/packages/knip/test/plugins/linthtml.test.ts b/packages/knip/test/plugins/linthtml.test.ts index 829038aa8..e673642b2 100644 --- a/packages/knip/test/plugins/linthtml.test.ts +++ b/packages/knip/test/plugins/linthtml.test.ts @@ -15,7 +15,6 @@ test('Find dependencies with the LintHTML plugin', async () => { assert.deepEqual(counters, { ...baseCounters, - unresolved: 1, processed: 1, total: 1, }); diff --git a/packages/knip/test/plugins/nest.test.ts b/packages/knip/test/plugins/nest.test.ts index 093845c87..9c0a127cd 100644 --- a/packages/knip/test/plugins/nest.test.ts +++ b/packages/knip/test/plugins/nest.test.ts @@ -15,7 +15,6 @@ test('Find dependencies with the nest plugin', async () => { assert.deepEqual(counters, { ...baseCounters, - unresolved: 1, processed: 6, total: 6, }); diff --git a/packages/knip/test/plugins/storybook.test.ts b/packages/knip/test/plugins/storybook.test.ts index a7134c514..8ed903082 100644 --- a/packages/knip/test/plugins/storybook.test.ts +++ b/packages/knip/test/plugins/storybook.test.ts @@ -27,7 +27,6 @@ test('Find dependencies with Storybook plugin', async () => { devDependencies: 1, unlisted: 6, binaries: 1, - unresolved: 2, processed: 3, total: 3, }); diff --git a/packages/knip/test/plugins/typescript.test.ts b/packages/knip/test/plugins/typescript.test.ts index 0f6278886..ccb21e857 100644 --- a/packages/knip/test/plugins/typescript.test.ts +++ b/packages/knip/test/plugins/typescript.test.ts @@ -13,11 +13,9 @@ test('Find dependencies with the TypeScript plugin', async () => { cwd, }); - assert(issues.unlisted['tsconfig.json']['@tsconfig/node16/tsconfig.json']); assert(issues.unlisted['tsconfig.json']['typescript-eslint-language-service']); assert(issues.unlisted['tsconfig.json']['ts-graphql-plugin']); assert(issues.unlisted['tsconfig.json']['tslib']); - assert(issues.unlisted['tsconfig.base.json']['@tsconfig/node20/tsconfig.json']); assert(issues.unlisted['tsconfig.ext.json']['@tsconfig/node20/tsconfig.json']); assert(issues.unlisted['tsconfig.jsx-import-source-preact.json']['preact']); assert(issues.unlisted['tsconfig.jsx-import-source-react.json']['vitest/globals']); @@ -25,8 +23,8 @@ test('Find dependencies with the TypeScript plugin', async () => { assert.deepEqual(counters, { ...baseCounters, - binaries: 1, // TODO - unlisted: 9, + binaries: 1, + unlisted: 8, processed: 0, total: 0, }); diff --git a/packages/knip/test/plugins/vike.test.ts b/packages/knip/test/plugins/vike.test.ts index 43c00c79b..ebea25517 100644 --- a/packages/knip/test/plugins/vike.test.ts +++ b/packages/knip/test/plugins/vike.test.ts @@ -15,7 +15,6 @@ test('Find dependencies with the vike plugin', async () => { assert.deepEqual(counters, { ...baseCounters, - unresolved: 1, processed: 22, total: 22, }); diff --git a/packages/knip/test/plugins/vue-webpack.test.ts b/packages/knip/test/plugins/vue-webpack.test.ts index 613a63485..3569b553e 100644 --- a/packages/knip/test/plugins/vue-webpack.test.ts +++ b/packages/knip/test/plugins/vue-webpack.test.ts @@ -15,7 +15,6 @@ test('Support compiler functions in config (vue + webpack)', async () => { assert.deepEqual(counters, { ...baseCounters, - unresolved: 3, processed: 6, total: 6, }); diff --git a/packages/knip/test/plugins/webpack.test.ts b/packages/knip/test/plugins/webpack.test.ts index 107342f8c..adadb43ba 100644 --- a/packages/knip/test/plugins/webpack.test.ts +++ b/packages/knip/test/plugins/webpack.test.ts @@ -24,7 +24,6 @@ test('Find dependencies with Webpack plugin', async () => { files: 2, devDependencies: 1, unlisted: 3, - unresolved: 14, processed: 11, total: 11, }); diff --git a/packages/knip/test/workspaces-plugin-config.test.ts b/packages/knip/test/workspaces-plugin-config.test.ts index e85540066..be44ebaee 100644 --- a/packages/knip/test/workspaces-plugin-config.test.ts +++ b/packages/knip/test/workspaces-plugin-config.test.ts @@ -15,7 +15,6 @@ test('Use root plugin config in workspaces', async () => { assert.deepEqual(counters, { ...baseCounters, - unresolved: 2, total: 26, processed: 26, }); diff --git a/packages/knip/test/workspaces-tooling.test.ts b/packages/knip/test/workspaces-tooling.test.ts index f64f56b0f..704ed6587 100644 --- a/packages/knip/test/workspaces-tooling.test.ts +++ b/packages/knip/test/workspaces-tooling.test.ts @@ -15,7 +15,6 @@ test('Find unused files, dependencies and exports in workspaces with eslint-conf assert.deepEqual(counters, { ...baseCounters, - unresolved: 4, processed: 7, total: 7, });