Skip to content

Commit

Permalink
Add feature to use different working dir using --cwd and glob-based c…
Browse files Browse the repository at this point in the history
…onfig
  • Loading branch information
webpro committed Oct 5, 2022
1 parent 458cb5a commit 36e67c7
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 24 deletions.
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@ This will analyze the project and output unused files, exports, types and duplic

```
❯ npx exportman
exportman --config ./config.js[on]
exportman --config ./config.js[on] [options]
Options:
--config [file] Path of configuration file (JS or JSON),
requires `entryFiles: []` and `filePatterns: []`
--cwd Working directory (default: current working directory)
--onlyFiles Report only unused files
--onlyExports Report only unused exports
--onlyTypes Report only unused types
Expand All @@ -72,8 +73,8 @@ Options:
Examples:
$ exportman --config ./exportman.json
$ exportman --config ./exportman.js --onlyFiles --onlyDuplicates
$ exportman --config ./exportman.json --cwd packages/client
More info: https://github.com/webpro/exportman
```
Expand All @@ -82,6 +83,30 @@ More info: https://github.com/webpro/exportman

### Monorepos

#### Separate packages

In repos with multiple packages, the `--cwd` option comes in handy. With similar package structures, the packages can be
configured conveniently using globs:

```json
{
"packages/*": {
"entryFiles": ["src/index.ts"],
"filePatterns": ["src/**/*.{ts,tsx}", "!**/*.spec.{ts,tsx}"]
}
}
```

Packages can also be explicitly configured per package directory. To scan the packages separately, using the first match
from the configuration file:

```
exportman --cwd packages/client --config exportman.json
exportman --cwd packages/services --config exportman.json
```

#### Connected projects (e.g. using Nx)

