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.
Files changed (120) hide show
  1. package/README.md +103 -20
  2. package/dist/runner/handleErrors/handleErrors.d.ts +1 -1
  3. package/dist/runner/handleErrors/handleErrors.js +3 -10
  4. package/dist/runner/handleErrors/handleErrors.js.map +1 -1
  5. package/dist/runner/index.js +0 -0
  6. package/dist/runner/runner.d.ts +4 -0
  7. package/dist/runner/runner.js +91 -37
  8. package/dist/runner/runner.js.map +1 -1
  9. package/dist/src/PubGrub/PubGrub.d.ts +28 -0
  10. package/dist/src/PubGrub/PubGrub.js +366 -0
  11. package/dist/src/PubGrub/PubGrub.js.map +1 -0
  12. package/dist/src/PubGrub/index.d.ts +2 -0
  13. package/dist/src/PubGrub/index.js +7 -0
  14. package/dist/src/PubGrub/index.js.map +1 -0
  15. package/dist/src/PubGrub/types.d.ts +56 -0
  16. package/dist/src/PubGrub/types.js +3 -0
  17. package/dist/src/PubGrub/types.js.map +1 -0
  18. package/dist/src/entryStore.d.ts +16 -0
  19. package/dist/src/entryStore.js +152 -0
  20. package/dist/src/entryStore.js.map +1 -0
  21. package/dist/src/errors.d.ts +28 -0
  22. package/dist/src/errors.js +30 -0
  23. package/dist/src/errors.js.map +1 -0
  24. package/dist/src/git/api/GithubError.d.ts +4 -0
  25. package/dist/src/git/api/GithubError.js +11 -0
  26. package/dist/src/git/api/GithubError.js.map +1 -0
  27. package/dist/src/git/api/api.d.ts +8 -0
  28. package/dist/src/git/api/api.js +133 -0
  29. package/dist/src/git/api/api.js.map +1 -0
  30. package/dist/src/git/api/index.d.ts +3 -0
  31. package/dist/src/git/api/index.js +8 -0
  32. package/dist/src/git/api/index.js.map +1 -0
  33. package/dist/src/git/git.d.ts +30 -23
  34. package/dist/src/git/git.js +8 -258
  35. package/dist/src/git/git.js.map +1 -1
  36. package/dist/src/git/index.d.ts +1 -0
  37. package/dist/src/git/index.js +3 -1
  38. package/dist/src/git/index.js.map +1 -1
  39. package/dist/src/git/local/index.d.ts +1 -0
  40. package/dist/src/git/local/index.js +6 -0
  41. package/dist/src/git/local/index.js.map +1 -0
  42. package/dist/src/git/local/local.d.ts +26 -0
  43. package/dist/src/git/local/local.js +288 -0
  44. package/dist/src/git/local/local.js.map +1 -0
  45. package/dist/src/index.d.ts +7 -0
  46. package/dist/src/index.js +16 -1
  47. package/dist/src/index.js.map +1 -1
  48. package/dist/src/printError.d.ts +1 -0
  49. package/dist/src/printError.js +158 -0
  50. package/dist/src/printError.js.map +1 -0
  51. package/dist/src/sv.d.ts +18 -8
  52. package/dist/src/sv.js +235 -131
  53. package/dist/src/sv.js.map +1 -1
  54. package/dist/src/versionUtil/index.d.ts +2 -0
  55. package/dist/src/versionUtil/index.js +6 -0
  56. package/dist/src/versionUtil/index.js.map +1 -0
  57. package/dist/src/versionUtil/versionUtil.d.ts +16 -0
  58. package/dist/src/versionUtil/versionUtil.js +70 -0
  59. package/dist/src/versionUtil/versionUtil.js.map +1 -0
  60. package/package.json +4 -2
  61. package/runner/handleErrors/handleErrors.ts +3 -10
  62. package/runner/runner.ts +128 -46
  63. package/src/PubGrub/PubGrub.ts +578 -0
  64. package/src/PubGrub/__tests__/PubGrub.test.ts +351 -0
  65. package/src/PubGrub/index.ts +12 -0
  66. package/src/PubGrub/types.ts +72 -0
  67. package/src/__tests__/entryStore.test.ts +248 -0
  68. package/src/__tests__/printError.test.ts +148 -0
  69. package/src/__tests__/sv.test.ts +441 -0
  70. package/src/entryStore.ts +212 -0
  71. package/src/errors.ts +39 -0
  72. package/src/git/api/GithubError.ts +5 -0
  73. package/src/git/api/api.ts +180 -0
  74. package/src/git/api/index.ts +3 -0
  75. package/src/git/git.ts +8 -396
  76. package/src/git/index.ts +1 -0
  77. package/src/git/local/index.ts +1 -0
  78. package/src/git/local/local.ts +430 -0
  79. package/src/index.ts +17 -0
  80. package/src/printError.ts +189 -0
  81. package/src/sv.ts +321 -134
  82. package/src/versionUtil/__tests__/versionUtil.test.ts +62 -0
  83. package/src/versionUtil/index.ts +2 -0
  84. package/src/versionUtil/versionUtil.ts +103 -0
  85. package/tsconfig.json +1 -1
  86. package/dist/runner/handleErrors/handleBuildGraphError.d.ts +0 -1
  87. package/dist/runner/handleErrors/handleBuildGraphError.js +0 -42
  88. package/dist/runner/handleErrors/handleBuildGraphError.js.map +0 -1
  89. package/dist/runner/handleErrors/handleGitError.d.ts +0 -1
  90. package/dist/runner/handleErrors/handleGitError.js +0 -27
  91. package/dist/runner/handleErrors/handleGitError.js.map +0 -1
  92. package/dist/runner/handleErrors/handleInstallError.d.ts +0 -1
  93. package/dist/runner/handleErrors/handleInstallError.js +0 -30
  94. package/dist/runner/handleErrors/handleInstallError.js.map +0 -1
  95. package/dist/src/__tests__/versionUtil.test.d.ts +0 -1
  96. package/dist/src/__tests__/versionUtil.test.js +0 -80
  97. package/dist/src/__tests__/versionUtil.test.js.map +0 -1
  98. package/dist/src/buildGraph/GraphError.d.ts +0 -11
  99. package/dist/src/buildGraph/GraphError.js +0 -11
  100. package/dist/src/buildGraph/GraphError.js.map +0 -1
  101. package/dist/src/buildGraph/buildGraph.d.ts +0 -8
  102. package/dist/src/buildGraph/buildGraph.js +0 -100
  103. package/dist/src/buildGraph/buildGraph.js.map +0 -1
  104. package/dist/src/buildGraph/index.d.ts +0 -2
  105. package/dist/src/buildGraph/index.js +0 -8
  106. package/dist/src/buildGraph/index.js.map +0 -1
  107. package/dist/src/git/__tests__/parseGitUrl.test.d.ts +0 -1
  108. package/dist/src/git/__tests__/parseGitUrl.test.js +0 -30
  109. package/dist/src/git/__tests__/parseGitUrl.test.js.map +0 -1
  110. package/dist/src/versionUtil.d.ts +0 -13
  111. package/dist/src/versionUtil.js +0 -79
  112. package/dist/src/versionUtil.js.map +0 -1
  113. package/runner/handleErrors/handleBuildGraphError.ts +0 -49
  114. package/runner/handleErrors/handleGitError.ts +0 -28
  115. package/runner/handleErrors/handleInstallError.ts +0 -26
  116. package/src/__tests__/versionUtil.test.ts +0 -96
  117. package/src/buildGraph/GraphError.ts +0 -12
  118. package/src/buildGraph/buildGraph.ts +0 -150
  119. package/src/buildGraph/index.ts +0 -2
  120. package/src/versionUtil.ts +0 -97
