Skip to content

Commit c1d7102

Browse files
committed
feat: allow skipping circular import edges
1 parent 57e0d3e commit c1d7102

4 files changed

Lines changed: 139 additions & 13 deletions

File tree

README.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@
9292
dpdm --skip-dynamic-imports circular index.js
9393
```
9494

95+
7. Ignore specified imports when finding circular dependencies:
96+
97+
```bash
98+
dpdm ./src/index.js --skip-imports 'src/a.js:.*' src/c.js:src/d.js
99+
```
100+
95101
### Options
96102

97103
```bash
@@ -132,6 +138,8 @@ Options:
132138
--detect-unused-files-from this file is a glob, used for finding unused files. [string]
133139
--skip-dynamic-imports Skip parse import(...) statement.
134140
[string] [choices: "tree", "circular"]
141+
--skip-imports Skip import edges from circular checks. Values are regexp
142+
ISSUER:DEPENDENCY pairs. [array]
135143
-h, --help Show help [boolean]
136144
```
137145
@@ -170,15 +178,15 @@ parseDependencyTree('./index', {
170178
* the parse options
171179
*/
172180
export interface ParseOptions {
173-
context: string;
174-
extensions: string[];
175-
js: string[];
176-
include: RegExp;
177-
exclude: RegExp;
178-
tsconfig: string | undefined;
179-
onProgress: (event: 'start' | 'end', target: string) => void;
180-
transform: boolean;
181-
skipDynamicImports: boolean;
181+
context: string;
182+
extensions: string[];
183+
js: string[];
184+
include: RegExp;
185+
exclude: RegExp;
186+
tsconfig: string | undefined;
187+
onProgress: (event: 'start' | 'end', target: string) => void;
188+
transform: boolean;
189+
skipDynamicImports: boolean;
182190
}
183191
184192
export enum DependencyKind {

src/bin/dpdm.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,36 @@ import {
2222
prettyWarning,
2323
simpleResolver,
2424
} from '../utils';
25+
import type { SkippedImport } from '../utils';
2526
import { hideBin } from 'yargs/helpers';
2627

28+
function splitSkipImportValue(value: string): string[] {
29+
return value.split(',').filter(Boolean);
30+
}
31+
32+
function normalizeCircularId(context: string, id: string) {
33+
const fullPath = path.isAbsolute(id) ? id : path.resolve(context, id);
34+
return path.relative(context, fullPath);
35+
}
36+
37+
function parseSkipImports(
38+
skipImports: string[],
39+
context: string,
40+
): SkippedImport[] {
41+
return skipImports.map((item) => {
42+
const parts = item.split(':');
43+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
44+
throw new TypeError(
45+
`skip import should be in the format ISSUER:DEPENDENCY`,
46+
);
47+
}
48+
return [
49+
normalizeCircularId(context, parts[0]),
50+
normalizeCircularId(context, parts[1]),
51+
];
52+
});
53+
}
54+
2755
async function main() {
2856
const y = yargs(hideBin(process.argv));
2957
const argv = await y
@@ -116,6 +144,11 @@ async function main() {
116144
choices: ['tree', 'circular'],
117145
desc: 'Skip parse import(...) statement.',
118146
})
147+
.option('skip-imports', {
148+
type: 'string',
149+
array: true,
150+
desc: 'Skip import edges from circular checks. Values are regexp ISSUER:DEPENDENCY pairs.',
151+
})
119152
.alias('h', 'help')
120153
.wrap(Math.min(y.terminalWidth(), 100))
121154
.parseAsync();
@@ -150,6 +183,12 @@ async function main() {
150183
let current = '';
151184

152185
const context = argv.context || process.cwd();
186+
const skippedImports = parseSkipImports(
187+
((argv.skipImports as string[] | undefined) || []).flatMap(
188+
splitSkipImportValue,
189+
),
190+
context,
191+
);
153192

154193
function onProgress(event: 'start' | 'end', target: string) {
155194
switch (event) {
@@ -200,6 +239,7 @@ async function main() {
200239
const circulars = parseCircular(
201240
tree,
202241
argv.skipDynamicImports === 'circular',
242+
skippedImports,
203243
);
204244
if (argv.output) {
205245
await fs.outputJSON(

src/utils.spec.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,15 @@ describe('util', () => {
6161
});
6262

6363
describe('When parsing circular', () => {
64-
function dependencyFactory(id: string): Dependency {
64+
function dependencyFactory(
65+
id: string,
66+
issuer: string = '',
67+
kind = DependencyKind.StaticImport,
68+
): Dependency {
6569
return {
66-
issuer: '',
70+
issuer,
6771
request: '',
68-
kind: DependencyKind.StaticImport,
72+
kind,
6973
id,
7074
};
7175
}
@@ -225,5 +229,62 @@ describe('util', () => {
225229
expect(actual[1]).toMatchObject(['start', 'mid', 'right']);
226230
});
227231
});
232+
233+
describe('When skip imports are specified', () => {
234+
it('Should not skip imports when the skip list is empty', () => {
235+
const tree = {
236+
a: [dependencyFactory('b', 'a')],
237+
b: [dependencyFactory('a', 'b')],
238+
};
239+
240+
expect(parseCircular(tree, false, [])).toEqual([['a', 'b']]);
241+
});
242+
243+
it('Should ignore a matching import edge when parsing circulars', () => {
244+
const tree = {
245+
a: [dependencyFactory('b', 'a')],
246+
b: [dependencyFactory('a', 'b')],
247+
};
248+
249+
expect(parseCircular(tree, false, [['b', 'a']])).toEqual([]);
250+
});
251+
252+
it('Should only ignore the specified import edge', () => {
253+
const tree = {
254+
start: [
255+
dependencyFactory('left', 'start'),
256+
dependencyFactory('right', 'start'),
257+
],
258+
left: [dependencyFactory('start', 'left')],
259+
right: [dependencyFactory('start', 'right')],
260+
};
261+
262+
const actual = parseCircular(tree, false, [['left', 'start']]);
263+
expect(actual).toHaveLength(1);
264+
expect(actual[0]).toMatchObject(['start', 'right']);
265+
});
266+
267+
it('Should support regexp patterns for skipped imports', () => {
268+
const tree = {
269+
'src/a.js': [
270+
dependencyFactory('src/b.js', 'src/a.js'),
271+
dependencyFactory('src/c.js', 'src/a.js'),
272+
],
273+
'src/b.js': [dependencyFactory('src/a.js', 'src/b.js')],
274+
'src/c.js': [dependencyFactory('src/a.js', 'src/c.js')],
275+
};
276+
277+
expect(parseCircular(tree, false, [['src/a.js', '.*']])).toEqual([]);
278+
});
279+
280+
it('Should not match skipped imports by prefix', () => {
281+
const tree = {
282+
a: [dependencyFactory('bc', 'a')],
283+
bc: [dependencyFactory('a', 'bc')],
284+
};
285+
286+
expect(parseCircular(tree, false, [['a', 'b']])).toEqual([['a', 'bc']]);
287+
});
288+
});
228289
});
229290
});

src/utils.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ import { Dependency, DependencyTree, ParseOptions } from './types';
1212

1313
const allBuiltins = new Set(builtinModules);
1414

15+
export type SkippedImport = readonly [string, string];
16+
17+
function createSkippedImportsRegExp(
18+
skipImports: readonly SkippedImport[],
19+
): RegExp {
20+
if (skipImports.length === 0) {
21+
return /$./;
22+
}
23+
return new RegExp(
24+
`^(?:${skipImports.map((item) => item.join(':')).join('|')})$`,
25+
);
26+
}
27+
1528
export const defaultOptions: ParseOptions = {
1629
context: process.cwd(),
1730
extensions: ['', '.ts', '.tsx', '.mjs', '.js', '.jsx', '.json'],
@@ -130,8 +143,10 @@ export function shortenTree(
130143
export function parseCircular(
131144
tree: DependencyTree,
132145
skipDynamicImports: boolean = false,
146+
skipImports: readonly SkippedImport[] = [],
133147
): string[][] {
134148
const circulars: string[][] = [];
149+
const skippedImports = createSkippedImportsRegExp(skipImports);
135150

136151
tree = { ...tree };
137152

@@ -147,7 +162,9 @@ export function parseCircular(
147162
deps.forEach((dep) => {
148163
if (
149164
dep.id &&
150-
(!skipDynamicImports || dep.kind !== DependencyKind.DynamicImport)
165+
(!skipDynamicImports ||
166+
dep.kind !== DependencyKind.DynamicImport) &&
167+
!skippedImports.test(`${dep.issuer}:${dep.id}`)
151168
) {
152169
visit(dep.id, used.slice());
153170
}

0 commit comments

Comments
 (0)