A good example of a large project setup is a monorepo, such as created with Nx. Let's take an example project
configuration for an Nx project using Next.js, Jest and Storybook:

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@
},
"license": "ISC",
"dependencies": {
"micromatch": "4.0.5",
"ts-morph": "16.0.0",
"ts-morph-helpers": "0.3.0"
},
"devDependencies": {
"@types/micromatch": "4.0.2",
"@types/node": "18.8.1",
"release-it": "15.5.0",
"tsx": "3.9.0",
Expand Down
18 changes: 12 additions & 6 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
import path from 'node:path';
import { parseArgs } from 'node:util';
import { printHelp } from './help';
import { resolveConfig } from './util';
import { logIssueGroupResult } from './log';
import { run } from '.';
import type { Configuration } from './types';
import type { ImportedConfiguration } from './types';

const {
positionals: [cwdArg],
values: {
help,
cwd: cwdArg,
config,
onlyFiles,
onlyExports,
Expand All @@ -20,9 +21,9 @@ const {
noProgress = false
}
} = parseArgs({
allowPositionals: true,
options: {
help: { type: 'boolean' },
cwd: { type: 'string' },
config: { type: 'string' },
onlyFiles: { type: 'boolean' },
onlyExports: { type: 'boolean' },
Expand All @@ -35,12 +36,12 @@ const {

if (help || !config) {
printHelp();
process.exit();
process.exit(0);
}

const cwd = cwdArg ? path.resolve(cwdArg) : process.cwd();

const configuration: Configuration = require(path.resolve(config));
const configuration: ImportedConfiguration = require(path.resolve(config));

const isShowProgress = !noProgress || !process.stdout.isTTY;
const isFindAll = !onlyFiles && !onlyExports && !onlyTypes && !onlyDuplicates;
Expand All @@ -50,8 +51,13 @@ const isFindUnusedTypes = onlyTypes === true || isFindAll;
const isFindDuplicateExports = onlyDuplicates === true || isFindAll;

const main = async () => {
const config = resolveConfig(configuration, cwdArg);
if (!config) {
printHelp();
process.exit(1);
}
const issues = await run({
...configuration,
...config,
cwd,
isShowProgress,
isFindUnusedFiles,
Expand Down
5 changes: 3 additions & 2 deletions src/help.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export const printHelp = () => {
console.log(`exportman --config ./config.js[on]
console.log(`exportman --config ./config.js[on] [options]
Options:
--config [file] Path of configuration file (JS or JSON),
requires \`entryFiles: []\` and \`filePatterns: []\`
--cwd Working directory (default: current working directory)
--onlyFiles Report only unused files
--onlyExports Report only unused exports
--onlyTypes Report only unused types
Expand All @@ -14,8 +15,8 @@ Options:
Examples:
$ exportman --config ./exportman.json
$ exportman --config ./exportman.js --onlyFiles --onlyDuplicates
$ exportman --config ./exportman.json --cwd packages/client
More info: https://github.com/webpro/exportman`);
};
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ export async function run(configuration: Configuration) {
} = configuration;

// Create workspace for entry files + resolved dependencies
const production = createProject(cwd, configuration.entryFiles);
const production = await createProject(cwd, configuration.entryFiles);
const entryFiles = production.getSourceFiles();
production.resolveSourceFileDependencies();
const productionFiles = production.getSourceFiles();

// Create workspace for the entire project
const project = createProject(cwd, configuration.filePatterns);
const project = await createProject(cwd, configuration.filePatterns);
const projectFiles = project.getSourceFiles();

// Slice & dice used & unused files
Expand Down
11 changes: 7 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { SourceFile } from 'ts-morph';

export type Configuration = {
cwd: string;
type LocalConfiguration = {
entryFiles: string[];
filePatterns: string[];
};

export type Configuration = LocalConfiguration & {
cwd: string;
isShowProgress: boolean;
isFindUnusedFiles?: boolean;
isFindUnusedExports?: boolean;
Expand All @@ -12,6 +13,8 @@ export type Configuration = {
isFollowSymbols?: boolean;
};

export type ImportedConfiguration = LocalConfiguration | Record<string, LocalConfiguration>;

type FilePath = string;
type Type = 'type' | 'interface' | 'enum';

Expand Down
47 changes: 44 additions & 3 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,55 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { ts, Project } from 'ts-morph';
import micromatch from 'micromatch';
import type { SourceFile, ExportedDeclarations } from 'ts-morph';
import type { ImportedConfiguration, Configuration } from './types';

export const createProject = (cwd: string, paths?: string | string[]) => {
export const resolveConfig = (importedConfiguration: ImportedConfiguration, cwdArg?: string) => {
if (cwdArg && !('filePatterns' in importedConfiguration)) {
const importedConfigKey = Object.keys(importedConfiguration).find(pattern => micromatch.isMatch(cwdArg, pattern));
if (importedConfigKey) {
return importedConfiguration[importedConfigKey];
}
}
if (!cwdArg && !('filePatterns' in importedConfiguration)) {
console.error('Unable to find `filePatterns` in configuration.');
console.info('Add it at root level, or use the --cwd argument with a matching configuration.\n');
return;
}
return importedConfiguration as Configuration;
};

const isFile = async (filePath: string) => {
try {
const stats = await fs.stat(filePath);
return stats.isFile();
} catch {
return false;
}
};

const findFile = async (cwd: string, fileName: string): Promise<string> => {
const filePath = path.join(cwd, fileName);
if (await isFile(filePath)) return filePath;
return findFile(path.resolve(cwd, '..'), fileName);
};

export const resolvePaths = (cwd: string, patterns: string | string[]) => {
return [patterns].flat().map(pattern => {
if (pattern.startsWith('!')) return '!' + path.join(cwd, pattern.slice(1));
return path.join(cwd, pattern);
});
};

export const createProject = async (cwd: string, paths?: string | string[]) => {
const tsConfigFilePath = await findFile(cwd, 'tsconfig.json');
const workspace = new Project({
tsConfigFilePath: path.join(cwd, 'tsconfig.json'),
tsConfigFilePath,
skipAddingFilesFromTsConfig: true,
skipFileDependencyResolution: true
});
if (paths) workspace.addSourceFilesAtPaths(paths);
if (paths) workspace.addSourceFilesAtPaths(resolvePaths(cwd, paths));
return workspace;
};

Expand Down
7 changes: 2 additions & 5 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import test from 'node:test';
import assert from 'node:assert';
import { run } from '../src/index';
import { SourceFile } from 'ts-morph';

const match = (sourceFile: SourceFile, filePath: string) => filePath.endsWith(filePath);

test('run', async () => {
const issues = await run({
cwd: 'test/fixtures/basic',
entryFiles: ['test/fixtures/basic/index.ts'],
filePatterns: ['test/fixtures/basic/*.ts'],
entryFiles: ['index.ts'],
filePatterns: ['*.ts'],
isShowProgress: false,
isFindUnusedFiles: true,
isFindUnusedExports: true,
Expand Down
20 changes: 20 additions & 0 deletions test/resolveConfig.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { resolveConfig } from '../src/util';

const baseConfig = { entryFiles: ['index.ts'], filePatterns: ['*.ts'] };

test('resolveConfig (default)', async () => {
const config = resolveConfig({ ...baseConfig });
assert.deepEqual(config, { entryFiles: ['index.ts'], filePatterns: ['*.ts'] });
});

test('resolveConfig (static)', async () => {
const config = resolveConfig({ workspace: { ...baseConfig } }, 'workspace');
assert.deepEqual(config, { entryFiles: ['index.ts'], filePatterns: ['*.ts'] });
});

test('resolveConfig (dynamic)', async () => {
const config = resolveConfig({ 'packages/*': { ...baseConfig } }, 'packages/a');
assert.deepEqual(config, { entryFiles: ['index.ts'], filePatterns: ['*.ts'] });
});

0 comments on commit 36e67c7

Please sign in to comment.