dpdm 4.0.1 → 4.2.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.
@@ -0,0 +1,134 @@
1
+ /*!
2
+ * Copyright 2019 acrazing <joking.young@gmail.com>. All rights reserved.
3
+ * @since 2026-05-09 14:35:00
4
+ */
5
+
6
+ import path from 'path';
7
+ import { DependencyKind } from './consts';
8
+ import { parseDependencyTree } from './parser';
9
+ import {
10
+ groupDependencyTreeByPackage,
11
+ groupEntriesByPackage,
12
+ parseCircular,
13
+ } from './utils';
14
+
15
+ describe('parser', () => {
16
+ const fixture = path.join(__dirname, '../fixtures/parser/monorepo');
17
+
18
+ it('should parse relative entries from a custom cwd', async () => {
19
+ const tree = await parseDependencyTree('packages/shared/src/index.ts', {
20
+ cwd: fixture,
21
+ });
22
+
23
+ expect(tree).toEqual({
24
+ 'packages/shared/src/index.ts': [
25
+ {
26
+ issuer: 'packages/shared/src/index.ts',
27
+ request: './dep',
28
+ kind: DependencyKind.StaticExport,
29
+ id: 'packages/shared/src/dep.ts',
30
+ },
31
+ ],
32
+ 'packages/shared/src/dep.ts': [],
33
+ });
34
+ });
35
+
36
+ it('should parse an absolute entry file path', async () => {
37
+ const tree = await parseDependencyTree(
38
+ path.join(fixture, 'packages/shared/src/index.ts'),
39
+ {
40
+ context: fixture,
41
+ },
42
+ );
43
+
44
+ expect(tree).toEqual({
45
+ 'packages/shared/src/index.ts': [
46
+ {
47
+ issuer: 'packages/shared/src/index.ts',
48
+ request: './dep',
49
+ kind: DependencyKind.StaticExport,
50
+ id: 'packages/shared/src/dep.ts',
51
+ },
52
+ ],
53
+ 'packages/shared/src/dep.ts': [],
54
+ });
55
+ });
56
+
57
+ it('should resolve aliases from an absolute tsconfig path', async () => {
58
+ const tree = await parseDependencyTree(
59
+ path.join(fixture, 'packages/alias-user/src/index.ts'),
60
+ {
61
+ context: fixture,
62
+ tsconfig: path.join(fixture, 'tsconfig.json'),
63
+ },
64
+ );
65
+
66
+ expect(tree['packages/alias-user/src/index.ts']).toEqual([
67
+ {
68
+ issuer: 'packages/alias-user/src/index.ts',
69
+ request: '~/dep',
70
+ kind: DependencyKind.StaticImport,
71
+ id: 'packages/shared/src/dep.ts',
72
+ },
73
+ ]);
74
+ });
75
+
76
+ it('should group dependencies and circulars by package', async () => {
77
+ const tree = await parseDependencyTree(
78
+ ['packages/app/src/index.ts', 'packages/ui/src/index.ts'],
79
+ { cwd: fixture },
80
+ );
81
+ const packageTree = groupDependencyTreeByPackage(tree, fixture);
82
+
83
+ expect(
84
+ groupEntriesByPackage(
85
+ [
86
+ 'packages/app/src/index.ts',
87
+ 'packages/ui/src/index.ts',
88
+ 'packages/app/src/local.ts',
89
+ ],
90
+ fixture,
91
+ ),
92
+ ).toEqual(['@repo/app', '@repo/ui']);
93
+ expect(packageTree).toEqual({
94
+ '@repo/app': [
95
+ {
96
+ issuer: '@repo/app',
97
+ request: '../../shared/src',
98
+ kind: DependencyKind.StaticImport,
99
+ id: '@repo/shared',
100
+ },
101
+ {
102
+ issuer: '@repo/app',
103
+ request: '../../ui/src',
104
+ kind: DependencyKind.StaticImport,
105
+ id: '@repo/ui',
106
+ },
107
+ ],
108
+ '@repo/ui': [
109
+ {
110
+ issuer: '@repo/ui',
111
+ request: '../../shared/src',
112
+ kind: DependencyKind.StaticImport,
113
+ id: '@repo/shared',
114
+ },
115
+ ],
116
+ '@repo/shared': [],
117
+ });
118
+ expect(parseCircular(packageTree)).toEqual([]);
119
+ });
120
+
121
+ it('should detect package-level circular dependencies', async () => {
122
+ const tree = await parseDependencyTree(
123
+ ['packages/cycle-a/src/index.ts', 'packages/cycle-b/src/index.ts'],
124
+ {
125
+ cwd: fixture,
126
+ },
127
+ );
128
+ const packageTree = groupDependencyTreeByPackage(tree, fixture);
129
+
130
+ const circulars = parseCircular(packageTree);
131
+ expect(circulars).toHaveLength(1);
132
+ expect(circulars[0].sort()).toEqual(['@repo/cycle-a', '@repo/cycle-b']);
133
+ });
134
+ });
package/src/parser.ts CHANGED
@@ -134,15 +134,14 @@ export async function parseDependencyTree(
134
134
  if (!Array.isArray(entries)) {
135
135
  entries = [entries];
136
136
  }
137
- const currentDirectory = process.cwd();
138
137
  const output: DependencyTree = {};
139
138
  const fullOptions = normalizeOptions(options);
140
139
  let resolve = simpleResolver;
141
- if (options.tsconfig) {
140
+ if (fullOptions.tsconfig) {
142
141
  const compilerOptions = ts.parseJsonConfigFileContent(
143
- ts.readConfigFile(options.tsconfig, ts.sys.readFile).config,
142
+ ts.readConfigFile(fullOptions.tsconfig, ts.sys.readFile).config,
144
143
  ts.sys,
145
- path.dirname(options.tsconfig),
144
+ path.dirname(fullOptions.tsconfig),
146
145
  ).options;
147
146
 
148
147
  const host = ts.createCompilerHost(compilerOptions);
@@ -170,12 +169,12 @@ export async function parseDependencyTree(
170
169
  }
171
170
  await Promise.all(
172
171
  entries.map((entry) =>
173
- G.glob(entry).then((matches) =>
172
+ G.glob(entry, { cwd: fullOptions.cwd }).then((matches) =>
174
173
  Promise.all(
175
174
  matches.map((filename) =>
176
175
  parseTreeRecursive(
177
- currentDirectory,
178
- path.join(currentDirectory, filename),
176
+ fullOptions.cwd,
177
+ path.resolve(fullOptions.cwd, filename),
179
178
  fullOptions,
180
179
  output,
181
180
  resolve,
package/src/types.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import { DependencyKind } from './consts';
7
7
 
8
8
  export interface ParseOptions {
9
+ cwd: string;
9
10
  context: string;
10
11
  extensions: string[];
11
12
  js: string[];
package/src/utils.spec.ts CHANGED
@@ -61,11 +61,15 @@ describe('util', () => {
61
61
  });
62
62
 
63
63
  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 {
65
69
  return {
66
- issuer: '',
70
+ issuer,
67
71
  request: '',
68
- kind: DependencyKind.StaticImport,
72
+ kind,
69
73
  id,
70
74
  };
71
75
  }
@@ -225,5 +229,62 @@ describe('util', () => {
225
229
  expect(actual[1]).toMatchObject(['start', 'mid', 'right']);
226
230
  });
227
231
  });
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
+ });
228
289
  });
229
290
  });
package/src/utils.ts CHANGED
@@ -12,7 +12,21 @@ import { Dependency, DependencyTree, ParseOptions } from './types';
12
12
 
13
13
  const allBuiltins = new Set(builtinModules);
14
14
 
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
+
15
28
  export const defaultOptions: ParseOptions = {
29
+ cwd: process.cwd(),
16
30
  context: process.cwd(),
17
31
  extensions: ['', '.ts', '.tsx', '.mjs', '.js', '.jsx', '.json'],
18
32
  js: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
@@ -26,27 +40,29 @@ export const defaultOptions: ParseOptions = {
26
40
 
27
41
  export function normalizeOptions(options: Partial<ParseOptions>): ParseOptions {
28
42
  const newOptions = { ...defaultOptions, ...options };
43
+ newOptions.cwd = path.resolve(options.cwd || process.cwd());
44
+ newOptions.context = path.resolve(newOptions.cwd, options.context || '.');
29
45
  if (newOptions.extensions.indexOf('') < 0) {
30
46
  newOptions.extensions.unshift('');
31
47
  }
32
- newOptions.context = path.resolve(newOptions.context);
33
48
  if (options.tsconfig === void 0) {
34
49
  try {
35
50
  const tsconfig = path.join(newOptions.context, 'tsconfig.json');
36
51
  const stat = fs.statSync(tsconfig);
37
52
  if (stat.isFile()) {
38
- options.tsconfig = tsconfig;
53
+ newOptions.tsconfig = tsconfig;
39
54
  }
40
55
  } catch {}
41
56
  } else {
57
+ const tsconfig = path.resolve(newOptions.cwd, options.tsconfig);
42
58
  let stat: fs.Stats | undefined;
43
59
  try {
44
- stat = fs.statSync(options.tsconfig);
60
+ stat = fs.statSync(tsconfig);
45
61
  } catch {}
46
62
  if (!stat || !stat.isFile()) {
47
63
  throw new Error(`specified tsconfig "${options.tsconfig}" is not a file`);
48
64
  }
49
- options.tsconfig = path.join(process.cwd(), options.tsconfig);
65
+ newOptions.tsconfig = tsconfig;
50
66
  }
51
67
  return newOptions;
52
68
  }
@@ -127,11 +143,135 @@ export function shortenTree(
127
143
  return output;
128
144
  }
129
145
 
146
+ function getPackageNameFromRequest(request: string): string | null {
147
+ if (request.startsWith('.') || path.isAbsolute(request)) {
148
+ return null;
149
+ }
150
+ const parts = request.split('/');
151
+ if (request.startsWith('@')) {
152
+ return parts.length > 1 ? parts.slice(0, 2).join('/') : request;
153
+ }
154
+ return parts[0] || null;
155
+ }
156
+
157
+ function getPackageNameFromPath(
158
+ context: string,
159
+ id: string,
160
+ cache: Map<string, string | null>,
161
+ ): string | null {
162
+ if (allBuiltins.has(id)) {
163
+ return id;
164
+ }
165
+ const fullPath = path.isAbsolute(id) ? id : path.resolve(context, id);
166
+ let current = path.extname(fullPath) ? path.dirname(fullPath) : fullPath;
167
+ const root = path.parse(current).root;
168
+
169
+ while (true) {
170
+ const cached = cache.get(current);
171
+ if (cached !== void 0) {
172
+ return cached;
173
+ }
174
+
175
+ try {
176
+ const pkg = fs.readJSONSync(path.join(current, 'package.json'));
177
+ const name =
178
+ typeof pkg.name === 'string' && pkg.name
179
+ ? pkg.name
180
+ : path.relative(context, current) || path.basename(current);
181
+ cache.set(current, name);
182
+ return name;
183
+ } catch {}
184
+
185
+ if (current === root) {
186
+ cache.set(current, null);
187
+ return null;
188
+ }
189
+ current = path.dirname(current);
190
+ }
191
+ }
192
+
193
+ export function getPackageName(context: string, id: string): string | null {
194
+ return getPackageNameFromPath(context, id, new Map());
195
+ }
196
+
197
+ export function groupDependencyTreeByPackage(
198
+ tree: DependencyTree,
199
+ context: string,
200
+ ): DependencyTree {
201
+ const packages: Record<string, Dependency[] | null> = {};
202
+ const edges: Record<string, Set<string>> = {};
203
+ const cache = new Map<string, string | null>();
204
+
205
+ function ensurePackage(id: string, ignored = false) {
206
+ if (!(id in packages)) {
207
+ packages[id] = ignored ? null : [];
208
+ } else if (packages[id] === null && !ignored) {
209
+ packages[id] = [];
210
+ }
211
+ }
212
+
213
+ for (const id in tree) {
214
+ const issuerPackage = getPackageNameFromPath(context, id, cache) || id;
215
+ const deps = tree[id];
216
+ ensurePackage(issuerPackage, deps === null);
217
+ if (!deps) {
218
+ continue;
219
+ }
220
+
221
+ for (const dep of deps) {
222
+ const dependencyPackage = dep.id
223
+ ? getPackageNameFromPath(context, dep.id, cache)
224
+ : getPackageNameFromRequest(dep.request);
225
+ if (!dependencyPackage || dependencyPackage === issuerPackage) {
226
+ continue;
227
+ }
228
+
229
+ ensurePackage(dependencyPackage, dep.id ? tree[dep.id] === null : false);
230
+ const edgeSet = (edges[issuerPackage] =
231
+ edges[issuerPackage] || new Set());
232
+ if (edgeSet.has(dependencyPackage)) {
233
+ continue;
234
+ }
235
+ edgeSet.add(dependencyPackage);
236
+ (packages[issuerPackage] as Dependency[]).push({
237
+ issuer: issuerPackage,
238
+ request: dep.request,
239
+ kind: dep.kind,
240
+ id: dependencyPackage,
241
+ });
242
+ }
243
+ }
244
+
245
+ for (const id in packages) {
246
+ packages[id]?.sort((a, b) => a.id!.localeCompare(b.id!));
247
+ }
248
+ return packages;
249
+ }
250
+
251
+ export function groupEntriesByPackage(
252
+ entries: string[],
253
+ context: string,
254
+ ): string[] {
255
+ const output: string[] = [];
256
+ const seen = new Set<string>();
257
+ const cache = new Map<string, string | null>();
258
+ for (const entry of entries) {
259
+ const id = getPackageNameFromPath(context, entry, cache) || entry;
260
+ if (!seen.has(id)) {
261
+ output.push(id);
262
+ seen.add(id);
263
+ }
264
+ }
265
+ return output;
266
+ }
267
+
130
268
  export function parseCircular(
131
269
  tree: DependencyTree,
132
270
  skipDynamicImports: boolean = false,
271
+ skipImports: readonly SkippedImport[] = [],
133
272
  ): string[][] {
134
273
  const circulars: string[][] = [];
274
+ const skippedImports = createSkippedImportsRegExp(skipImports);
135
275
 
136
276
  tree = { ...tree };
137
277
 
@@ -147,7 +287,9 @@ export function parseCircular(
147
287
  deps.forEach((dep) => {
148
288
  if (
149
289
  dep.id &&
150
- (!skipDynamicImports || dep.kind !== DependencyKind.DynamicImport)
290
+ (!skipDynamicImports ||
291
+ dep.kind !== DependencyKind.DynamicImport) &&
292
+ !skippedImports.test(`${dep.issuer}:${dep.id}`)
151
293
  ) {
152
294
  visit(dep.id, used.slice());
153
295
  }