package/src/sv.ts CHANGED
@@ -1,153 +1,333 @@
1
1
  import path from 'path';
2
- import { pkgJSONManager } from "./pkgJSONManager";
3
- import { buildGraph } from "./buildGraph";
4
- import { RunOptions } from "./runOptions";
5
- import { versionUtil } from "./versionUtil";
6
- import { git, GitError } from "./git";
2
+ import { readFile } from 'fs/promises';
3
+ import { pkgJSONManager } from './pkgJSONManager';
4
+ import {
5
+ PubGrub,
6
+ IOverride,
7
+ TDependencies,
8
+ TPubGrubResult,
9
+ } from './PubGrub';
10
+ import { EntryStore } from './entryStore';
11
+ import { RunOptions } from './runOptions';
12
+ import { versionUtil } from './versionUtil';
13
+ import { git, GitError } from './git';
14
+ import { SVError } from './errors';
15
+
16
+ /*
17
+ * Правка package.json родителя (внутри его сабмодуля): parent-level
18
+ * мутации обязаны пережить публикацию родителя, поэтому диапазон
19
+ * пишется в его поле sv.
20
+ */
21
+ const updateModuleJson = async (
22
+ parent: string,
23
+ override: IOverride,
24
+ ): Promise<void> => {
25
+ const parentJson = await pkgJSONManager.read(parent);
26
+
27
+ if (!parentJson) throw new SVError('ADDON_NOT_FOUND', { name: parent });
28
+
29
+ const sv = { ...((parentJson.sv || {}) as TDependencies) };
30
+
31
+ if (override.add) sv[override.add] = override.version || '*';
32
+
33
+ if (override.delete) {
34
+ const key = Object.keys(sv)
35
+ .find(depUrl => git.parseUrl(depUrl).name === override.delete);
36
+
37
+ if (!key) throw new SVError('ADDON_NOT_FOUND', { name: override.delete });
38
+
39
+ delete sv[key];
40
+ }
41
+
42
+ parentJson.sv = sv;
43
+ await pkgJSONManager.write(parentJson, parent);
44
+ };
45
+
46
+ /*
47
+ * modulesDir должен быть npm-workspace, иначе сабмодули не попадут
48
+ * в node_modules. Поддерживаем оба формата: workspaces: [] и
49
+ * workspaces: { packages: [] }. Возвращает true, если json изменён.
50
+ */
51
+ const ensureWorkspaces = (baseJson: any): boolean => {
52
+ const entry = `${RunOptions.modulesDir}/*`;
53
+
54
+ if (Array.isArray(baseJson.workspaces)) {
55
+ if (baseJson.workspaces.includes(entry)) return false;
56
+
57
+ baseJson.workspaces.push(entry);
58
+
59
+ return true;
60
+ }
61
+
62
+ const packages = baseJson.workspaces?.packages;
63
+
64
+ if (Array.isArray(packages)) {
65
+ if (packages.includes(entry)) return false;
66
+
67
+ packages.push(entry);
68
+
69
+ return true;
70
+ }
71
+
72
+ baseJson.workspaces = [entry];
73
+
74
+ return true;
75
+ };
76
+
77
+ const MODULES_DIR_FIELD = 'sv-dir';
78
+ const DEFAULT_MODULES_DIR = 'modules';
79
+
80
+ /*
81
+ * Уже добавленные сабмодули — источник правды о директории:
82
+ * .gitmodules с единым родителем у всех path (addons/X, addons/Y → addons).
83
+ */
84
+ const deriveModulesDir = async (): Promise<string | null> => {
85
+ try {
86
+ const content = await readFile(
87
+ path.join(RunOptions.cwd, '.gitmodules'),
88
+ 'utf-8',
89
+ );
90
+
91
+ const dirs = new Set(
92
+ [...content.matchAll(/^\s*path\s*=\s*(.+)$/gm)]
93
+ .map(match => match[1].trim().split('/')[0]),
94
+ );
95
+
96
+ return dirs.size === 1 ? [...dirs][0] : null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ };
7
101
 
8
102
  export class SV {
103
+ private resolution: TPubGrubResult | null = null;
104
+
105
+ private store = new EntryStore();
106
+
107
+ private explicitModulesDir?: string;
108
+
9
109
  constructor (
10
110
  projectDir: string,
11
- modulesDir: string = 'addons',
111
+ modulesDir?: string,
112
+ options: { githubToken?: string } = {},
12
113
  ) {
13
114
  RunOptions.cwd = projectDir;
14
- RunOptions.modulesDir = modulesDir;
115
+ RunOptions.modulesDir = modulesDir || DEFAULT_MODULES_DIR;
116
+ this.explicitModulesDir = modulesDir;
117
+ git.api.setToken(options.githubToken);
15
118
  }
16
119
 
17
- public buildGraph = buildGraph;
120
+ /*
121
+ * Директория модулей: явно переданная (CLI/JS) → записанная в
122
+ * package.json#sv-dir → выведенная из .gitmodules → 'modules'.
123
+ * Эффективная записывается в sv-dir. Возвращает true, если json изменён.
124
+ */
125
+ private syncModulesDir = async (baseJson: any): Promise<boolean> => {
126
+ const recorded = baseJson[MODULES_DIR_FIELD] as string | undefined;
18
127
 
19
- // eslint-disable-next-line class-methods-use-this
20
- public install = async (gitUrl: string, parentModule?: string) => {
21
- const { url, version, name } = git.parseUrl(gitUrl);
128
+ if (!this.explicitModulesDir) {
129
+ if (recorded) {
130
+ RunOptions.modulesDir = recorded;
22
131
 
23
- if (!name) {
24
- throw new Error('NOT_A_GIT_URL');
25
- }
132
+ return false;
133
+ }
26
134
 
27
- const targetPkg = await pkgJSONManager.read(parentModule);
28
- const installedPkg = await pkgJSONManager.read(name);
135
+ const derived = await deriveModulesDir();
29
136
 
30
- if (!installedPkg) {
31
- await git.addSumbmodule(url);
137
+ if (derived) RunOptions.modulesDir = derived;
32
138
  }
33
139
 
34
- const versions = await git.listVersions(name);
35
- const latestVersion = versionUtil.latest(versions);
36
- const requestedVersion = version || latestVersion;
140
+ if (recorded === RunOptions.modulesDir) return false;
37
141
 
38
- if (!versions.includes(requestedVersion) && versions.length) {
39
- throw new Error('REQUESTED_VERSION_NOT_EXISTS');
40
- }
41
-
42
- if (!targetPkg.sv) {
43
- targetPkg.sv = {};
44
- }
142
+ baseJson[MODULES_DIR_FIELD] = RunOptions.modulesDir;
45
143
 
46
- targetPkg.sv[url] = requestedVersion ? `^${requestedVersion}` : '*';
47
- await pkgJSONManager.write(targetPkg, parentModule);
48
- await buildGraph();
144
+ return true;
49
145
  };
50
146
 
51
- // eslint-disable-next-line class-methods-use-this
52
- public update = async () => {
53
- const graph = await buildGraph();
54
- if (!graph) return;
55
- await Promise.all(Object.entries(graph).map(async ([name, data]) => {
56
- const { version, used } = data;
57
- const latestVersion = versionUtil.latest(used);
58
- if (latestVersion !== version) {
59
- await git.checkout(name, latestVersion);
147
+ private resolveDeps = (
148
+ rootDeps: TDependencies,
149
+ ): Promise<TPubGrubResult> => new PubGrub(this.store).resolve(rootDeps);
150
+
151
+ /*
152
+ * Синк рабочей копии с резолвом: недостающие сабмодули добавляются,
153
+ * версии переключаются, исчезнувшие из резолва модули удаляются.
154
+ */
155
+ private syncModules = async (resolution: TPubGrubResult): Promise<void> => {
156
+ const previous = this.resolution || {};
157
+
158
+ await Promise.all(Object.entries(resolution).map(async ([name, entry]) => {
159
+ const dir = path.join(RunOptions.modulesDir, name);
160
+
161
+ if (!await git.local.isGitRepo(dir)) {
162
+ await git.local.addSubmodule(entry.url);
163
+ }
164
+
165
+ if (await git.local.currentVersion(name) !== entry.version) {
166
+ await git.local.checkout(name, entry.version);
60
167
  }
61
168
  }));
169
+
170
+ const removed = Object.keys(previous).filter(name => !resolution[name]);
171
+
172
+ await Promise.all(removed.map(name => git.local.rm(name)));
62
173
  };
63
174
 
64
175
  /*
65
- * Устанавливает конкретную версию аддона: checkout на тег + запись constraint'а
66
- * в package.json проекта (иначе buildGraph вернёт latest(used)).
67
- * constraint по умолчанию — точный пин выбранной версии; при обновлении до
68
- * последней имеет смысл передать `^<version>`, чтобы новые версии продолжали приходить.
176
+ * Общий путь мутаций: проверка через resolve внутри simulate, и только
177
+ * если ошибки нет установка/переключение модулей и правка package.json
178
+ * (корня или родителя).
69
179
  */
70
- // eslint-disable-next-line class-methods-use-this
71
- public setVersion = async (name: string, version: string, constraint?: string) => {
72
- const versions = await git.listVersions(name);
73
-
74
- if (!versions.includes(version)) {
75
- throw new Error('REQUESTED_VERSION_NOT_EXISTS');
180
+ private apply = async (
181
+ baseJson: any,
182
+ override: IOverride,
183
+ parent?: string,
184
+ ): Promise<void> => {
185
+ const dirChanged = await this.syncModulesDir(baseJson);
186
+ const rootDeps = (baseJson.sv || {}) as TDependencies;
187
+ let nextRootDeps = rootDeps;
188
+
189
+ const resolution = await this.store.simulate(
190
+ [override],
191
+ rootDeps,
192
+ deps => {
193
+ nextRootDeps = deps;
194
+
195
+ return this.resolveDeps(deps);
196
+ },
197
+ );
198
+
199
+ await this.syncModules(resolution);
200
+
201
+ if (parent) {
202
+ await updateModuleJson(parent, override);
203
+
204
+ if (dirChanged) await pkgJSONManager.write(baseJson);
205
+ } else {
206
+ baseJson.sv = nextRootDeps;
207
+ await pkgJSONManager.write(baseJson);
76
208
  }
77
209
 
78
- const dir = path.join(RunOptions.modulesDir, name);
79
- const dirty = await git.hasChanges(dir);
210
+ this.resolution = resolution;
211
+ };
80
212
 
81
- /*
82
- * checkout с локальными изменениями транзакционен: git переносит их на
83
- * новую версию, а если перенести нельзя — отказывается, НЕ трогая дерево.
84
- * Значит при ошибке мы гарантированно остаёмся на исходной версии с теми
85
- * же изменениями — сообщаем, что перенос нужно сделать вручную.
86
- */
87
- try {
88
- await git.checkout(name, version);
89
- } catch (e) {
90
- if (dirty) {
91
- throw new GitError('GIT_DIRTY_SWITCH_CONFLICT');
92
- }
213
+ public init = async (): Promise<void> => {
214
+ const baseJson = await pkgJSONManager.read();
215
+
216
+ if (!baseJson) throw new SVError('NOT_A_NPM');
93
217
 
94
- throw e;
218
+ if (!await git.local.isGitRepo(RunOptions.cwd)) {
219
+ throw new SVError('NOT_A_GIT_REPO');
95
220
  }
96
221
 
97
- const json = await pkgJSONManager.read();
98
- if (!json?.sv) return;
222
+ const dirChanged = await this.syncModulesDir(baseJson);
99
223
 
100
- const key = Object.keys(json.sv).find(k => k.endsWith(`${name}.git`));
101
- const nextConstraint = constraint || version;
102
- if (key && json.sv[key] !== nextConstraint) {
103
- json.sv[key] = nextConstraint;
104
- await pkgJSONManager.write(json);
224
+ if (ensureWorkspaces(baseJson) || dirChanged) {
225
+ await pkgJSONManager.write(baseJson);
105
226
  }
227
+
228
+ const rootDeps = (baseJson.sv || {}) as TDependencies;
229
+ const resolution = await this.resolveDeps(rootDeps);
230
+
231
+ await this.syncModules(resolution);
232
+
233
+ this.resolution = resolution;
234
+ };
235
+
236
+ public getResolution = (): TPubGrubResult => {
237
+ if (!this.resolution) throw new SVError('NOT_INITIALIZED');
238
+
239
+ return this.resolution;
106
240
  };
107
241
 
108
242
  /*
109
- * Constraint корневого проекта на аддон (ключ в sv ищем по имени, как в remove).
243
+ * Установка/смена версии модуля. Первый аргумент url репозитория
244
+ * или имя уже установленного модуля. Без parentName — в корне проекта,
245
+ * без version — максимально возможная ('*'). Второй аргумент —
246
+ * версия, если это валидный semver-диапазон, иначе имя родителя.
110
247
  */
111
- // eslint-disable-next-line class-methods-use-this
112
- public getConstraint = async (name: string): Promise<string | null> => {
113
- const json = await pkgJSONManager.read();
114
- if (!json?.sv) return null;
248
+ public put = async (
249
+ url: string,
250
+ versionOrParent?: string,
251
+ parentName?: string,
252
+ ): Promise<void> => {
253
+ const isVersion = !!versionOrParent
254
+ && versionUtil.validate(versionOrParent, true);
115
255
 
116
- const key = Object.keys(json.sv).find(k => k.endsWith(`${name}.git`));
256
+ const range = (isVersion ? versionOrParent : undefined) || '*';
257
+ const parent = (isVersion ? parentName : versionOrParent) || undefined;
258
+ const baseJson = await pkgJSONManager.read();
117
259
 
118
- return key ? json.sv[key] : null;
119
- };
260
+ if (!baseJson) throw new SVError('NOT_A_NPM');
120
261
 
121
- // eslint-disable-next-line class-methods-use-this
122
- public setConstraint = async (name: string, constraint: string): Promise<void> => {
123
- const json = await pkgJSONManager.read();
124
- if (!json?.sv) return;
262
+ /*
263
+ * Вместо url можно передать имя уже установленного модуля
264
+ * url берём из текущего резолва.
265
+ */
266
+ let moduleUrl = url;
267
+
268
+ if (!git.parseUrl(url).name) {
269
+ const existing = this.getResolution()[url];
270
+
271
+ if (!existing) throw new SVError('NOT_A_GIT_URL', { url });
125
272
 
126
- const key = Object.keys(json.sv).find(k => k.endsWith(`${name}.git`));
273
+ moduleUrl = existing.url;
274
+ }
127
275
 
128
- if (key && json.sv[key] !== constraint) {
129
- json.sv[key] = constraint;
130
- await pkgJSONManager.write(json);
276
+ if (parent && !this.getResolution()[parent]) {
277
+ throw new SVError('ADDON_NOT_FOUND', { name: parent });
131
278
  }
279
+
280
+ await this.apply(
281
+ baseJson,
282
+ { parent, add: moduleUrl, version: range },
283
+ parent,
284
+ );
132
285
  };
133
286
 
134
- // eslint-disable-next-line class-methods-use-this
135
- public remove = async (submoduleName: string, parentModule?: string) => {
136
- await git.rm(submoduleName);
137
- const json = await pkgJSONManager.read(parentModule);
287
+ /* Удаление модуля из родителя (без parentName — из корня проекта). */
288
+ public delete = async (name: string, parentName?: string): Promise<void> => {
289
+ const baseJson = await pkgJSONManager.read();
138
290
 
139
- if (!json.sv) return;
291
+ if (!baseJson) throw new SVError('NOT_A_NPM');
292
+
293
+ if (parentName) {
294
+ const parentEntry = this.getResolution()[parentName];
295
+
296
+ if (!parentEntry) {
297
+ throw new SVError('ADDON_NOT_FOUND', { name: parentName });
298
+ }
140
299
 
141
- json.sv = Object.keys(json.sv).reduce((acc, k) => {
142
- if (k.endsWith(`${submoduleName}.git`)) {
143
- return acc;
300
+ if (!(name in parentEntry.dependencies)) {
301
+ throw new SVError('ADDON_NOT_FOUND', { name });
144
302
  }
303
+ }
304
+
305
+ await this.apply(
306
+ baseJson,
307
+ { parent: parentName, delete: name },
308
+ parentName,
309
+ );
310
+ };
311
+
312
+ public listVersions = (
313
+ name?: string,
314
+ ): Record<string, string[]> | string[] => {
315
+ const resolution = this.getResolution();
316
+
317
+ if (name) {
318
+ const entry = resolution[name];
145
319
 
146
- return { ...acc, [k]: json.sv[k] };
147
- }, {});
320
+ if (!entry) throw new SVError('ADDON_NOT_FOUND', { name });
148
321
 
149
- await pkgJSONManager.write(json, parentModule);
150
- await buildGraph();
322
+ return versionUtil.sort(Object.keys(entry.versions));
323
+ }
324
+
325
+ return Object.fromEntries(
326
+ Object.entries(resolution).map(([entryName, entry]) => [
327
+ entryName,
328
+ versionUtil.sort(Object.keys(entry.versions)),
329
+ ]),
330
+ );
151
331
  };
152
332
 
153
333
  // eslint-disable-next-line class-methods-use-this
@@ -156,11 +336,9 @@ export class SV {
156
336
  ? path.join(RunOptions.modulesDir, module)
157
337
  : RunOptions.cwd;
158
338
 
159
- /* Сначала дешёвая проверка рабочей копии; если чисто — смотрим,
160
- * нет ли незапушенных коммитов (идёт fetch в remote) */
161
- if (await git.hasChanges(dir)) return true;
339
+ if (await git.local.hasChanges(dir)) return true;
162
340
 
163
- return git.hasUnpushedCommits(dir);
341
+ return git.local.hasUnpushedCommits(dir);
164
342
  };
165
343
 
166
344
  // eslint-disable-next-line class-methods-use-this
@@ -169,7 +347,7 @@ export class SV {
169
347
  ? path.join(RunOptions.modulesDir, module)
170
348
  : RunOptions.cwd;
171
349
 
172
- return git.hasChanges(dir);
350
+ return git.local.hasChanges(dir);
173
351
  };
174
352
 
175
353
  // eslint-disable-next-line class-methods-use-this
@@ -178,7 +356,7 @@ export class SV {
178
356
  ? path.join(RunOptions.modulesDir, module)
179
357
  : RunOptions.cwd;
180
358
 
181
- const tags = await git.tagsAtHead(dir);
359
+ const tags = await git.local.tagsAtHead(dir);
182
360
 
183
361
  return versionUtil.latest(tags) || null;
184
362
  };
@@ -189,7 +367,7 @@ export class SV {
189
367
  ? path.join(RunOptions.modulesDir, module)
190
368
  : RunOptions.cwd;
191
369
 
192
- const versions = await git.listTags(dir);
370
+ const versions = await git.local.listTags(dir);
193
371
 
194
372
  return versionUtil.latest(versions) || '0.0.0';
195
373
  };
@@ -200,7 +378,7 @@ export class SV {
200
378
  ? path.join(RunOptions.modulesDir, module)
201
379
  : RunOptions.cwd;
202
380
 
203
- return git.getRemote(dir);
381
+ return git.local.getRemote(dir);
204
382
  };
205
383
 
206
384
  public isPublished = async (module?: string): Promise<boolean> => {
@@ -208,11 +386,31 @@ export class SV {
208
386
  ? path.join(RunOptions.modulesDir, module)
209
387
  : RunOptions.cwd;
210
388
 
211
- if (!await git.isGitRepo(dir)) return false;
389
+ if (!await git.local.isGitRepo(dir)) return false;
212
390
 
213
391
  return !!(await this.getRemote(module));
214
392
  };
215
393
 
394
+ // eslint-disable-next-line class-methods-use-this
395
+ public isRemoteAhead = async (module?: string): Promise<boolean> => {
396
+ const dir = module
397
+ ? path.join(RunOptions.modulesDir, module)
398
+ : RunOptions.cwd;
399
+
400
+ if (!await git.local.isGitRepo(dir)) return false;
401
+
402
+ return git.local.isRemoteAhead(dir);
403
+ };
404
+
405
+ // eslint-disable-next-line class-methods-use-this
406
+ public pullRebaseAutostash = async (module?: string): Promise<void> => {
407
+ const dir = module
408
+ ? path.join(RunOptions.modulesDir, module)
409
+ : RunOptions.cwd;
410
+
411
+ return git.local.pullRebaseAutostash(dir);
412
+ };
413
+
216
414
  public publish = async (opts: {
217
415
  module?: string,
218
416
  repoUrl?: string,
@@ -220,24 +418,19 @@ export class SV {
220
418
  bump: 'release' | 'minor' | 'major',
221
419
  }): Promise<string> => {
222
420
  const { module, repoUrl, message, bump } = opts;
421
+
223
422
  const dir = module
224
423
  ? path.join(RunOptions.modulesDir, module)
225
424
  : RunOptions.cwd;
226
425
 
227
426
  if (!await this.isPublished(module)) {
228
- if (!repoUrl) {
229
- throw new GitError('GIT_REPO_URL_REQUIRED');
230
- }
427
+ if (!repoUrl) throw new GitError('GIT_REPO_URL_REQUIRED');
231
428
 
232
- if (!await git.isGitRepo(dir)) {
233
- await git.init(dir);
234
- }
429
+ if (!await git.local.isGitRepo(dir)) await git.local.init(dir);
235
430
 
236
- await git.addRemote(dir, repoUrl);
431
+ await git.local.addRemote(dir, repoUrl);
237
432
  }
238
433
 
239
- /* Публикация не последней версии запрещена: HEAD на теге, отличном
240
- * от последнего, — обновлять такую базу можно только вручную */
241
434
  const currentTag = await this.currentVersion(module);
242
435
  const latestKnown = await this.latestVersion(module);
243
436
 
@@ -245,41 +438,35 @@ export class SV {
245
438
  throw new GitError('GIT_NOT_LATEST_VERSION');
246
439
  }
247
440
 
248
- /* Аддоны checkout'нуты на тег: поднимаем ветку до sync/commit,
249
- * иначе коммиты и push уйдут в detached HEAD */
250
- await git.ensureBranch(dir);
441
+ await git.local.ensureBranch(dir);
251
442
 
252
- if (await git.isRemoteAhead(dir)) {
253
- await git.pullRebaseAutostash(dir);
443
+ if (await git.local.isRemoteAhead(dir)) {
444
+ await git.local.pullRebaseAutostash(dir);
254
445
  }
255
446
 
256
- const dirty = await git.hasChanges(dir);
447
+ const dirty = await git.local.hasChanges(dir);
257
448
  const headTag = await this.currentVersion(module);
258
449
 
259
- /* Дерево чистое и HEAD уже отмечен версионным тегом:
260
- * коммитить и бампить нечего — просто доставляем ветку и тег на remote */
261
450
  if (!dirty && headTag) {
262
- await git.push(dir, headTag);
451
+ await git.local.push(dir, headTag);
263
452
 
264
453
  return headTag;
265
454
  }
266
455
 
267
456
  const next = versionUtil.bump(await this.latestVersion(module), bump);
268
-
269
- /* Фиксируем версию и в package.json, чтобы она не расходилась с git-тегом */
270
457
  const pkg = await pkgJSONManager.read(module);
458
+
271
459
  if (pkg) {
272
460
  pkg.version = next;
273
461
  await pkgJSONManager.write(pkg, module);
274
462
  }
275
463
 
276
- /* Перечитываем: запись версии в package.json сама по себе даёт изменения */
277
- if (await git.hasChanges(dir)) {
278
- await git.commitAll(dir, message || 'Update');
464
+ if (await git.local.hasChanges(dir)) {
465
+ await git.local.commitAll(dir, message || 'Update');
279
466
  }
280
467
 
281
- await git.addTag(dir, next);
282
- await git.push(dir, next);
468
+ await git.local.addTag(dir, next);
469
+ await git.local.push(dir, next);
283
470
 
284
471
  return next;
285
472
  };
@@ -0,0 +1,62 @@
1
+ import { versionUtil } from '../versionUtil';
2
+
3
+ describe('versionUtil: SemVer primitives', () => {
4
+ it('validates exact versions and ranges separately', () => {
5
+ expect(versionUtil.validate('1.2.3')).toBe(true);
6
+ expect(versionUtil.validate('1.2.3-beta.1')).toBe(true);
7
+ expect(versionUtil.validate('v1.2.3')).toBe(false);
8
+ expect(versionUtil.validate('^1.2.3')).toBe(false);
9
+ expect(versionUtil.validate('^1.2.3', true)).toBe(true);
10
+ expect(versionUtil.validate('~1.2.3', true)).toBe(true);
11
+ expect(versionUtil.validate('*', true)).toBe(true);
12
+ expect(versionUtil.validate('', true)).toBe(false);
13
+ expect(versionUtil.validate('not-a-range', true)).toBe(false);
14
+ });
15
+
16
+ it('uses standard caret semantics for zero versions', () => {
17
+ expect(versionUtil.satisfies('1.9.0', '^1.2.3')).toBe(true);
18
+ expect(versionUtil.satisfies('2.0.0', '^1.2.3')).toBe(false);
19
+ expect(versionUtil.satisfies('0.2.9', '^0.2.3')).toBe(true);
20
+ expect(versionUtil.satisfies('0.3.0', '^0.2.3')).toBe(false);
21
+ expect(versionUtil.satisfies('0.0.3', '^0.0.3')).toBe(true);
22
+ expect(versionUtil.satisfies('0.0.4', '^0.0.3')).toBe(false);
23
+ });
24
+
25
+ it('selects the highest version satisfying every constraint', () => {
26
+ const versions = ['2.0.0', '1.4.2', '1.3.2', 'broken'];
27
+
28
+ expect(versionUtil.sort(versions)).toEqual(['2.0.0', '1.4.2', '1.3.2']);
29
+ expect(versionUtil.select(versions, ['^1.3.2', '^1.4.0']))
30
+ .toEqual(['1.4.2']);
31
+ expect(versionUtil.latest(versions, ['^1.0.0'])).toBe('1.4.2');
32
+ expect(versionUtil.compare('1.2.0', '1.1.9')).toBeGreaterThan(0);
33
+ });
34
+ });
35
+
36
+ describe('versionUtil: finite sets used by PubGrub', () => {
37
+ const one = ['1.0.0', '1.2.0'];
38
+ const two = ['1.2.0', '2.0.0'];
39
+
40
+ it('intersects, joins and subtracts version sets', () => {
41
+ expect(versionUtil.intersection(one, two)).toEqual(['1.2.0']);
42
+ expect(versionUtil.union(one, two)).toEqual(['2.0.0', '1.2.0', '1.0.0']);
43
+ expect(versionUtil.difference(one, two)).toEqual(['1.0.0']);
44
+ });
45
+
46
+ it('checks intersection and subset relations', () => {
47
+ expect(versionUtil.intersects(one, two)).toBe(true);
48
+ expect(versionUtil.intersects(['1.0.0'], ['2.0.0'])).toBe(false);
49
+ expect(versionUtil.isSubset(['1.2.0'], one)).toBe(true);
50
+ expect(versionUtil.isSubset(one, ['1.2.0'])).toBe(false);
51
+ });
52
+ });
53
+
54
+ describe('versionUtil.bump', () => {
55
+ it.each([
56
+ ['release', '1.2.4'],
57
+ ['minor', '1.3.0'],
58
+ ['major', '2.0.0'],
59
+ ] as const)('bumps %s', (kind, expected) => {
60
+ expect(versionUtil.bump('1.2.3', kind)).toBe(expected);
61
+ });
62
+ });
@@ -0,0 +1,2 @@
1
+ export { versionUtil } from './versionUtil';
2
+ export type { TVersionConstraint } from './versionUtil';