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.
- package/README.md +45 -12
- package/lib/bin/dpdm.js +63 -11
- package/lib/bin/dpdm.js.map +1 -1
- package/lib/bin/dpdm.mjs +64 -12
- package/lib/bin/dpdm.mjs.map +1 -1
- package/lib/parser.js +3 -4
- package/lib/parser.js.map +1 -1
- package/lib/parser.mjs +3 -4
- package/lib/parser.mjs.map +1 -1
- package/lib/parser.spec.d.ts +5 -0
- package/lib/parser.spec.js +105 -0
- package/lib/parser.spec.js.map +1 -0
- package/lib/parser.spec.mjs +103 -0
- package/lib/parser.spec.mjs.map +1 -0
- package/lib/types.d.ts +1 -0
- package/lib/utils.d.ts +5 -1
- package/lib/utils.js +122 -6
- package/lib/utils.js.map +1 -1
- package/lib/utils.mjs +119 -6
- package/lib/utils.mjs.map +1 -1
- package/lib/utils.spec.js +50 -3
- package/lib/utils.spec.js.map +1 -1
- package/lib/utils.spec.mjs +50 -3
- package/lib/utils.spec.mjs.map +1 -1
- package/package.json +2 -1
- package/src/bin/dpdm.ts +91 -17
- package/src/parser.spec.ts +134 -0
- package/src/parser.ts +6 -7
- package/src/types.ts +1 -0
- package/src/utils.spec.ts +64 -3
- package/src/utils.ts +147 -5
|
@@ -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 (
|
|
140
|
+
if (fullOptions.tsconfig) {
|
|
142
141
|
const compilerOptions = ts.parseJsonConfigFileContent(
|
|
143
|
-
ts.readConfigFile(
|
|
142
|
+
ts.readConfigFile(fullOptions.tsconfig, ts.sys.readFile).config,
|
|
144
143
|
ts.sys,
|
|
145
|
-
path.dirname(
|
|
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
|
-
|
|
178
|
-
path.
|
|
176
|
+
fullOptions.cwd,
|
|
177
|
+
path.resolve(fullOptions.cwd, filename),
|
|
179
178
|
fullOptions,
|
|
180
179
|
output,
|
|
181
180
|
resolve,
|
package/src/types.ts
CHANGED
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 ||
|
|
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
|
}
|