submodule-version 2.4.1 → 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 +98 -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 +16 -8
  52. package/dist/src/sv.js +222 -143
  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 +304 -146
  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];
125
270
 
126
- const key = Object.keys(json.sv).find(k => k.endsWith(`${name}.git`));
271
+ if (!existing) throw new SVError('NOT_A_GIT_URL', { url });
127
272
 
128
- if (key && json.sv[key] !== constraint) {
129
- json.sv[key] = constraint;
130
- await pkgJSONManager.write(json);
273
+ moduleUrl = existing.url;
131
274
  }
275
+
276
+ if (parent && !this.getResolution()[parent]) {
277
+ throw new SVError('ADDON_NOT_FOUND', { name: parent });
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');
140
292
 
141
- json.sv = Object.keys(json.sv).reduce((acc, k) => {
142
- if (k.endsWith(`${submoduleName}.git`)) {
143
- return acc;
293
+ if (parentName) {
294
+ const parentEntry = this.getResolution()[parentName];
295
+
296
+ if (!parentEntry) {
297
+ throw new SVError('ADDON_NOT_FOUND', { name: parentName });
298
+ }
299
+
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 });
321
+
322
+ return versionUtil.sort(Object.keys(entry.versions));
323
+ }
148
324
 
149
- await pkgJSONManager.write(json, parentModule);
150
- await buildGraph();
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,38 +386,29 @@ 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
 
216
- /*
217
- * Remote-ветка впереди локальной (fetch + rev-list HEAD..@{u}).
218
- * Нет репозитория/remote/upstream — обновлений нет.
219
- */
220
394
  // eslint-disable-next-line class-methods-use-this
221
395
  public isRemoteAhead = async (module?: string): Promise<boolean> => {
222
396
  const dir = module
223
397
  ? path.join(RunOptions.modulesDir, module)
224
398
  : RunOptions.cwd;
225
399
 
226
- if (!await git.isGitRepo(dir)) return false;
400
+ if (!await git.local.isGitRepo(dir)) return false;
227
401
 
228
- return git.isRemoteAhead(dir);
402
+ return git.local.isRemoteAhead(dir);
229
403
  };
230
404
 
231
- /*
232
- * Догоняем remote: pull --rebase --autostash. При конфликте всё
233
- * восстанавливается как было (rebase --abort + stash pop) и кидается
234
- * GIT_SYNC_CONFLICT — дальше возможен только ручной ребейз.
235
- */
236
405
  // eslint-disable-next-line class-methods-use-this
237
406
  public pullRebaseAutostash = async (module?: string): Promise<void> => {
238
407
  const dir = module
239
408
  ? path.join(RunOptions.modulesDir, module)
240
409
  : RunOptions.cwd;
241
410
 
242
- return git.pullRebaseAutostash(dir);
411
+ return git.local.pullRebaseAutostash(dir);
243
412
  };
244
413
 
245
414
  public publish = async (opts: {
@@ -249,24 +418,19 @@ export class SV {
249
418
  bump: 'release' | 'minor' | 'major',
250
419
  }): Promise<string> => {
251
420
  const { module, repoUrl, message, bump } = opts;
421
+
252
422
  const dir = module
253
423
  ? path.join(RunOptions.modulesDir, module)
254
424
  : RunOptions.cwd;
255
425
 
256
426
  if (!await this.isPublished(module)) {
257
- if (!repoUrl) {
258
- throw new GitError('GIT_REPO_URL_REQUIRED');
259
- }
427
+ if (!repoUrl) throw new GitError('GIT_REPO_URL_REQUIRED');
260
428
 
261
- if (!await git.isGitRepo(dir)) {
262
- await git.init(dir);
263
- }
429
+ if (!await git.local.isGitRepo(dir)) await git.local.init(dir);
264
430
 
265
- await git.addRemote(dir, repoUrl);
431
+ await git.local.addRemote(dir, repoUrl);
266
432
  }
267
433
 
268
- /* Публикация не последней версии запрещена: HEAD на теге, отличном
269
- * от последнего, — обновлять такую базу можно только вручную */
270
434
  const currentTag = await this.currentVersion(module);
271
435
  const latestKnown = await this.latestVersion(module);
272
436
 
@@ -274,41 +438,35 @@ export class SV {
274
438
  throw new GitError('GIT_NOT_LATEST_VERSION');
275
439
  }
276
440
 
277
- /* Аддоны checkout'нуты на тег: поднимаем ветку до sync/commit,
278
- * иначе коммиты и push уйдут в detached HEAD */
279
- await git.ensureBranch(dir);
441
+ await git.local.ensureBranch(dir);
280
442
 
281
- if (await git.isRemoteAhead(dir)) {
282
- await git.pullRebaseAutostash(dir);
443
+ if (await git.local.isRemoteAhead(dir)) {
444
+ await git.local.pullRebaseAutostash(dir);
283
445
  }
284
446
 
285
- const dirty = await git.hasChanges(dir);
447
+ const dirty = await git.local.hasChanges(dir);
286
448
  const headTag = await this.currentVersion(module);
287
449
 
288
- /* Дерево чистое и HEAD уже отмечен версионным тегом:
289
- * коммитить и бампить нечего — просто доставляем ветку и тег на remote */
290
450
  if (!dirty && headTag) {
291
- await git.push(dir, headTag);
451
+ await git.local.push(dir, headTag);
292
452
 
293
453
  return headTag;
294
454
  }
295
455
 
296
456
  const next = versionUtil.bump(await this.latestVersion(module), bump);
297
-
298
- /* Фиксируем версию и в package.json, чтобы она не расходилась с git-тегом */
299
457
  const pkg = await pkgJSONManager.read(module);
458
+
300
459
  if (pkg) {
301
460
  pkg.version = next;
302
461
  await pkgJSONManager.write(pkg, module);
303
462
  }
304
463
 
305
- /* Перечитываем: запись версии в package.json сама по себе даёт изменения */
306
- if (await git.hasChanges(dir)) {
307
- await git.commitAll(dir, message || 'Update');
464
+ if (await git.local.hasChanges(dir)) {
465
+ await git.local.commitAll(dir, message || 'Update');
308
466
  }
309
467
 
310
- await git.addTag(dir, next);
311
- await git.push(dir, next);
468
+ await git.local.addTag(dir, next);
469
+ await git.local.push(dir, next);
312
470
 
313
471
  return next;
314
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';