Skip to content

Commit

Permalink
Add --watch (incl. required refactor)
Browse files Browse the repository at this point in the history
  • Loading branch information
webpro committed Apr 11, 2024
1 parent b890461 commit 7bea7a2
Show file tree
Hide file tree
Showing 13 changed files with 482 additions and 214 deletions.
40 changes: 30 additions & 10 deletions packages/docs/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,16 @@ Total running time: 5s (mem: 631.27MB)
- `median`: the median invocation
- `sum` the accumulated time of all invocations

### `--isolate-workspaces`
This is not yet available in Bun, since it does not support
`performance.timerify`
([GitHub issue](https://github.com/oven-sh/bun/issues/9271)).

By default, Knip optimizes performance by adding eligible workspaces to existing
TypeScript programs, based on the compatibility of their `compilerOptions`. Use
this flag to disable this behavior and create one program per workspace.
### `knip-bun`

You can see the behavior in action in [debug mode][1]. Look for messages like
this:
Run Knip using the Bun runtime (instead of Node.js).

```sh
[*] Installed 4 programs for 18 workspaces
...
[*] Analyzing used resolved files [P1/1] (78)
```shell
knip-bun
```

## Configuration
Expand Down Expand Up @@ -170,10 +167,33 @@ mode][6].

Read more at [Production Mode][5].

### `--isolate-workspaces`

By default, Knip optimizes performance by adding eligible workspaces to existing
TypeScript programs, based on the compatibility of their `compilerOptions`. Use
this flag to disable this behavior and create one program per workspace.

You can see the behavior in action in [debug mode][1]. Look for messages like
this:

```sh
[*] Installed 4 programs for 18 workspaces
...
[*] Analyzing used resolved files [P1/1] (78)
```

### `--fix`

Read more at [auto-fix][7].

### `--watch`

Watch current directory, and update reported issues when a file is modified,
added or deleted.

Watch mode focuses on imports and exports in source files. During watch mode,
changes in `package.json` and/or `node_modules` are not supported.

## Filters

Available [issue types][8] when filtering output using `--include` or
Expand Down
5 changes: 3 additions & 2 deletions packages/knip/src/ConsoleStreamer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ export class ConsoleStreamer {
this.lines = messages.length;
}

cast(message: string) {
cast(message: string | string[]) {
if (!this.isEnabled) return;
this.update([message]);
if (Array.isArray(message)) this.update(message);
else this.update([message]);
}

clear() {
Expand Down
9 changes: 8 additions & 1 deletion packages/knip/src/IssueCollector.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import micromatch from 'micromatch';
import { initCounters, initIssues } from './issues/initializers.js';
import type { ConfigurationHint, Issue, Rules } from './types/issues.js';
import { relative } from './util/path.js';
import { timerify } from './util/Performance.js';
import { relative } from './util/path.js';

type Filters = Partial<{
dir: string;
Expand Down Expand Up @@ -83,6 +83,13 @@ export class IssueCollector {
}
}

purge() {
const unusedFiles = this.issues.files;
this.issues = initIssues();
this.counters = initCounters();
return unusedFiles;
}

getIssues() {
return {
issues: this.issues,
Expand Down
1 change: 1 addition & 0 deletions packages/knip/src/PrincipalFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type PrincipalOptions = {
isGitIgnored: (path: string) => boolean;
isIsolateWorkspaces: boolean;
isSkipLibs: boolean;
isWatch: boolean;
};

const mapToAbsolutePaths = (paths: NonNullable<Paths>, cwd: string): Paths =>
Expand Down
44 changes: 36 additions & 8 deletions packages/knip/src/ProjectPrincipal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export class ProjectPrincipal {
syncCompilers: SyncCompilers;
asyncCompilers: AsyncCompilers;
isSkipLibs: boolean;
isWatch: boolean;

cache: CacheConsultant<SerializableFile>;

Expand All @@ -85,7 +86,7 @@ export class ProjectPrincipal {

findReferences?: ts.LanguageService['findReferences'];

constructor({ compilerOptions, cwd, compilers, isGitIgnored, isSkipLibs }: PrincipalOptions, n: number) {
constructor({ compilerOptions, cwd, compilers, isGitIgnored, isSkipLibs, isWatch }: PrincipalOptions, n: number) {
this.cwd = cwd;

this.isGitIgnored = isGitIgnored;
Expand All @@ -102,6 +103,7 @@ export class ProjectPrincipal {
this.syncCompilers = syncCompilers;
this.asyncCompilers = asyncCompilers;
this.isSkipLibs = isSkipLibs;
this.isWatch = isWatch;

this.cache = new CacheConsultant(`project-${n}`);
}
Expand All @@ -113,6 +115,7 @@ export class ProjectPrincipal {
entryPaths: this.entryPaths,
compilers: [this.syncCompilers, this.asyncCompilers],
isSkipLibs: this.isSkipLibs,
useResolverCache: !this.isWatch,
});

this.backend = {
Expand Down Expand Up @@ -168,9 +171,19 @@ export class ProjectPrincipal {
public addProjectPath(filePath: string) {
if (!isInNodeModules(filePath) && this.hasAcceptedExtension(filePath)) {
this.projectPaths.add(filePath);
this.deletedFiles.delete(filePath);
}
}

// TODO Organize better
deletedFiles = new Set();
public removeProjectPath(filePath: string) {
this.entryPaths.delete(filePath);
this.projectPaths.delete(filePath);
this.invalidateFile(filePath);
this.deletedFiles.add(filePath);
}

public addReferencedDependencies(workspaceName: string, referencedDependencies: ReferencedDependencies) {
for (const referencedDependency of referencedDependencies)
this.referencedDependencies.add([...referencedDependency, workspaceName]);
Expand Down Expand Up @@ -205,6 +218,21 @@ export class ProjectPrincipal {
return Array.from(this.projectPaths).filter(filePath => !sourceFiles.has(filePath));
}

private getResolvedModuleHandler(sourceFile: BoundSourceFile) {
const getResolvedModule = this.backend.program?.getResolvedModule;
const resolver = getResolvedModule
? (specifier: string) => getResolvedModule(sourceFile, specifier, /* mode */ undefined)
: (specifier: string) => sourceFile.resolvedModules?.get(specifier, /* mode */ undefined);
if (!this.isWatch) return resolver;

// TODO It's either this awkward bit in watch mode to handle deleted files, or some large refactoring
return (specifier: string) => {
const m = resolver(specifier);
if (m?.resolvedModule?.resolvedFileName && this.deletedFiles.has(m.resolvedModule.resolvedFileName)) return;
return m;
};
}

public analyzeSourceFile(filePath: string, options: Omit<GetImportsAndExportsOptions, 'skipExports'>) {
const fd = this.cache.getFileDescriptor(filePath);
if (!fd.changed && fd.meta?.data) return deserialize(fd.meta.data);
Expand All @@ -218,14 +246,9 @@ export class ProjectPrincipal {

const skipExports = this.skipExportsAnalysis.has(filePath);

const getResolvedModule: GetResolvedModule = specifier =>
this.backend.program?.getResolvedModule
? this.backend.program.getResolvedModule(sourceFile, specifier, /* mode */ undefined)
: sourceFile.resolvedModules?.get(specifier, /* mode */ undefined);

const { imports, exports, scripts } = _getImportsAndExports(
sourceFile,
getResolvedModule,
this.getResolvedModuleHandler(sourceFile),
this.backend.typeChecker,
{ ...options, skipExports }
);
Expand Down Expand Up @@ -278,6 +301,11 @@ export class ProjectPrincipal {
};
}

invalidateFile(filePath: string) {
this.backend.fileManager.snapshotCache.delete(filePath);
this.backend.fileManager.sourceFileCache.delete(filePath);
}

public resolveModule(specifier: string, filePath: string = specifier) {
return this.backend.resolveModuleNames([specifier], filePath)[0];
}
Expand Down Expand Up @@ -321,7 +349,7 @@ export class ProjectPrincipal {
reconcileCache(serializableMap: SerializableMap) {
for (const filePath in serializableMap) {
const fd = this.cache.getFileDescriptor(filePath);
if (!fd || !fd.meta) continue;
if (!(fd?.meta)) continue;
fd.meta.data = serialize(serializableMap[filePath]);
}
this.cache.reconcile();
Expand Down
18 changes: 10 additions & 8 deletions packages/knip/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import picocolors from 'picocolors';
import prettyMilliseconds from 'pretty-ms';
import { main } from './index.js';
import type { IssueType, ReporterOptions } from './types/issues.js';
import { Performance } from './util/Performance.js';
import { perfObserver } from './util/Performance.js';
import parsedArgValues, { helpText } from './util/cli-arguments.js';
import { getKnownError, hasCause, isConfigurationError, isKnownError } from './util/errors.js';
import { cwd } from './util/path.js';
import './util/register.js';
import { runPreprocessors, runReporters } from './util/reporter.js';
import { splitTags } from './util/tag.js';
import { version } from './version.js';
Expand All @@ -23,7 +22,6 @@ const {
'include-entry-exports': isIncludeEntryExports = false,
'include-libs': isIncludeLibs = false,
'isolate-workspaces': isIsolateWorkspaces = false,
performance: isObservePerf = false,
production: isProduction = false,
'reporter-options': reporterOptions = '',
'preprocessor-options': preprocessorOptions = '',
Expand All @@ -34,6 +32,7 @@ const {
version: isVersion,
'experimental-tags': experimentalTags = [],
tags = [],
watch: isWatch = false,
} = parsedArgValues;

if (isHelp) {
Expand All @@ -50,23 +49,25 @@ const isShowProgress = isNoProgress === false && process.stdout.isTTY && typeof

const run = async () => {
try {
const perfObserver = new Performance(isObservePerf);

const { report, issues, counters, rules, configurationHints } = await main({
cwd,
tsConfigFile: tsConfig,
gitignore: !isNoGitIgnore,
isDebug,
isProduction: isStrict || isProduction,
isStrict,
isShowProgress,
isIncludeEntryExports,
isIncludeLibs,
isIsolateWorkspaces,
isWatch,
tags: tags.length > 0 ? splitTags(tags) : splitTags(experimentalTags),
isFix: isFix || fixTypes.length > 0,
fixTypes: fixTypes.flatMap(type => type.split(',')),
});

if (isWatch) return;

const initialData: ReporterOptions = {
report,
issues,
Expand All @@ -88,11 +89,12 @@ const run = async () => {
.filter(reportGroup => finalData.report[reportGroup] && rules[reportGroup] === 'error')
.reduce((errorCount: number, reportGroup) => errorCount + finalData.counters[reportGroup], 0);

if (isObservePerf) {
if (perfObserver.isEnabled) {
await perfObserver.finalize();
console.log(`\n${perfObserver.getTable()}`);
const mem = Math.round((perfObserver.getMemHeapUsage() / 1024 / 1024) * 100) / 100;
console.log('\nTotal running time:', prettyMilliseconds(perfObserver.getTotalTime()), `(mem: ${mem}MB)`);
const mem = perfObserver.getCurrentMemUsageInMb();
const duration = perfObserver.getCurrentDurationInMs();
console.log('\nTotal running time:', prettyMilliseconds(duration), `(mem: ${mem}MB)`);
perfObserver.reset();
}

Expand Down
Loading

0 comments on commit 7bea7a2

Please sign in to comment.