submodule-version 2.3.2 → 3.0.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 +103 -20
- package/dist/runner/handleErrors/handleErrors.d.ts +1 -1
- package/dist/runner/handleErrors/handleErrors.js +3 -10
- package/dist/runner/handleErrors/handleErrors.js.map +1 -1
- package/dist/runner/index.js +0 -0
- package/dist/runner/runner.d.ts +4 -0
- package/dist/runner/runner.js +91 -37
- package/dist/runner/runner.js.map +1 -1
- package/dist/src/PubGrub/PubGrub.d.ts +28 -0
- package/dist/src/PubGrub/PubGrub.js +366 -0
- package/dist/src/PubGrub/PubGrub.js.map +1 -0
- package/dist/src/PubGrub/index.d.ts +2 -0
- package/dist/src/PubGrub/index.js +7 -0
- package/dist/src/PubGrub/index.js.map +1 -0
- package/dist/src/PubGrub/types.d.ts +56 -0
- package/dist/src/PubGrub/types.js +3 -0
- package/dist/src/PubGrub/types.js.map +1 -0
- package/dist/src/entryStore.d.ts +16 -0
- package/dist/src/entryStore.js +152 -0
- package/dist/src/entryStore.js.map +1 -0
- package/dist/src/errors.d.ts +28 -0
- package/dist/src/errors.js +30 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/git/api/GithubError.d.ts +4 -0
- package/dist/src/git/api/GithubError.js +11 -0
- package/dist/src/git/api/GithubError.js.map +1 -0
- package/dist/src/git/api/api.d.ts +8 -0
- package/dist/src/git/api/api.js +133 -0
- package/dist/src/git/api/api.js.map +1 -0
- package/dist/src/git/api/index.d.ts +3 -0
- package/dist/src/git/api/index.js +8 -0
- package/dist/src/git/api/index.js.map +1 -0
- package/dist/src/git/git.d.ts +30 -23
- package/dist/src/git/git.js +8 -258
- package/dist/src/git/git.js.map +1 -1
- package/dist/src/git/index.d.ts +1 -0
- package/dist/src/git/index.js +3 -1
- package/dist/src/git/index.js.map +1 -1
- package/dist/src/git/local/index.d.ts +1 -0
- package/dist/src/git/local/index.js +6 -0
- package/dist/src/git/local/index.js.map +1 -0
- package/dist/src/git/local/local.d.ts +26 -0
- package/dist/src/git/local/local.js +288 -0
- package/dist/src/git/local/local.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +16 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/printError.d.ts +1 -0
- package/dist/src/printError.js +158 -0
- package/dist/src/printError.js.map +1 -0
- package/dist/src/sv.d.ts +18 -8
- package/dist/src/sv.js +235 -131
- package/dist/src/sv.js.map +1 -1
- package/dist/src/versionUtil/index.d.ts +2 -0
- package/dist/src/versionUtil/index.js +6 -0
- package/dist/src/versionUtil/index.js.map +1 -0
- package/dist/src/versionUtil/versionUtil.d.ts +16 -0
- package/dist/src/versionUtil/versionUtil.js +70 -0
- package/dist/src/versionUtil/versionUtil.js.map +1 -0
- package/package.json +4 -2
- package/runner/handleErrors/handleErrors.ts +3 -10
- package/runner/runner.ts +128 -46
- package/src/PubGrub/PubGrub.ts +578 -0
- package/src/PubGrub/__tests__/PubGrub.test.ts +351 -0
- package/src/PubGrub/index.ts +12 -0
- package/src/PubGrub/types.ts +72 -0
- package/src/__tests__/entryStore.test.ts +248 -0
- package/src/__tests__/printError.test.ts +148 -0
- package/src/__tests__/sv.test.ts +441 -0
- package/src/entryStore.ts +212 -0
- package/src/errors.ts +39 -0
- package/src/git/api/GithubError.ts +5 -0
- package/src/git/api/api.ts +180 -0
- package/src/git/api/index.ts +3 -0
- package/src/git/git.ts +8 -396
- package/src/git/index.ts +1 -0
- package/src/git/local/index.ts +1 -0
- package/src/git/local/local.ts +430 -0
- package/src/index.ts +17 -0
- package/src/printError.ts +189 -0
- package/src/sv.ts +321 -134
- package/src/versionUtil/__tests__/versionUtil.test.ts +62 -0
- package/src/versionUtil/index.ts +2 -0
- package/src/versionUtil/versionUtil.ts +103 -0
- package/tsconfig.json +1 -1
- package/dist/runner/handleErrors/handleBuildGraphError.d.ts +0 -1
- package/dist/runner/handleErrors/handleBuildGraphError.js +0 -42
- package/dist/runner/handleErrors/handleBuildGraphError.js.map +0 -1
- package/dist/runner/handleErrors/handleGitError.d.ts +0 -1
- package/dist/runner/handleErrors/handleGitError.js +0 -27
- package/dist/runner/handleErrors/handleGitError.js.map +0 -1
- package/dist/runner/handleErrors/handleInstallError.d.ts +0 -1
- package/dist/runner/handleErrors/handleInstallError.js +0 -30
- package/dist/runner/handleErrors/handleInstallError.js.map +0 -1
- package/dist/src/__tests__/versionUtil.test.d.ts +0 -1
- package/dist/src/__tests__/versionUtil.test.js +0 -80
- package/dist/src/__tests__/versionUtil.test.js.map +0 -1
- package/dist/src/buildGraph/GraphError.d.ts +0 -11
- package/dist/src/buildGraph/GraphError.js +0 -11
- package/dist/src/buildGraph/GraphError.js.map +0 -1
- package/dist/src/buildGraph/buildGraph.d.ts +0 -8
- package/dist/src/buildGraph/buildGraph.js +0 -100
- package/dist/src/buildGraph/buildGraph.js.map +0 -1
- package/dist/src/buildGraph/index.d.ts +0 -2
- package/dist/src/buildGraph/index.js +0 -8
- package/dist/src/buildGraph/index.js.map +0 -1
- package/dist/src/git/__tests__/parseGitUrl.test.d.ts +0 -1
- package/dist/src/git/__tests__/parseGitUrl.test.js +0 -30
- package/dist/src/git/__tests__/parseGitUrl.test.js.map +0 -1
- package/dist/src/versionUtil.d.ts +0 -13
- package/dist/src/versionUtil.js +0 -79
- package/dist/src/versionUtil.js.map +0 -1
- package/runner/handleErrors/handleBuildGraphError.ts +0 -49
- package/runner/handleErrors/handleGitError.ts +0 -28
- package/runner/handleErrors/handleInstallError.ts +0 -26
- package/src/__tests__/versionUtil.test.ts +0 -96
- package/src/buildGraph/GraphError.ts +0 -12
- package/src/buildGraph/buildGraph.ts +0 -150
- package/src/buildGraph/index.ts +0 -2
- package/src/versionUtil.ts +0 -97
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { PubGrub } from '../PubGrub';
|
|
2
|
+
import type {
|
|
3
|
+
IEntrySource,
|
|
4
|
+
IPackageEntry,
|
|
5
|
+
TDependencies,
|
|
6
|
+
TPubGrubResult,
|
|
7
|
+
} from '../types';
|
|
8
|
+
|
|
9
|
+
const url = (name: string) => `git@git:repo/${name}.git`;
|
|
10
|
+
|
|
11
|
+
const defineModule = (
|
|
12
|
+
name: string,
|
|
13
|
+
versions: Record<string, TDependencies>,
|
|
14
|
+
): IPackageEntry => ({ name, url: url(name), versions });
|
|
15
|
+
|
|
16
|
+
const A = defineModule('A', {
|
|
17
|
+
'1.0.0': { B: '^1.0.0', C: '^1.3.2' },
|
|
18
|
+
'1.2.0': { B: '^1.2.0', C: '^1.3.2' },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const B = defineModule('B', {
|
|
22
|
+
'1.0.0': { C: '^1.3.2' },
|
|
23
|
+
'1.2.0': { C: '^1.4.2' },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const C = defineModule('C', {
|
|
27
|
+
'1.3.2': {},
|
|
28
|
+
'1.4.2': { E: '^0.0.3' },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const E = defineModule('E', {
|
|
32
|
+
'0.0.3': {},
|
|
33
|
+
'0.0.4': {},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const BacktrackA = defineModule('BacktrackA', {
|
|
37
|
+
'1.0.0': { BacktrackB: '^1.0.0' },
|
|
38
|
+
'2.0.0': { BacktrackB: '^2.0.0' },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const BacktrackB = defineModule('BacktrackB', {
|
|
42
|
+
'1.5.0': {},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const DeepA = defineModule('DeepA', {
|
|
46
|
+
'1.0.0': { DeepB: '^1.0.0' },
|
|
47
|
+
'2.0.0': { DeepB: '^2.0.0' },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const DeepB = defineModule('DeepB', {
|
|
51
|
+
'1.0.0': { DeepC: '^1.0.0' },
|
|
52
|
+
'2.0.0': { DeepC: '^2.0.0' },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const DeepC = defineModule('DeepC', {
|
|
56
|
+
'1.1.0': {},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const DiamondLeft = defineModule('DiamondLeft', {
|
|
60
|
+
'1.0.0': { DiamondShared: '^1.0.0' },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const DiamondRight = defineModule('DiamondRight', {
|
|
64
|
+
'1.0.0': { DiamondShared: '^1.4.0' },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const DiamondShared = defineModule('DiamondShared', {
|
|
68
|
+
'1.2.0': {},
|
|
69
|
+
'1.5.0': {},
|
|
70
|
+
'2.0.0': {},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const LazyA = defineModule('LazyA', {
|
|
74
|
+
'1.0.0': { Unused: '*' },
|
|
75
|
+
'2.0.0': { LazyB: '*' },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const LazyB = defineModule('LazyB', {
|
|
79
|
+
'1.0.0': { AlsoUnused: '*' },
|
|
80
|
+
'2.0.0': {},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const Unused = defineModule('Unused', { '1.0.0': {} });
|
|
84
|
+
const AlsoUnused = defineModule('AlsoUnused', { '1.0.0': {} });
|
|
85
|
+
|
|
86
|
+
const FallbackA = defineModule('FallbackA', {
|
|
87
|
+
'1.0.0': {},
|
|
88
|
+
'2.0.0': { FallbackB: '*' },
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const FallbackB = defineModule('FallbackB', {
|
|
92
|
+
'1.0.0': { FallbackA: '^2.0.0' },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const ConflictA = defineModule('ConflictA', { '1.0.0': {} });
|
|
96
|
+
|
|
97
|
+
const ConflictB = defineModule('ConflictB', {
|
|
98
|
+
'1.0.0': { ConflictA: '^2.0.0' },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const ExactA = defineModule('ExactA', { '1.0.0': {} });
|
|
102
|
+
const Empty = defineModule('Empty', {});
|
|
103
|
+
|
|
104
|
+
const InvalidA = defineModule('InvalidA', {
|
|
105
|
+
'1.0.0': { InvalidB: 'invalid' },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const InvalidB = defineModule('InvalidB', { '1.0.0': {} });
|
|
109
|
+
|
|
110
|
+
const CycleA = defineModule('CycleA', {
|
|
111
|
+
'1.0.0': { CycleB: '*' },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const CycleB = defineModule('CycleB', {
|
|
115
|
+
'1.0.0': { CycleC: '*' },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const CycleC = defineModule('CycleC', {
|
|
119
|
+
'1.0.0': { CycleA: '*' },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const SelfCycle = defineModule('SelfCycle', {
|
|
123
|
+
'1.0.0': { SelfCycle: '*' },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const ALL_MODULES = [
|
|
127
|
+
A,
|
|
128
|
+
B,
|
|
129
|
+
C,
|
|
130
|
+
E,
|
|
131
|
+
BacktrackA,
|
|
132
|
+
BacktrackB,
|
|
133
|
+
DeepA,
|
|
134
|
+
DeepB,
|
|
135
|
+
DeepC,
|
|
136
|
+
DiamondLeft,
|
|
137
|
+
DiamondRight,
|
|
138
|
+
DiamondShared,
|
|
139
|
+
LazyA,
|
|
140
|
+
LazyB,
|
|
141
|
+
Unused,
|
|
142
|
+
AlsoUnused,
|
|
143
|
+
FallbackA,
|
|
144
|
+
FallbackB,
|
|
145
|
+
ConflictA,
|
|
146
|
+
ConflictB,
|
|
147
|
+
ExactA,
|
|
148
|
+
Empty,
|
|
149
|
+
InvalidA,
|
|
150
|
+
InvalidB,
|
|
151
|
+
CycleA,
|
|
152
|
+
CycleB,
|
|
153
|
+
CycleC,
|
|
154
|
+
SelfCycle,
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const source = (modules: IPackageEntry[]) => {
|
|
158
|
+
const entries = Object.fromEntries(modules.map(entry => [entry.name, entry]));
|
|
159
|
+
const calls: string[] = [];
|
|
160
|
+
|
|
161
|
+
const store: IEntrySource = {
|
|
162
|
+
fetch: jest.fn(async (depPath: string) => {
|
|
163
|
+
calls.push(depPath);
|
|
164
|
+
|
|
165
|
+
const name = depPath.endsWith('.git')
|
|
166
|
+
? depPath.match(/\/([^/]+)\.git$/)?.[1]
|
|
167
|
+
: depPath;
|
|
168
|
+
|
|
169
|
+
return entries[name!];
|
|
170
|
+
}),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return { store, calls };
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
class TestRoot {
|
|
177
|
+
public fetched: string[] = [];
|
|
178
|
+
|
|
179
|
+
constructor (
|
|
180
|
+
private modules: IPackageEntry[],
|
|
181
|
+
private dependencies: TDependencies = {},
|
|
182
|
+
) {}
|
|
183
|
+
|
|
184
|
+
public add = (
|
|
185
|
+
entry: IPackageEntry,
|
|
186
|
+
range = '*',
|
|
187
|
+
): TestRoot => new TestRoot(this.modules, {
|
|
188
|
+
...this.dependencies,
|
|
189
|
+
[entry.url]: range,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
public resolve = (): Promise<TPubGrubResult> => {
|
|
193
|
+
const fixture = source(this.modules);
|
|
194
|
+
|
|
195
|
+
this.fetched = fixture.calls;
|
|
196
|
+
|
|
197
|
+
return new PubGrub(fixture.store).resolve(this.dependencies);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const root = new TestRoot(ALL_MODULES);
|
|
202
|
+
|
|
203
|
+
const selected = (result: TPubGrubResult) => Object.fromEntries(
|
|
204
|
+
Object.entries(result).map(([name, entry]) => [name, entry.version]),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
describe('PubGrub resolution', () => {
|
|
208
|
+
it('resolves an empty root', async () => {
|
|
209
|
+
const result = await root.resolve();
|
|
210
|
+
|
|
211
|
+
expect(result).toEqual({});
|
|
212
|
+
expect(root.fetched).toEqual([]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('selects highest compatible nested versions', async () => {
|
|
216
|
+
const currentRoot = root.add(A, '^1.0.0');
|
|
217
|
+
const result = await currentRoot.resolve();
|
|
218
|
+
|
|
219
|
+
expect(selected(result)).toEqual({
|
|
220
|
+
A: '1.2.0',
|
|
221
|
+
B: '1.2.0',
|
|
222
|
+
C: '1.4.2',
|
|
223
|
+
E: '0.0.3',
|
|
224
|
+
});
|
|
225
|
+
expect(result.B.dependencies).toEqual({ C: '^1.4.2' });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('backtracks from an incompatible newer parent version', async () => {
|
|
229
|
+
const currentRoot = root.add(BacktrackA);
|
|
230
|
+
const result = await currentRoot.resolve();
|
|
231
|
+
|
|
232
|
+
expect(selected(result)).toEqual({
|
|
233
|
+
BacktrackA: '1.0.0',
|
|
234
|
+
BacktrackB: '1.5.0',
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('backtracks through multiple dependency levels', async () => {
|
|
239
|
+
const currentRoot = root.add(DeepA);
|
|
240
|
+
const result = await currentRoot.resolve();
|
|
241
|
+
|
|
242
|
+
expect(selected(result)).toEqual({
|
|
243
|
+
DeepA: '1.0.0',
|
|
244
|
+
DeepB: '1.0.0',
|
|
245
|
+
DeepC: '1.1.0',
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('intersects constraints from two root modules', async () => {
|
|
250
|
+
const currentRoot = root
|
|
251
|
+
.add(DiamondLeft)
|
|
252
|
+
.add(DiamondRight);
|
|
253
|
+
|
|
254
|
+
const result = await currentRoot.resolve();
|
|
255
|
+
|
|
256
|
+
expect(selected(result)).toEqual({
|
|
257
|
+
DiamondLeft: '1.0.0',
|
|
258
|
+
DiamondRight: '1.0.0',
|
|
259
|
+
DiamondShared: '1.5.0',
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('does not fetch dependencies of unselected tags', async () => {
|
|
264
|
+
const currentRoot = root.add(LazyA);
|
|
265
|
+
const result = await currentRoot.resolve();
|
|
266
|
+
|
|
267
|
+
expect(selected(result)).toEqual({
|
|
268
|
+
LazyA: '2.0.0',
|
|
269
|
+
LazyB: '2.0.0',
|
|
270
|
+
});
|
|
271
|
+
expect(currentRoot.fetched).toEqual([url('LazyA'), 'LazyB']);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('backs away from a circular newer version', async () => {
|
|
275
|
+
const currentRoot = root.add(FallbackA);
|
|
276
|
+
const result = await currentRoot.resolve();
|
|
277
|
+
|
|
278
|
+
expect(selected(result)).toEqual({ FallbackA: '1.0.0' });
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('PubGrub errors', () => {
|
|
283
|
+
it('reports every requirement in a version conflict', async () => {
|
|
284
|
+
const currentRoot = root
|
|
285
|
+
.add(ConflictA, '^1.0.0')
|
|
286
|
+
.add(ConflictB);
|
|
287
|
+
|
|
288
|
+
await expect(currentRoot.resolve()).rejects.toMatchObject({
|
|
289
|
+
error: 'VERSION_CONFLICT',
|
|
290
|
+
details: {
|
|
291
|
+
name: 'ConflictA',
|
|
292
|
+
parents: {
|
|
293
|
+
'<root>': '^1.0.0',
|
|
294
|
+
'ConflictB@1.0.0': '^2.0.0',
|
|
295
|
+
},
|
|
296
|
+
versions: ['1.0.0'],
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('reports an unavailable exact version', async () => {
|
|
302
|
+
const currentRoot = root.add(ExactA, '9.0.0');
|
|
303
|
+
|
|
304
|
+
await expect(currentRoot.resolve()).rejects.toMatchObject({
|
|
305
|
+
error: 'VERSION_CONFLICT',
|
|
306
|
+
details: { name: 'ExactA', versions: ['1.0.0'] },
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('reports a module without version tags', async () => {
|
|
311
|
+
const currentRoot = root.add(Empty);
|
|
312
|
+
|
|
313
|
+
await expect(currentRoot.resolve()).rejects.toMatchObject({
|
|
314
|
+
error: 'VERSION_CONFLICT',
|
|
315
|
+
details: { name: 'Empty', versions: [] },
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('reports an invalid nested constraint and its owner', async () => {
|
|
320
|
+
const currentRoot = root.add(InvalidA);
|
|
321
|
+
|
|
322
|
+
await expect(currentRoot.resolve()).rejects.toMatchObject({
|
|
323
|
+
error: 'UNKNOWN_VERSION',
|
|
324
|
+
details: {
|
|
325
|
+
name: 'InvalidB',
|
|
326
|
+
parent: 'InvalidA@1.0.0',
|
|
327
|
+
version: 'invalid',
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('reports the complete circular chain', async () => {
|
|
333
|
+
const currentRoot = root.add(CycleA);
|
|
334
|
+
|
|
335
|
+
await expect(currentRoot.resolve()).rejects.toMatchObject({
|
|
336
|
+
error: 'CIRCULAR_DEPENDENCY',
|
|
337
|
+
details: {
|
|
338
|
+
chain: ['CycleC', 'CycleA', 'CycleB', 'CycleC'],
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('reports a self dependency', async () => {
|
|
344
|
+
const currentRoot = root.add(SelfCycle);
|
|
345
|
+
|
|
346
|
+
await expect(currentRoot.resolve()).rejects.toMatchObject({
|
|
347
|
+
error: 'CIRCULAR_DEPENDENCY',
|
|
348
|
+
details: { chain: ['SelfCycle', 'SelfCycle'] },
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type TDependencies = Record<string, string>;
|
|
2
|
+
|
|
3
|
+
export interface IPackageEntry {
|
|
4
|
+
name: string;
|
|
5
|
+
url: string;
|
|
6
|
+
versions: Record<string, TDependencies>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface IResolvedPackage extends IPackageEntry {
|
|
10
|
+
version: string;
|
|
11
|
+
dependencies: TDependencies;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TPubGrubResult = Record<string, IResolvedPackage>;
|
|
15
|
+
|
|
16
|
+
export interface IEntrySource {
|
|
17
|
+
fetch(depPath: string): Promise<IPackageEntry>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Потенциальное изменение дерева зависимостей на время entryStore.simulate.
|
|
22
|
+
* Без parent — root (применяется к rootDependencies самим вызывающим кодом).
|
|
23
|
+
*/
|
|
24
|
+
export interface IOverride {
|
|
25
|
+
parent?: string;
|
|
26
|
+
add?: string;
|
|
27
|
+
version?: string;
|
|
28
|
+
delete?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IRequirement {
|
|
32
|
+
range: string;
|
|
33
|
+
positive: boolean;
|
|
34
|
+
requiredBy: string;
|
|
35
|
+
chain: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ITerm {
|
|
39
|
+
module: string;
|
|
40
|
+
range: string;
|
|
41
|
+
positive: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type TIncompatibilityCause = {
|
|
45
|
+
type: 'dependency' | 'root';
|
|
46
|
+
requirement: IRequirement;
|
|
47
|
+
} | {
|
|
48
|
+
type: 'learned';
|
|
49
|
+
conflict: TConflict;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export interface IIncompatibility {
|
|
53
|
+
terms: ITerm[];
|
|
54
|
+
cause: TIncompatibilityCause;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface IVersionConflict {
|
|
58
|
+
type: 'version';
|
|
59
|
+
name: string;
|
|
60
|
+
requirements: IRequirement[];
|
|
61
|
+
versions: string[];
|
|
62
|
+
packages: Set<string>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ICircularConflict {
|
|
66
|
+
type: 'circular';
|
|
67
|
+
name: string;
|
|
68
|
+
chain: string[];
|
|
69
|
+
packages: Set<string>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type TConflict = IVersionConflict | ICircularConflict;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { entryStore } from '../entryStore';
|
|
2
|
+
import { git } from '../git';
|
|
3
|
+
|
|
4
|
+
jest.mock('../git', () => {
|
|
5
|
+
const actual = jest.requireActual('../git');
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
git: {
|
|
10
|
+
...actual.git,
|
|
11
|
+
api: { fetchVersions: jest.fn() },
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const fetchVersionsMock = git.api.fetchVersions as jest.Mock;
|
|
17
|
+
const url = (name: string) => `git@git:repo/${name}.git`;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
entryStore.clear();
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('entryStore.fetch', () => {
|
|
25
|
+
const singleModuleLoad
|
|
26
|
+
= 'loads all tags of one module without fetching nested modules';
|
|
27
|
+
|
|
28
|
+
const nestedName
|
|
29
|
+
= 'resolves nested name through the URL learned from its parent';
|
|
30
|
+
|
|
31
|
+
it(singleModuleLoad, async () => {
|
|
32
|
+
fetchVersionsMock.mockResolvedValueOnce([
|
|
33
|
+
{
|
|
34
|
+
version: '1.0.0',
|
|
35
|
+
pkg: { sv: { [url('B')]: '^1.0.0' } },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
version: '2.0.0',
|
|
39
|
+
pkg: { sv: { [url('C')]: '^2.0.0' } },
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
await expect(entryStore.fetch(url('A'))).resolves.toEqual({
|
|
44
|
+
name: 'A',
|
|
45
|
+
url: url('A'),
|
|
46
|
+
versions: {
|
|
47
|
+
'1.0.0': { B: '^1.0.0' },
|
|
48
|
+
'2.0.0': { C: '^2.0.0' },
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(fetchVersionsMock).toHaveBeenCalledTimes(1);
|
|
53
|
+
expect(fetchVersionsMock).toHaveBeenCalledWith(url('A'));
|
|
54
|
+
expect(entryStore.urlOf('B')).toBe(url('B'));
|
|
55
|
+
expect(entryStore.urlOf('C')).toBe(url('C'));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it(nestedName, async () => {
|
|
59
|
+
fetchVersionsMock
|
|
60
|
+
.mockResolvedValueOnce([{
|
|
61
|
+
version: '1.0.0',
|
|
62
|
+
pkg: { sv: { [url('B')]: '*' } },
|
|
63
|
+
}])
|
|
64
|
+
.mockResolvedValueOnce([{ version: '2.0.0', pkg: {} }]);
|
|
65
|
+
|
|
66
|
+
await entryStore.fetch(url('A'));
|
|
67
|
+
const result = await entryStore.fetch('B');
|
|
68
|
+
|
|
69
|
+
expect(result.name).toBe('B');
|
|
70
|
+
expect(fetchVersionsMock).toHaveBeenLastCalledWith(url('B'));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('deduplicates concurrent and repeated requests', async () => {
|
|
74
|
+
fetchVersionsMock.mockResolvedValue([{ version: '1.0.0', pkg: {} }]);
|
|
75
|
+
|
|
76
|
+
const first = entryStore.fetch(url('A'));
|
|
77
|
+
const second = entryStore.fetch(url('A'));
|
|
78
|
+
|
|
79
|
+
expect(first).toBe(second);
|
|
80
|
+
|
|
81
|
+
await Promise.all([first, second]);
|
|
82
|
+
await entryStore.fetch('A');
|
|
83
|
+
|
|
84
|
+
expect(fetchVersionsMock).toHaveBeenCalledTimes(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('does not cache a failed request', async () => {
|
|
88
|
+
fetchVersionsMock
|
|
89
|
+
.mockRejectedValueOnce(new Error('network'))
|
|
90
|
+
.mockResolvedValueOnce([{ version: '1.0.0', pkg: {} }]);
|
|
91
|
+
|
|
92
|
+
await expect(entryStore.fetch(url('A'))).rejects.toThrow('network');
|
|
93
|
+
await expect(entryStore.fetch(url('A'))).resolves.toMatchObject({
|
|
94
|
+
name: 'A',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(fetchVersionsMock).toHaveBeenCalledTimes(2);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('rejects malformed nested dependency URLs', async () => {
|
|
101
|
+
fetchVersionsMock.mockResolvedValueOnce([{
|
|
102
|
+
version: '1.0.0',
|
|
103
|
+
pkg: { sv: { B: '*' } },
|
|
104
|
+
}]);
|
|
105
|
+
|
|
106
|
+
await expect(entryStore.fetch(url('A'))).rejects.toMatchObject({
|
|
107
|
+
error: 'NOT_A_GIT_URL',
|
|
108
|
+
details: { url: 'B' },
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('entryStore.simulate', () => {
|
|
114
|
+
it('rewrites parent versions inside the callback only', async () => {
|
|
115
|
+
fetchVersionsMock.mockResolvedValue([{ version: '1.0.0', pkg: {} }]);
|
|
116
|
+
|
|
117
|
+
const patched = await entryStore.simulate(
|
|
118
|
+
[{ parent: 'A', add: url('B'), version: '^1.2.3' }],
|
|
119
|
+
{},
|
|
120
|
+
() => entryStore.fetch(url('A')),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(patched.versions['1.0.0']).toEqual({ B: '^1.2.3' });
|
|
124
|
+
expect(entryStore.urlOf('B')).toBe(url('B'));
|
|
125
|
+
|
|
126
|
+
/* после коллбэка оверрайдов нет, кэш остался сырым */
|
|
127
|
+
const raw = await entryStore.fetch(url('A'));
|
|
128
|
+
|
|
129
|
+
expect(raw.versions['1.0.0']).toEqual({});
|
|
130
|
+
expect(fetchVersionsMock).toHaveBeenCalledTimes(1);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('lets the solver fetch an overridden child by name', async () => {
|
|
134
|
+
fetchVersionsMock.mockImplementation(async (moduleUrl: string) => (
|
|
135
|
+
moduleUrl === url('B')
|
|
136
|
+
? [{ version: '2.0.0', pkg: {} }]
|
|
137
|
+
: [{ version: '1.0.0', pkg: {} }]
|
|
138
|
+
));
|
|
139
|
+
|
|
140
|
+
const child = await entryStore.simulate(
|
|
141
|
+
[{ parent: 'A', add: url('B') }],
|
|
142
|
+
{},
|
|
143
|
+
async () => {
|
|
144
|
+
await entryStore.fetch(url('A'));
|
|
145
|
+
|
|
146
|
+
return entryStore.fetch('B');
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(child).toEqual({
|
|
151
|
+
name: 'B',
|
|
152
|
+
url: url('B'),
|
|
153
|
+
versions: { '2.0.0': {} },
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('deletes a dependency from every parent version', async () => {
|
|
158
|
+
fetchVersionsMock.mockResolvedValue([{
|
|
159
|
+
version: '1.0.0',
|
|
160
|
+
pkg: { sv: { [url('B')]: '^1.0.0' } },
|
|
161
|
+
}]);
|
|
162
|
+
|
|
163
|
+
const patched = await entryStore.simulate(
|
|
164
|
+
[{ parent: 'A', delete: 'B' }],
|
|
165
|
+
{},
|
|
166
|
+
() => entryStore.fetch(url('A')),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(patched.versions['1.0.0']).toEqual({});
|
|
170
|
+
|
|
171
|
+
const raw = await entryStore.fetch(url('A'));
|
|
172
|
+
|
|
173
|
+
expect(raw.versions['1.0.0']).toEqual({ B: '^1.0.0' });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('clears overrides when the callback throws', async () => {
|
|
177
|
+
fetchVersionsMock.mockResolvedValue([{ version: '1.0.0', pkg: {} }]);
|
|
178
|
+
|
|
179
|
+
await expect(entryStore.simulate(
|
|
180
|
+
[{ parent: 'A', add: url('B') }],
|
|
181
|
+
{},
|
|
182
|
+
async () => {
|
|
183
|
+
throw new Error('boom');
|
|
184
|
+
},
|
|
185
|
+
)).rejects.toThrow('boom');
|
|
186
|
+
|
|
187
|
+
const raw = await entryStore.fetch(url('A'));
|
|
188
|
+
|
|
189
|
+
expect(raw.versions['1.0.0']).toEqual({});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('rejects an override with a malformed module url', async () => {
|
|
193
|
+
fetchVersionsMock.mockResolvedValue([{ version: '1.0.0', pkg: {} }]);
|
|
194
|
+
|
|
195
|
+
await expect(entryStore.simulate(
|
|
196
|
+
[{ parent: 'A', add: 'not-a-url' }],
|
|
197
|
+
{},
|
|
198
|
+
() => entryStore.fetch(url('A')),
|
|
199
|
+
)).rejects.toMatchObject({
|
|
200
|
+
error: 'NOT_A_GIT_URL',
|
|
201
|
+
details: { url: 'not-a-url' },
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('passes root overrides as effective rootDeps', async () => {
|
|
206
|
+
const seen = await entryStore.simulate(
|
|
207
|
+
[{ add: url('B'), version: '^1.2.3' }],
|
|
208
|
+
{ [url('A')]: '*' },
|
|
209
|
+
async rootDeps => rootDeps,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(seen).toEqual({ [url('A')]: '*', [url('B')]: '^1.2.3' });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('defaults the version of a root override to *', async () => {
|
|
216
|
+
const seen = await entryStore.simulate(
|
|
217
|
+
[{ add: url('B') }],
|
|
218
|
+
{},
|
|
219
|
+
async rootDeps => rootDeps,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
expect(seen).toEqual({ [url('B')]: '*' });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('deletes a root dependency by module name', async () => {
|
|
226
|
+
const seen = await entryStore.simulate(
|
|
227
|
+
[{ delete: 'A' }],
|
|
228
|
+
{ [url('A')]: '*', [url('B')]: '^2.0.0' },
|
|
229
|
+
async rootDeps => rootDeps,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
expect(seen).toEqual({ [url('B')]: '^2.0.0' });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('rejects a root delete of a module missing from rootDeps', async () => {
|
|
236
|
+
const cb = jest.fn(async (rootDeps: Record<string, string>) => rootDeps);
|
|
237
|
+
|
|
238
|
+
await expect(entryStore.simulate(
|
|
239
|
+
[{ delete: 'Ghost' }],
|
|
240
|
+
{},
|
|
241
|
+
cb,
|
|
242
|
+
)).rejects.toMatchObject({
|
|
243
|
+
error: 'ADDON_NOT_FOUND',
|
|
244
|
+
details: { name: 'Ghost' },
|
|
245
|
+
});
|
|
246
|
+
expect(cb).not.toHaveBeenCalled();
|
|
247
|
+
});
|
|
248
|
+
});
|