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
@@ -0,0 +1,212 @@
1
+ import { SVError } from './errors';
2
+ import { git } from './git';
3
+ import type {
4
+ IEntrySource,
5
+ IOverride,
6
+ IPackageEntry,
7
+ TDependencies,
8
+ } from './PubGrub/types';
9
+
10
+ /*
11
+ * Оверрайды без parent применяются к корню: add добавляет/меняет
12
+ * диапазон по url, delete убирает зависимость по имени модуля.
13
+ */
14
+ const applyRootOverrides = (
15
+ overrides: IOverride[],
16
+ rootDeps: TDependencies,
17
+ ): TDependencies => {
18
+ const next = { ...rootDeps };
19
+
20
+ overrides.forEach(override => {
21
+ if (override.add) {
22
+ const { name } = git.parseUrl(override.add);
23
+
24
+ if (!name) {
25
+ throw new SVError('NOT_A_GIT_URL', { url: override.add });
26
+ }
27
+
28
+ next[override.add] = override.version || '*';
29
+ }
30
+
31
+ if (override.delete) {
32
+ const key = Object.keys(next)
33
+ .find(depUrl => git.parseUrl(depUrl).name === override.delete);
34
+
35
+ if (!key) {
36
+ throw new SVError('ADDON_NOT_FOUND', { name: override.delete });
37
+ }
38
+
39
+ delete next[key];
40
+ }
41
+ });
42
+
43
+ return next;
44
+ };
45
+
46
+ export class EntryStore implements IEntrySource {
47
+ private cache = new Map<string, Promise<IPackageEntry>>();
48
+
49
+ private urlByName = new Map<string, string>();
50
+
51
+ /* Активны только внутри simulate; вне коллбэка оверрайдов не существует */
52
+ private overrides: IOverride[] | null = null;
53
+
54
+ private rememberUrl = (name: string, url: string): void => {
55
+ const current = this.urlByName.get(name);
56
+
57
+ if (current && current !== url) {
58
+ throw new SVError('ADDON_NOT_FOUND', {
59
+ name,
60
+ urls: [current, url],
61
+ });
62
+ }
63
+
64
+ this.urlByName.set(name, url);
65
+ };
66
+
67
+ private normalizeDependencies = (
68
+ dependencies: TDependencies = {},
69
+ ): TDependencies => Object.fromEntries(
70
+ Object.entries(dependencies).map(([depUrl, constraint]) => {
71
+ const { name } = git.parseUrl(depUrl);
72
+
73
+ if (!name) {
74
+ throw new SVError('NOT_A_GIT_URL', { url: depUrl });
75
+ }
76
+
77
+ this.rememberUrl(name, depUrl);
78
+
79
+ return [name, constraint];
80
+ }),
81
+ );
82
+
83
+ private load = async (url: string, name: string): Promise<IPackageEntry> => {
84
+ const fetched = await git.api.fetchVersions(url);
85
+ const versions: Record<string, TDependencies> = {};
86
+
87
+ fetched.forEach(({ version, pkg }) => {
88
+ versions[version] = this.normalizeDependencies(pkg?.sv || {});
89
+ });
90
+
91
+ return { name, url, versions };
92
+ };
93
+
94
+ private resolveIdentity = (
95
+ depPath: string,
96
+ ): { name: string, url: string } => {
97
+ const parsed = git.parseUrl(depPath);
98
+
99
+ if (parsed.name) {
100
+ this.rememberUrl(parsed.name, parsed.url);
101
+
102
+ return { name: parsed.name, url: parsed.url };
103
+ }
104
+
105
+ const url = this.urlByName.get(depPath);
106
+
107
+ if (!url) {
108
+ throw new SVError('ADDON_NOT_FOUND', { name: depPath });
109
+ }
110
+
111
+ return { name: depPath, url };
112
+ };
113
+
114
+ /*
115
+ * Патчим клон — сырой энтри в кэше остаётся нетронутым, оверрайды
116
+ * не переживают simulate. add/delete применяются к каждой версии
117
+ * родителя: будто модуль установлен (или удалён) в любой версии.
118
+ */
119
+ private applyOverrides = (entry: IPackageEntry): IPackageEntry => {
120
+ const related = (this.overrides || [])
121
+ .filter(override => override.parent === entry.name);
122
+
123
+ if (!related.length) return entry;
124
+
125
+ const versions = Object.fromEntries(
126
+ Object.entries(entry.versions).map(([version, deps]) => {
127
+ const patched = { ...deps };
128
+
129
+ related.forEach(override => {
130
+ if (override.add) {
131
+ const { name } = git.parseUrl(override.add);
132
+
133
+ if (!name) {
134
+ throw new SVError('NOT_A_GIT_URL', { url: override.add });
135
+ }
136
+
137
+ this.rememberUrl(name, override.add);
138
+ patched[name] = override.version || '*';
139
+ }
140
+
141
+ if (override.delete) {
142
+ delete patched[override.delete];
143
+ }
144
+ });
145
+
146
+ return [version, patched];
147
+ }),
148
+ );
149
+
150
+ return { ...entry, versions };
151
+ };
152
+
153
+ /*
154
+ * Загружает только один модуль: сразу все его semver-теги и зависимости
155
+ * каждого тега. Сами подзависимости здесь не запрашиваются — PubGrub
156
+ * вызовет fetch(name), только если реально рассмотрит нужную версию.
157
+ */
158
+ public fetch = (depPath: string): Promise<IPackageEntry> => {
159
+ let identity: { name: string, url: string };
160
+
161
+ try {
162
+ identity = this.resolveIdentity(depPath);
163
+ } catch (error) {
164
+ return Promise.reject(error);
165
+ }
166
+
167
+ const cached = this.cache.get(identity.url);
168
+
169
+ if (cached) {
170
+ return this.overrides ? cached.then(this.applyOverrides) : cached;
171
+ }
172
+
173
+ const loading = this.load(identity.url, identity.name);
174
+
175
+ this.cache.set(identity.url, loading);
176
+ loading.catch(() => this.cache.delete(identity.url));
177
+
178
+ return this.overrides ? loading.then(this.applyOverrides) : loading;
179
+ };
180
+
181
+ /*
182
+ * Прогон коллбэка с потенциальными изменениями дерева. Оверрайды без
183
+ * parent превращаются в эффективные rootDeps и уходят аргументом
184
+ * в коллбэк, parent-оверрайды перезаписывают модули на всех fetch'ах
185
+ * внутри. После завершения (в т.ч. по ошибке) оверрайды снимаются.
186
+ */
187
+ public simulate = async <T>(
188
+ overrides: IOverride[],
189
+ rootDeps: TDependencies,
190
+ cb: (rootDeps: TDependencies) => Promise<T>,
191
+ ): Promise<T> => {
192
+ this.overrides = overrides.filter(override => override.parent);
193
+
194
+ try {
195
+ const rootOverrides = overrides.filter(override => !override.parent);
196
+
197
+ return await cb(applyRootOverrides(rootOverrides, rootDeps));
198
+ } finally {
199
+ this.overrides = null;
200
+ }
201
+ };
202
+
203
+ public urlOf = (name: string): string | undefined => this.urlByName.get(name);
204
+
205
+ /* Изоляция тестов и явное обновление remote-каталога. */
206
+ public clear = (): void => {
207
+ this.cache.clear();
208
+ this.urlByName.clear();
209
+ };
210
+ }
211
+
212
+ export const entryStore = new EntryStore();
package/src/errors.ts ADDED
@@ -0,0 +1,39 @@
1
+ export const ERR = {
2
+ NOT_A_NPM: 'NOT_A_NPM',
3
+ NOT_A_GIT_REPO: 'NOT_A_GIT_REPO',
4
+ NOT_A_GIT_URL: 'NOT_A_GIT_URL',
5
+ NOT_INITIALIZED: 'NOT_INITIALIZED',
6
+ NOT_IMPLEMENTED: 'NOT_IMPLEMENTED',
7
+ PATH_NOT_FOUND: 'PATH_NOT_FOUND',
8
+ ADDON_NOT_FOUND: 'ADDON_NOT_FOUND',
9
+ REQUESTED_VERSION_NOT_EXISTS: 'REQUESTED_VERSION_NOT_EXISTS',
10
+ VERSION_CONFLICT: 'VERSION_CONFLICT',
11
+ CIRCULAR_DEPENDENCY: 'CIRCULAR_DEPENDENCY',
12
+ UNKNOWN_VERSION: 'UNKNOWN_VERSION',
13
+ GIT_DIRTY_SWITCH_CONFLICT: 'GIT_DIRTY_SWITCH_CONFLICT',
14
+ } as const;
15
+
16
+ export type TSVErrorCode = keyof typeof ERR;
17
+
18
+ export type TSVErrorDetails = {
19
+ /* Путь от корня проекта до блокирующего аддона (имена узлов) */
20
+ chain?: string[];
21
+ requestedVersion?: string;
22
+ installedVersion?: string;
23
+ parents?: Record<string, string>;
24
+ versions?: string[];
25
+ [k: string]: unknown;
26
+ };
27
+
28
+ /*
29
+ * Единственный тип ошибки, который уходит наружу из методов SV.
30
+ * GitError остаётся во внутреннем git-слое.
31
+ */
32
+ export class SVError extends Error {
33
+ constructor (
34
+ public error: TSVErrorCode,
35
+ public details: TSVErrorDetails = {},
36
+ ) {
37
+ super(error);
38
+ }
39
+ }
@@ -0,0 +1,5 @@
1
+ export class GithubError extends Error {
2
+ constructor (type: string, public details: Record<string, any> = {}) {
3
+ super(type);
4
+ }
5
+ }
@@ -0,0 +1,180 @@
1
+ import { versionUtil } from '../../versionUtil';
2
+ import { GithubError } from './GithubError';
3
+
4
+ /*
5
+ * GitHub GraphQL API без внешних зависимостей (глобальный fetch, Node 18+).
6
+ * Токен обязателен (GraphQL не работает анонимно): из JS API
7
+ * (git.api.setToken) или GITHUB_TOKEN из env.
8
+ */
9
+ const ENDPOINT = 'https://api.github.com/graphql';
10
+
11
+ /*
12
+ * owner/repo из git-url'а: https/ssh (github.com:owner/repo.git)
13
+ * и github:-shorthand (github:owner/repo).
14
+ */
15
+ const parseSlug = (url: string): string | null => {
16
+ const shorthand = /^github:(?<slug>[\w.-]+\/[\w.-]+)$/.exec(url);
17
+
18
+ if (shorthand?.groups) return shorthand.groups.slug;
19
+
20
+ const match = /github\.com[/:](?<slug>[\w.-]+\/[\w.-]+?)(\.git)?$/.exec(url);
21
+
22
+ return match?.groups?.slug || null;
23
+ };
24
+
25
+ /* Токен, переданный через JS API (SV); без него — GITHUB_TOKEN из env */
26
+ let apiToken: string | undefined;
27
+
28
+ interface IGqlError {
29
+ type?: string;
30
+ message: string;
31
+ }
32
+
33
+ const gql = async (
34
+ query: string,
35
+ variables: Record<string, unknown>,
36
+ ): Promise<any> => {
37
+ const token = apiToken || process.env.GITHUB_TOKEN;
38
+
39
+ if (!token) {
40
+ throw new GithubError('GITHUB_TOKEN_REQUIRED', {});
41
+ }
42
+
43
+ const res = await fetch(ENDPOINT, {
44
+ method: 'POST',
45
+ headers: {
46
+ Accept: 'application/vnd.github+json',
47
+ 'User-Agent': 'submodule-version',
48
+ Authorization: `Bearer ${token}`,
49
+ },
50
+ body: JSON.stringify({ query, variables }),
51
+ });
52
+
53
+ if (!res.ok) {
54
+ throw new GithubError('GITHUB_API_FAILED', { status: res.status });
55
+ }
56
+
57
+ const body = await res.json() as {
58
+ data?: any,
59
+ errors?: IGqlError[],
60
+ };
61
+
62
+ if (body.errors?.length) {
63
+ /* NOT_FOUND = репо не существует или нет доступа (как 404 в REST) */
64
+ const notFound = body.errors.some(e => e.type === 'NOT_FOUND');
65
+
66
+ throw new GithubError('GITHUB_API_FAILED', {
67
+ status: notFound ? 404 : 400,
68
+ errors: body.errors.map(e => e.message),
69
+ });
70
+ }
71
+
72
+ return body.data;
73
+ };
74
+
75
+ const TAGS_QUERY = `
76
+ query ($owner: String!, $name: String!, $cursor: String) {
77
+ repository(owner: $owner, name: $name) {
78
+ refs(refPrefix: "refs/tags/", first: 100, after: $cursor) {
79
+ nodes { name }
80
+ pageInfo { hasNextPage endCursor }
81
+ }
82
+ }
83
+ }
84
+ `;
85
+
86
+ /* Все semver-теги репозитория (пагинация по 100) */
87
+ const listTags = async (slug: string): Promise<string[]> => {
88
+ const [owner, name] = slug.split('/');
89
+ const tags: string[] = [];
90
+ let cursor: string | null = null;
91
+
92
+ do {
93
+ const data = await gql(TAGS_QUERY, { owner, name, cursor });
94
+
95
+ if (!data.repository) {
96
+ throw new GithubError('GITHUB_API_FAILED', { status: 404, url: slug });
97
+ }
98
+
99
+ const { nodes, pageInfo } = data.repository.refs;
100
+
101
+ nodes.forEach((node: { name: string }) => {
102
+ if (versionUtil.validate(node.name)) tags.push(node.name);
103
+ });
104
+
105
+ cursor = pageInfo.hasNextPage ? pageInfo.endCursor : null;
106
+ } while (cursor);
107
+
108
+ return tags;
109
+ };
110
+
111
+ /*
112
+ * package.json каждого тега одним запросом: alias v<i> на
113
+ * object(expression: "<tag>:package.json"). Теги отфильтрованы по semver,
114
+ * так что инъекция в expression исключена. Нет файла — null.
115
+ */
116
+ const listPackages = async (
117
+ slug: string,
118
+ tags: string[],
119
+ ): Promise<Array<any | null>> => {
120
+ if (!tags.length) return [];
121
+
122
+ const [owner, name] = slug.split('/');
123
+
124
+ const fields = tags.map((tag, i) => (
125
+ // eslint-disable-next-line max-len
126
+ `v${i}: object(expression: ${JSON.stringify(`${tag}:package.json`)}) { ... on Blob { text } }`
127
+ )).join('\n');
128
+
129
+ const query = `
130
+ query ($owner: String!, $name: String!) {
131
+ repository(owner: $owner, name: $name) {
132
+ ${fields}
133
+ }
134
+ }
135
+ `;
136
+
137
+ const data = await gql(query, { owner, name });
138
+
139
+ return tags.map((tag, i) => {
140
+ const text = data.repository?.[`v${i}`]?.text;
141
+
142
+ if (!text) return null;
143
+
144
+ try {
145
+ return JSON.parse(text);
146
+ } catch {
147
+ return null;
148
+ }
149
+ });
150
+ };
151
+
152
+ export interface IGithubVersion {
153
+ version: string;
154
+ /* package.json на теге; нет файла или битый JSON — null */
155
+ pkg: any | null;
156
+ }
157
+
158
+ export const api = {
159
+ /* Токен из JS API; без него (и при undefined) работает env-переменная */
160
+ setToken: (token?: string): void => {
161
+ apiToken = token;
162
+ },
163
+
164
+ /*
165
+ * Теги репозитория + package.json каждого тега: 1 запрос на страницу
166
+ * тегов + 1 батч-запрос на deps всех тегов.
167
+ */
168
+ fetchVersions: async (url: string): Promise<IGithubVersion[]> => {
169
+ const slug = parseSlug(url);
170
+
171
+ if (!slug) {
172
+ throw new GithubError('GITHUB_NOT_A_REPO_URL', { url });
173
+ }
174
+
175
+ const tags = await listTags(slug);
176
+ const pkgs = await listPackages(slug, tags);
177
+
178
+ return tags.map((version, i) => ({ version, pkg: pkgs[i] }));
179
+ },
180
+ };
@@ -0,0 +1,3 @@
1
+ export { api } from './api';
2
+ export type { IGithubVersion } from './api';
3
+ export { GithubError } from './GithubError';