knip 6.6.2 → 6.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,9 +3,13 @@ export declare class ConsoleStreamer {
3
3
  isEnabled: boolean;
4
4
  isWatch: boolean;
5
5
  private lines;
6
+ private stdoutBaseline;
7
+ private stderrBaseline;
6
8
  constructor(options: MainOptions);
7
9
  private clearLines;
8
10
  private clearScreen;
11
+ private snapshot;
12
+ private hadExternalWrites;
9
13
  private update;
10
14
  cast(message: string | string[], sub?: string): void;
11
15
  clear(): void;
@@ -2,9 +2,12 @@ export class ConsoleStreamer {
2
2
  isEnabled = false;
3
3
  isWatch = false;
4
4
  lines = 0;
5
+ stdoutBaseline = 0;
6
+ stderrBaseline = 0;
5
7
  constructor(options) {
6
8
  this.isEnabled = options.isShowProgress;
7
9
  this.isWatch = options.isWatch;
10
+ this.snapshot();
8
11
  }
9
12
  clearLines(count) {
10
13
  if (count > 0) {
@@ -18,6 +21,14 @@ export class ConsoleStreamer {
18
21
  clearScreen() {
19
22
  process.stdout.write('\x1b[2J\x1b[1;1f');
20
23
  }
24
+ snapshot() {
25
+ this.stdoutBaseline = process.stdout.bytesWritten ?? 0;
26
+ this.stderrBaseline = process.stderr.bytesWritten ?? 0;
27
+ }
28
+ hadExternalWrites() {
29
+ return ((process.stdout.bytesWritten ?? 0) > this.stdoutBaseline ||
30
+ (process.stderr.bytesWritten ?? 0) > this.stderrBaseline);
31
+ }
21
32
  update(messages) {
22
33
  this.clear();
23
34
  process.stdout.write(`${messages.join('\n')}\n`);
@@ -26,17 +37,24 @@ export class ConsoleStreamer {
26
37
  cast(message, sub) {
27
38
  if (!this.isEnabled)
28
39
  return;
40
+ if (this.hadExternalWrites())
41
+ this.lines = 0;
29
42
  if (Array.isArray(message))
30
43
  this.update(message);
31
44
  else
32
45
  this.update([`${message}${!sub || sub === '.' ? '' : ` (${sub})`}…`]);
46
+ this.snapshot();
33
47
  }
34
48
  clear() {
35
49
  if (!this.isEnabled)
36
50
  return;
51
+ if (this.hadExternalWrites())
52
+ this.lines = 0;
37
53
  if (this.isWatch)
38
54
  this.clearScreen();
39
55
  else
40
56
  this.clearLines(this.lines);
57
+ this.lines = 0;
58
+ this.snapshot();
41
59
  }
42
60
  }
@@ -5,6 +5,7 @@ import { debugLogArray, debugLogObject } from './util/debug.js';
5
5
  import { load, save } from './util/package-json.js';
6
6
  import { extname, join } from './util/path.js';
7
7
  import { removeExport } from './util/remove-export.js';
8
+ const MODULE_MARKER = /^[ \t]*(import|export)\b/m;
8
9
  export const fix = async (issues, counters, options) => {
9
10
  const fixer = new IssueFixer(options);
10
11
  const touchedFiles = await fixer.fixIssues(issues);
@@ -73,9 +74,13 @@ class IssueFixer {
73
74
  if (fixes.length === 0)
74
75
  continue;
75
76
  const absFilePath = join(this.options.cwd, filePath);
76
- const sourceFileText = fixes
77
+ const originalSource = await readFile(absFilePath, 'utf-8');
78
+ let sourceFileText = fixes
77
79
  .sort((a, b) => b[0] - a[0])
78
- .reduce((text, [start, end, flags]) => removeExport({ text, start, end, flags }), await readFile(absFilePath, 'utf-8'));
80
+ .reduce((text, [start, end, flags]) => removeExport({ text, start, end, flags }), originalSource);
81
+ if (MODULE_MARKER.test(originalSource) && !MODULE_MARKER.test(sourceFileText)) {
82
+ sourceFileText = `${sourceFileText.trimEnd()}\n\nexport {};\n`;
83
+ }
79
84
  await writeFile(absFilePath, sourceFileText);
80
85
  touchedFiles.add(absFilePath);
81
86
  for (const type of types) {
@@ -15,8 +15,10 @@ import { isConfig, isDeferResolve, isDependency, toConfig, toDebugString, toEntr
15
15
  import { getPackageNameFromSpecifier } from './util/modules.js';
16
16
  import { getKeysByValue } from './util/object.js';
17
17
  import { timerify } from './util/Performance.js';
18
- import { basename, dirname, isInternal, join } from './util/path.js';
18
+ import { basename, dirname, isInternal, join, toRelative } from './util/path.js';
19
19
  import { extractPatternExtensions } from './util/pattern-extensions.js';
20
+ import { formatCauseMessage } from './util/errors.js';
21
+ import { logError } from './util/log.js';
20
22
  import { loadConfigForPlugin } from './util/plugin.js';
21
23
  import { ELLIPSIS } from './util/string.js';
22
24
  const nullConfig = { config: null, entry: null, project: null };
@@ -327,15 +329,27 @@ export class WorkspaceWorker {
327
329
  }
328
330
  if (!cache.resolveConfig) {
329
331
  const isLoad = typeof plugin.isLoadConfig === 'function' ? plugin.isLoadConfig(resolveOpts, this.dependencies) : true;
330
- const localConfig = isLoad && (await loadConfigForPlugin(configFilePath, plugin, resolveOpts, pluginName));
331
- if (localConfig) {
332
- const inputs = await plugin.resolveConfig(localConfig, resolveOpts);
333
- if (plugin.isFilterTransitiveDependencies && !isManifest) {
334
- this.filterTransitiveDependencies(inputs, configFilePath);
332
+ if (isLoad) {
333
+ try {
334
+ const localConfig = await loadConfigForPlugin(configFilePath, plugin, resolveOpts, pluginName);
335
+ if (localConfig) {
336
+ const inputs = await plugin.resolveConfig(localConfig, resolveOpts);
337
+ if (plugin.isFilterTransitiveDependencies && !isManifest) {
338
+ this.filterTransitiveDependencies(inputs, configFilePath);
339
+ }
340
+ for (const input of inputs)
341
+ addInput(input, configFilePath);
342
+ cache.resolveConfig = inputs;
343
+ }
344
+ }
345
+ catch (error) {
346
+ if (!(error instanceof Error))
347
+ throw error;
348
+ const relPath = toRelative(configFilePath, this.options.cwd);
349
+ const cause = formatCauseMessage(error, this.options.cwd);
350
+ logError(`Error loading ${relPath} (${cause})`);
351
+ logError('Please fix or visit https://knip.dev/reference/known-issues');
335
352
  }
336
- for (const input of inputs)
337
- addInput(input, configFilePath);
338
- cache.resolveConfig = inputs;
339
353
  }
340
354
  }
341
355
  }
package/dist/cli.js CHANGED
@@ -74,7 +74,8 @@ const main = async () => {
74
74
  }
75
75
  if ((!args['no-exit-code'] && totalErrorCount > Number(args['max-issues'] ?? 0)) ||
76
76
  (!options.isDisableConfigHints && options.isTreatConfigHintsAsErrors && configurationHints.length > 0)) {
77
- process.exit(1);
77
+ process.exitCode = 1;
78
+ return;
78
79
  }
79
80
  }
80
81
  catch (error) {
@@ -82,7 +83,7 @@ const main = async () => {
82
83
  if (!args.debug && error instanceof Error && isKnownError(error)) {
83
84
  const knownErrors = getKnownErrors(error);
84
85
  for (const knownError of knownErrors)
85
- logError('ERROR', knownError.message);
86
+ logError(knownError.message);
86
87
  if (hasErrorCause(knownErrors[0])) {
87
88
  console.error('Reason:', knownErrors[0].cause.message);
88
89
  if (isModuleNotFoundError(knownErrors[0].cause))
@@ -92,10 +93,11 @@ const main = async () => {
92
93
  }
93
94
  if (isConfigurationError(knownErrors[0]))
94
95
  console.log('\nRun `knip --help` or visit https://knip.dev for help');
95
- process.exit(2);
96
+ process.exitCode = 2;
97
+ return;
96
98
  }
97
99
  throw error;
98
100
  }
99
- process.exit(0);
101
+ process.exitCode = 0;
100
102
  };
101
103
  await main();
@@ -1,6 +1,7 @@
1
1
  import { createGraphExplorer } from '../graph-explorer/explorer.js';
2
2
  import { getIssueType, hasStrictlyEnumReferences } from '../graph-explorer/utils.js';
3
3
  import traceReporter from '../reporters/trace.js';
4
+ import { shouldCountRefs } from '../typescript/visitors/helpers.js';
4
5
  import { getPackageNameFromModuleSpecifier } from '../util/modules.js';
5
6
  import { perfObserver } from '../util/Performance.js';
6
7
  import { findMatch } from '../util/regex.js';
@@ -9,6 +10,7 @@ export const analyze = async ({ analyzedFiles, counselor, chief, collector, depu
9
10
  const shouldIgnore = getShouldIgnoreHandler(options.isProduction);
10
11
  const shouldIgnoreTags = getShouldIgnoreTagHandler(options.tags);
11
12
  const explorer = createGraphExplorer(graph, entryPaths);
13
+ const ignoreExportsUsedInFile = chief.config.ignoreExportsUsedInFile;
12
14
  const isReferencedInUsedExport = (exportedItem, filePath, includeEntryExports, visited) => {
13
15
  if (!exportedItem.referencedIn)
14
16
  return false;
@@ -16,13 +18,15 @@ export const analyze = async ({ analyzedFiles, counselor, chief, collector, depu
16
18
  if (!file)
17
19
  return false;
18
20
  for (const containingExport of exportedItem.referencedIn) {
19
- if (explorer.isReferenced(filePath, containingExport, { includeEntryExports })[0])
20
- return true;
21
21
  const inExport = file.exports.get(containingExport);
22
22
  if (!inExport)
23
23
  continue;
24
- if (inExport.hasRefsInFile && (inExport.type === 'type' || inExport.type === 'interface'))
25
- return true;
24
+ if (shouldCountRefs(ignoreExportsUsedInFile, inExport.type)) {
25
+ if (inExport.hasRefsInFile)
26
+ return true;
27
+ if (explorer.isReferenced(filePath, containingExport, { includeEntryExports })[0])
28
+ return true;
29
+ }
26
30
  if (inExport.referencedIn) {
27
31
  const v = visited ?? new Set();
28
32
  if (!v.has(containingExport)) {
@@ -25,11 +25,11 @@ export interface NuxtConfig {
25
25
  css?: string[];
26
26
  alias?: Record<string, string>;
27
27
  }
28
- export interface TemplateExpressionNode {
28
+ interface TemplateExpressionNode {
29
29
  content: string;
30
30
  isStatic: boolean;
31
31
  }
32
- export interface TemplateAstProp {
32
+ interface TemplateAstProp {
33
33
  type: number;
34
34
  exp?: TemplateExpressionNode;
35
35
  arg?: TemplateExpressionNode;
@@ -41,7 +41,7 @@ export interface TemplateAstNode {
41
41
  content?: TemplateExpressionNode;
42
42
  children?: TemplateAstNode[];
43
43
  }
44
- export interface Descriptor {
44
+ interface Descriptor {
45
45
  script: {
46
46
  content: string;
47
47
  } | null;
@@ -58,3 +58,4 @@ export type VueSfc = {
58
58
  descriptor: Descriptor;
59
59
  };
60
60
  };
61
+ export {};
@@ -1,10 +1,11 @@
1
- export type RaycastManifestCommand = {
1
+ type RaycastManifestCommand = {
2
2
  name?: unknown;
3
3
  };
4
- export type RaycastManifestTool = {
4
+ type RaycastManifestTool = {
5
5
  name?: unknown;
6
6
  };
7
7
  export type RaycastManifest = {
8
8
  commands?: RaycastManifestCommand[];
9
9
  tools?: RaycastManifestTool[];
10
10
  };
11
+ export {};
@@ -17,6 +17,7 @@ export const getEnvSpecifier = (env) => {
17
17
  return `vitest-environment-${env}`;
18
18
  };
19
19
  const builtInReporters = [
20
+ 'agent',
20
21
  'basic',
21
22
  'blob',
22
23
  'default',
@@ -26,6 +27,7 @@ const builtInReporters = [
26
27
  'html',
27
28
  'json',
28
29
  'junit',
30
+ 'minimal',
29
31
  'tap',
30
32
  'tap-flat',
31
33
  'tree',
@@ -18,10 +18,12 @@ export default ({ report, issues, cwd }) => {
18
18
  const longestSymbol = issuesForType.sort(sortLongestSymbol)[0].symbol.length;
19
19
  const sortedByFilePath = issuesForType.sort(sortLongestFilePath);
20
20
  const longestFilePath = getFilePath(sortedByFilePath[0]).length;
21
- console.log(`| ${'Name'.padEnd(longestSymbol)} | ${'Location'.padEnd(longestFilePath)} | Severity |`);
22
- console.log(`| :${'-'.repeat(longestSymbol - 1)} | :${'-'.repeat(longestFilePath - 1)} | :------- |`);
21
+ const nameWidth = Math.max(longestSymbol, 'Name'.length);
22
+ const locationWidth = Math.max(longestFilePath, 'Location'.length);
23
+ console.log(`| ${'Name'.padEnd(nameWidth)} | ${'Location'.padEnd(locationWidth)} | Severity |`);
24
+ console.log(`| :${'-'.repeat(nameWidth - 1)} | :${'-'.repeat(locationWidth - 1)} | :------- |`);
23
25
  for (const issue of sortedByFilePath) {
24
- console.log(`| ${issue.symbol.padEnd(longestSymbol)} | ${getFilePath(issue).padEnd(longestFilePath)} | ${(issue.severity ?? '').padEnd(8)} |`);
26
+ console.log(`| ${issue.symbol.padEnd(nameWidth)} | ${getFilePath(issue).padEnd(locationWidth)} | ${(issue.severity ?? '').padEnd(8)} |`);
25
27
  }
26
28
  console.log('');
27
29
  }
@@ -7,7 +7,7 @@ export default ({ graph, explorer, options, workspaceFilePathFilter }) => {
7
7
  if (options.traceDependency) {
8
8
  const pattern = toRegexOrString(options.traceDependency);
9
9
  const toRel = (path) => toRelative(path, options.cwd);
10
- const table = new Table({ truncateStart: ['filePath'] });
10
+ const table = new Table({ truncate: { filePath: 'start' } });
11
11
  const seen = new Set();
12
12
  for (const [packageName, { imports }] of explorer.getDependencyUsage(pattern)) {
13
13
  const filtered = imports.filter(i => workspaceFilePathFilter(i.filePath));
@@ -17,7 +17,7 @@ const getIdentifier = (hint) => {
17
17
  return hint.identifier.toString();
18
18
  };
19
19
  const getTableForHints = (hints) => {
20
- const table = new Table({ truncateStart: ['identifier', 'workspace', 'filePath'] });
20
+ const table = new Table({ truncate: { identifier: 'start', workspace: 'start', filePath: 'start' } });
21
21
  for (const hint of hints) {
22
22
  table.row();
23
23
  table.cell('identifier', getIdentifier(hint));
@@ -41,7 +41,7 @@ const highlightSymbol = (issue) => (_) => {
41
41
  return symbol;
42
42
  };
43
43
  export const getTableForType = (issues, cwd, options = { isUseColors: true }) => {
44
- const table = new Table({ truncateStart: ['filePath'], noTruncate: ['symbolType'] });
44
+ const table = new Table({ truncate: { filePath: 'start', symbolType: 'none' } });
45
45
  for (const issue of issues.sort(sortByPos)) {
46
46
  table.row();
47
47
  const print = options.isUseColors && (issue.isFixed || issue.severity === 'warn') ? dim : plain;
@@ -100,7 +100,7 @@ export type SourceMap = {
100
100
  srcDir: string;
101
101
  outDir: string;
102
102
  };
103
- export interface ResolveSourceMapOptions {
103
+ interface ResolveSourceMapOptions {
104
104
  cwd: string;
105
105
  manifest: Manifest;
106
106
  dependencies: Set<string>;
@@ -109,7 +109,7 @@ export interface ResolveSourceMapOptions {
109
109
  }
110
110
  export type ResolveSourceMap = (options: ResolveSourceMapOptions) => Promise<SourceMap[]> | SourceMap[];
111
111
  export type HandleInput = (input: Input) => string | undefined;
112
- export type RegisterCompilerInput = {
112
+ type RegisterCompilerInput = {
113
113
  extension: string;
114
114
  compiler: CompilerSync;
115
115
  };
@@ -117,7 +117,7 @@ export type RegisterCompiler = (input: RegisterCompilerInput) => void;
117
117
  export type ResolveFromAST = (program: Program, options: PluginOptions & {
118
118
  readFile: (filePath: string) => string;
119
119
  }) => Input[];
120
- export type RegisterCompilersOptions = {
120
+ type RegisterCompilersOptions = {
121
121
  cwd: string;
122
122
  hasDependency: HasDependency;
123
123
  registerCompiler: RegisterCompiler;
@@ -52,7 +52,7 @@ export interface ExportMember extends Position {
52
52
  readonly flags: number;
53
53
  hasRefsInFile: boolean;
54
54
  }
55
- export type ExportMap = Map<Identifier, Export>;
55
+ type ExportMap = Map<Identifier, Export>;
56
56
  export type Imports = Set<Import>;
57
57
  export type FileNode = {
58
58
  imports: {
@@ -63,7 +63,7 @@ export interface WalkState extends WalkContext {
63
63
  addExport: (identifier: string, type: SymbolType, pos: number, members: ExportMember[], fix: Fix, isReExport: boolean, jsDocTags: Set<string>) => void;
64
64
  getFix: (start: number, end: number, flags?: number) => Fix;
65
65
  getTypeFix: (start: number, end: number) => Fix;
66
- collectRefsInType: (node: any, exportName: string, signatureOnly: boolean, inMember?: boolean) => void;
66
+ collectRefsInType: (node: any, exportName: string, signatureOnly: boolean) => void;
67
67
  addRefInExport: (name: string, exportName: string) => void;
68
68
  isInNamespace: (node: Span) => boolean;
69
69
  }
@@ -47,26 +47,17 @@ const _addExport = (identifier, type, pos, members, fix, isReExport, jsDocTags)
47
47
  });
48
48
  }
49
49
  };
50
- const MEMBER_CONTAINERS = new Set([
51
- 'TSPropertySignature',
52
- 'TSMethodSignature',
53
- 'TSIndexSignature',
54
- 'TSCallSignatureDeclaration',
55
- 'TSConstructSignatureDeclaration',
56
- ]);
57
- const _collectRefsInType = (node, exportName, signatureOnly, inMember = false) => {
50
+ const _collectRefsInType = (node, exportName, signatureOnly) => {
58
51
  if (!node || typeof node !== 'object')
59
52
  return;
60
53
  if (node.type === 'TSTypeQuery') {
61
- if (inMember) {
62
- const name = node.exprName.type === 'Identifier' ? node.exprName.name : undefined;
63
- if (name) {
64
- const refs = state.referencedInExport.get(name);
65
- if (refs)
66
- refs.add(exportName);
67
- else
68
- state.referencedInExport.set(name, new Set([exportName]));
69
- }
54
+ const name = node.exprName.type === 'Identifier' ? node.exprName.name : undefined;
55
+ if (name) {
56
+ const refs = state.referencedInExport.get(name);
57
+ if (refs)
58
+ refs.add(exportName);
59
+ else
60
+ state.referencedInExport.set(name, new Set([exportName]));
70
61
  }
71
62
  return;
72
63
  }
@@ -80,7 +71,6 @@ const _collectRefsInType = (node, exportName, signatureOnly, inMember = false) =
80
71
  else
81
72
  state.referencedInExport.set(name, new Set([exportName]));
82
73
  }
83
- const nextInMember = inMember || MEMBER_CONTAINERS.has(node.type);
84
74
  for (const key in node) {
85
75
  if (key === 'type' || key === 'parent')
86
76
  continue;
@@ -88,11 +78,11 @@ const _collectRefsInType = (node, exportName, signatureOnly, inMember = false) =
88
78
  if (Array.isArray(val)) {
89
79
  for (const item of val) {
90
80
  if (item && typeof item === 'object' && item.type)
91
- _collectRefsInType(item, exportName, signatureOnly, nextInMember);
81
+ _collectRefsInType(item, exportName, signatureOnly);
92
82
  }
93
83
  }
94
84
  else if (val && typeof val === 'object' && val.type) {
95
- _collectRefsInType(val, exportName, signatureOnly, nextInMember);
85
+ _collectRefsInType(val, exportName, signatureOnly);
96
86
  }
97
87
  }
98
88
  };
@@ -111,7 +111,7 @@ class Performance {
111
111
  table.cell('sum', stats.sum, twoFixed);
112
112
  table.cell('%', (stats.sum / totalDuration) * 100, v => (typeof v === 'number' ? `${v.toFixed(0)}%` : ''));
113
113
  }
114
- table.sort('sum|desc');
114
+ table.sort('sum', 'desc');
115
115
  return table.toString();
116
116
  }
117
117
  addMemoryMark(label) {
@@ -43,14 +43,14 @@ export const createOptions = async (options) => {
43
43
  if (invalid.length > 0) {
44
44
  loadedConfig[key] = value.filter((v) => validIssueTypes.has(v));
45
45
  for (const name of invalid)
46
- logWarning('WARNING', `Ignored unknown issue type "${name}" in ${key}`);
46
+ logWarning(`Ignored unknown issue type "${name}" in ${key}`);
47
47
  }
48
48
  }
49
49
  else if (typeof value === 'object') {
50
50
  for (const name in value) {
51
51
  if (!validIssueTypes.has(name)) {
52
52
  delete value[name];
53
- logWarning('WARNING', `Ignored unknown issue type "${name}" in ${key}`);
53
+ logWarning(`Ignored unknown issue type "${name}" in ${key}`);
54
54
  }
55
55
  }
56
56
  }
@@ -11,5 +11,6 @@ export declare const hasErrorCause: (error: Error) => error is ErrorWithCause;
11
11
  export declare const isConfigurationError: (error: Error) => error is ConfigurationError;
12
12
  export declare const isModuleNotFoundError: (error: Error) => boolean;
13
13
  export declare const isLoaderError: (error: Error) => error is LoaderError;
14
+ export declare const formatCauseMessage: (error: Error, cwd: string) => string;
14
15
  export declare const getKnownErrors: (error: Error) => Error[];
15
16
  export {};
@@ -9,6 +9,12 @@ export const hasErrorCause = (error) => !isZodErrorLike(error) && error.cause in
9
9
  export const isConfigurationError = (error) => error instanceof ConfigurationError;
10
10
  export const isModuleNotFoundError = (error) => 'code' in error && error.code === 'MODULE_NOT_FOUND';
11
11
  export const isLoaderError = (error) => error instanceof LoaderError;
12
+ export const formatCauseMessage = (error, cwd) => {
13
+ let root = error;
14
+ while (root.cause instanceof Error)
15
+ root = root.cause;
16
+ return root.message.split('\n', 1)[0].replace(`${cwd}/`, '');
17
+ };
12
18
  export const getKnownErrors = (error) => {
13
19
  if (isZodErrorLike(error))
14
20
  return [...error.issues].map(error => {
package/dist/util/fs.js CHANGED
@@ -54,7 +54,7 @@ export const hasFileWithExtension = (cwd, dirName, extensions) => {
54
54
  return false;
55
55
  };
56
56
  export const loadJSON = async (filePath) => {
57
- const contents = await loadFile(filePath);
57
+ const contents = (await loadFile(filePath)).replace(/^/, '');
58
58
  try {
59
59
  return JSON.parse(contents);
60
60
  }
@@ -1,2 +1,2 @@
1
- export declare const logWarning: (prefix: string, message: string) => void;
2
- export declare const logError: (prefix: string, message: string) => void;
1
+ export declare const logWarning: (message: string) => void;
2
+ export declare const logError: (message: string) => void;
package/dist/util/log.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import st from './colors.js';
2
- export const logWarning = (prefix, message) => {
3
- console.warn(`${st.yellow(prefix)}: ${message}`);
2
+ export const logWarning = (message) => {
3
+ console.warn(`${st.yellow('WARNING')}: ${message}`);
4
4
  };
5
- export const logError = (prefix, message) => {
6
- console.error(`${st.red(prefix)}: ${message}`);
5
+ export const logError = (message) => {
6
+ console.error(`${st.red('ERROR')}: ${message}`);
7
7
  };
@@ -1,7 +1,9 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { glob } from 'tinyglobby';
3
3
  import { partition } from './array.js';
4
+ import { debugLog } from './debug.js';
4
5
  import { ConfigurationError } from './errors.js';
6
+ import { logWarning } from './log.js';
5
7
  import { getPackageName } from './package-name.js';
6
8
  import { join } from './path.js';
7
9
  export default async function mapWorkspaces(cwd, workspaces) {
@@ -20,7 +22,7 @@ export default async function mapWorkspaces(cwd, workspaces) {
20
22
  const dir = join(cwd, name);
21
23
  const manifestPath = join(cwd, match);
22
24
  try {
23
- const manifestStr = await readFile(manifestPath, 'utf8');
25
+ const manifestStr = (await readFile(manifestPath, 'utf8')).replace(/^/, '');
24
26
  const manifest = JSON.parse(manifestStr);
25
27
  const pkgName = getPackageName(manifest, dir);
26
28
  const pkg = { dir, name, pkgName, manifestPath, manifestStr, manifest };
@@ -31,8 +33,12 @@ export default async function mapWorkspaces(cwd, workspaces) {
31
33
  throw new ConfigurationError(`Missing package name in ${manifestPath}`);
32
34
  }
33
35
  catch (error) {
34
- if (error?.code === 'ENOENT')
36
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
35
37
  debugLog('*', `Unable to load package.json for ${name}`);
38
+ }
39
+ else if (error instanceof SyntaxError) {
40
+ logWarning(`Skipping workspace ${name}: invalid JSON in ${manifestPath} (${error.message})`);
41
+ }
36
42
  else
37
43
  throw error;
38
44
  }
@@ -1,20 +1,22 @@
1
1
  type Value = string | number | undefined | false | null;
2
+ type TruncateMode = 'start' | 'end' | 'none';
3
+ type SortOrder = 'asc' | 'desc';
2
4
  export declare class Table {
3
5
  private columns;
4
6
  private rows;
5
7
  private header;
6
8
  private maxWidth;
7
- private truncateStart;
8
- private noTruncate;
9
+ private truncateModes;
9
10
  constructor(options?: {
10
11
  maxWidth?: number;
11
12
  header?: boolean;
12
- truncateStart?: string[];
13
- noTruncate?: string[];
13
+ truncate?: Record<string, TruncateMode>;
14
14
  });
15
15
  row(): this;
16
16
  cell(column: string, value: Value, formatter?: (value: Value) => string): this;
17
- sort(column: string): this;
17
+ sort(column: string, order?: SortOrder): this;
18
+ private modeFor;
19
+ private distributeWidths;
18
20
  toCells(): string[][];
19
21
  toRows(): string[];
20
22
  toString(): string;
@@ -1,20 +1,20 @@
1
1
  import { stripVTControlCharacters } from 'node:util';
2
2
  import { pad, truncate, truncateStart } from './string.js';
3
- const DEFAULT_MAX_WIDTH = process.stdout.columns || 120;
4
3
  const MIN_TRUNCATED_WIDTH = 4;
5
4
  const COLUMN_SEPARATOR = ' ';
5
+ const visibleLength = (text) => stripVTControlCharacters(text).length;
6
+ const isPrintable = (value) => typeof value === 'string' || typeof value === 'number';
7
+ const toDisplay = (value) => (isPrintable(value) ? String(value) : '');
6
8
  export class Table {
7
9
  columns = [];
8
10
  rows = [];
9
11
  header;
10
12
  maxWidth;
11
- truncateStart = [];
12
- noTruncate = [];
13
+ truncateModes;
13
14
  constructor(options) {
14
15
  this.header = options?.header ?? false;
15
- this.maxWidth = options?.maxWidth || DEFAULT_MAX_WIDTH;
16
- this.truncateStart = options?.truncateStart || [];
17
- this.noTruncate = options?.noTruncate || [];
16
+ this.maxWidth = options?.maxWidth || process.stdout.columns || 120;
17
+ this.truncateModes = options?.truncate ?? {};
18
18
  }
19
19
  row() {
20
20
  this.rows.push({});
@@ -25,67 +25,103 @@ export class Table {
25
25
  this.columns.push(column);
26
26
  const row = this.rows[this.rows.length - 1];
27
27
  const align = typeof value === 'number' ? 'right' : 'left';
28
- const formatted = formatter ? formatter(value) : undefined;
29
- row[column] = { value, formatted, align };
28
+ const formatted = formatter?.(value);
29
+ const display = formatted ?? toDisplay(value);
30
+ row[column] = { value, formatted, align, width: visibleLength(display) };
30
31
  return this;
31
32
  }
32
- sort(column) {
33
+ sort(column, order = 'asc') {
34
+ const dir = order === 'desc' ? -1 : 1;
33
35
  this.rows.sort((a, b) => {
34
- const [columnName, order] = column.split('|');
35
- const vA = a[columnName].value;
36
- const vB = b[columnName].value;
36
+ const vA = a[column]?.value;
37
+ const vB = b[column]?.value;
37
38
  if (typeof vA === 'string' && typeof vB === 'string')
38
- return (order === 'desc' ? -1 : 1) * vA.localeCompare(vB);
39
+ return dir * vA.localeCompare(vB);
39
40
  if (typeof vA === 'number' && typeof vB === 'number')
40
- return order === 'desc' ? vB - vA : vA - vB;
41
- return !vA ? 1 : !vB ? -1 : 0;
41
+ return dir * (vA - vB);
42
+ return !isPrintable(vA) ? 1 : !isPrintable(vB) ? -1 : 0;
42
43
  });
43
44
  return this;
44
45
  }
46
+ modeFor(column) {
47
+ return this.truncateModes[column] ?? 'end';
48
+ }
49
+ distributeWidths(columns, widths, separatorWidth) {
50
+ const truncatable = columns.filter(col => this.modeFor(col) !== 'none');
51
+ if (truncatable.length === 0)
52
+ return;
53
+ const reserved = columns.filter(col => this.modeFor(col) === 'none').reduce((sum, col) => sum + widths[col], 0);
54
+ const budget = Math.max(0, this.maxWidth - separatorWidth - reserved);
55
+ const original = {};
56
+ for (const col of truncatable)
57
+ original[col] = widths[col];
58
+ const unresolved = new Set(truncatable);
59
+ let remainingBudget = budget;
60
+ let changed = true;
61
+ while (changed && unresolved.size > 0) {
62
+ changed = false;
63
+ const share = Math.floor(remainingBudget / unresolved.size);
64
+ for (const col of unresolved) {
65
+ if (original[col] <= share) {
66
+ widths[col] = original[col];
67
+ remainingBudget -= original[col];
68
+ unresolved.delete(col);
69
+ changed = true;
70
+ }
71
+ }
72
+ }
73
+ if (unresolved.size === 0)
74
+ return;
75
+ const overMin = [...unresolved].reduce((sum, col) => sum + (original[col] - MIN_TRUNCATED_WIDTH), 0);
76
+ const excess = Math.max(0, remainingBudget - unresolved.size * MIN_TRUNCATED_WIDTH);
77
+ let distributed = 0;
78
+ for (const col of unresolved) {
79
+ const share = overMin > 0 ? Math.floor(((original[col] - MIN_TRUNCATED_WIDTH) * excess) / overMin) : 0;
80
+ widths[col] = MIN_TRUNCATED_WIDTH + share;
81
+ distributed += share;
82
+ }
83
+ const leftover = excess - distributed;
84
+ if (leftover > 0)
85
+ widths[[...unresolved][0]] += leftover;
86
+ }
45
87
  toCells() {
46
- const columns = this.columns.filter(col => this.rows.some(row => typeof row[col].value === 'string' || typeof row[col].value === 'number'));
47
- if (this.header) {
88
+ const columns = this.columns.filter(col => this.rows.some(row => isPrintable(row[col]?.value)));
89
+ const rows = [];
90
+ if (this.header && this.rows.length > 0) {
48
91
  const headerRow = {};
49
92
  const linesRow = {};
50
93
  for (const col of columns) {
51
- headerRow[col] = { value: col, align: this.rows[0][col].align === 'right' ? 'center' : 'left' };
52
- linesRow[col] = { value: '', fill: '-' };
94
+ const align = this.rows[0][col]?.align === 'right' ? 'center' : 'left';
95
+ headerRow[col] = { value: col, align, width: col.length };
96
+ linesRow[col] = { value: '', fill: '-', width: 0 };
53
97
  }
54
- this.rows.unshift(linesRow);
55
- this.rows.unshift(headerRow);
98
+ rows.push(headerRow, linesRow);
56
99
  }
57
- const columnWidths = columns.reduce((acc, col) => {
58
- acc[col] = Math.max(...this.rows.map(row => stripVTControlCharacters(row[col]?.formatted ?? String(row[col].value || '')).length));
59
- return acc;
60
- }, {});
61
- const separatorWidth = (columns.length - 1) * COLUMN_SEPARATOR.length;
62
- const totalWidth = Object.values(columnWidths).reduce((sum, width) => sum + width, 0) + separatorWidth;
63
- if (totalWidth > this.maxWidth) {
64
- const reservedWidth = columns
65
- .filter(col => this.noTruncate.includes(col))
66
- .reduce((sum, col) => sum + columnWidths[col], 0);
67
- const truncatableColumns = columns.filter(col => !this.noTruncate.includes(col));
68
- const minWidth = truncatableColumns.length * 4;
69
- const availableWidth = this.maxWidth - separatorWidth - reservedWidth - minWidth;
70
- const truncatableWidth = truncatableColumns.reduce((sum, col) => sum + columnWidths[col], 0) - minWidth;
71
- const reduction = availableWidth / truncatableWidth;
72
- let roundingDiff = availableWidth;
73
- for (const col of truncatableColumns) {
74
- const reducedWidth = MIN_TRUNCATED_WIDTH + Math.floor((columnWidths[col] - MIN_TRUNCATED_WIDTH) * reduction);
75
- columnWidths[col] = reducedWidth;
76
- roundingDiff -= reducedWidth - MIN_TRUNCATED_WIDTH;
77
- }
78
- if (roundingDiff > 0) {
79
- columnWidths[truncatableColumns.length > 0 ? truncatableColumns[0] : columns[0]] += roundingDiff;
100
+ rows.push(...this.rows);
101
+ const columnWidths = {};
102
+ for (const col of columns) {
103
+ let max = 0;
104
+ for (const row of rows) {
105
+ const w = row[col]?.width ?? 0;
106
+ if (w > max)
107
+ max = w;
80
108
  }
109
+ columnWidths[col] = max;
110
+ }
111
+ const separatorWidth = columns.length > 1 ? (columns.length - 1) * COLUMN_SEPARATOR.length : 0;
112
+ const totalWidth = columns.reduce((sum, col) => sum + columnWidths[col], 0) + separatorWidth;
113
+ if (totalWidth > this.maxWidth) {
114
+ this.distributeWidths(columns, columnWidths, separatorWidth);
81
115
  }
82
- return this.rows.map(row => columns.map((col, index) => {
116
+ return rows.map(row => columns.map((col, index) => {
83
117
  const cell = row[col];
84
118
  const width = columnWidths[col];
85
- const fill = cell.fill || ' ';
86
- const padded = pad(String(cell.formatted || cell.value || ''), width, fill, cell.align);
87
- const truncated = this.truncateStart.includes(col) ? truncateStart(padded, width) : truncate(padded, width);
88
- return index === 0 ? truncated : COLUMN_SEPARATOR + truncated;
119
+ const fill = cell?.fill || ' ';
120
+ const display = cell?.formatted ?? toDisplay(cell?.value);
121
+ const padded = pad(display, width, fill, cell?.align);
122
+ const mode = this.modeFor(col);
123
+ const rendered = mode === 'none' ? padded : mode === 'start' ? truncateStart(padded, width) : truncate(padded, width);
124
+ return index === 0 ? rendered : COLUMN_SEPARATOR + rendered;
89
125
  }));
90
126
  }
91
127
  toRows() {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "6.6.2";
1
+ export declare const version = "6.7.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '6.6.2';
1
+ export const version = '6.7.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knip",
3
- "version": "6.6.2",
3
+ "version": "6.7.0",
4
4
  "description": "Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects",
5
5
  "keywords": [
6
6
  "analysis",