@10stars/config 17.0.3 → 17.0.4

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/link-remote.ts +68 -42
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@10stars/config",
3
- "version": "17.0.3",
3
+ "version": "17.0.4",
4
4
  "private": false,
5
5
  "bin": {
6
6
  "config": "./src/index.ts"
@@ -1,6 +1,7 @@
1
- import { execFileSync } from "node:child_process"
1
+ import { execFile, execFileSync } from "node:child_process"
2
2
  import fs from "node:fs/promises"
3
3
  import path from "node:path"
4
+ import { promisify } from "node:util"
4
5
 
5
6
  import type * as TF from "type-fest"
6
7
 
@@ -39,9 +40,14 @@ type Sibling = { abs: string; ref: string | null }
39
40
  const git = (args: string[], cwd?: string): string =>
40
41
  execFileSync(`git`, args, { cwd, encoding: `utf8` }).trim()
41
42
 
42
- const gitInherit = (args: string[], cwd?: string): void => {
43
- execFileSync(`git`, args, { cwd, stdio: `inherit` })
44
- }
43
+ const execFileAsync = promisify(execFile)
44
+
45
+ // Async git so independent clones (a BFS level's siblings, or all pinned --from clones) run
46
+ // concurrently: the sync `git` helper's execFileSync would serialize them by blocking the event
47
+ // loop until each finishes. Output is captured (not inherited) so concurrent progress can't
48
+ // interleave into unreadable logs; callers log one line per repo.
49
+ const gitAsync = (args: string[], cwd?: string): Promise<unknown> =>
50
+ execFileAsync(`git`, args, { cwd })
45
51
 
46
52
  const tryGit = (args: string[], cwd?: string): string | null => {
47
53
  try {
@@ -107,22 +113,34 @@ const fileSiblingRoots = async (dir: string): Promise<string[]> => {
107
113
  }
108
114
 
109
115
  /**
110
- * BFS the transitive `file:` sibling graph from `cwd`, invoking `visit` once per newly discovered
111
- * sibling root in dependents-first order. Cloning inside `visit` makes that sibling's own `file:`
112
- * deps discoverable when it is later dequeued; a still-missing dir simply yields no children.
116
+ * BFS the transitive `file:` sibling graph from `cwd` in dependents-first order, invoking `visit`
117
+ * on each level's newly discovered siblings concurrently (`Promise.all`). Cloning a sibling in
118
+ * `visit` makes its own `file:` deps discoverable when the next level expands from it. Levels stay
119
+ * sequential (a child is only found after its parent is cloned) but siblings within a level are
120
+ * independent and clone in parallel. The `visited` set dedupes repos reached by more than one path,
121
+ * both across and within a level. Returns the deduped siblings in discovery (dependents-first) order.
113
122
  */
114
- const bfsSiblings = async (cwd: string, visit: (sib: string) => Promise<void>): Promise<void> => {
123
+ const bfsSiblings = async (
124
+ cwd: string,
125
+ visit: (sib: string) => Promise<void>,
126
+ ): Promise<string[]> => {
115
127
  const visited = new Set<string>([path.resolve(cwd)])
116
- const queue = [path.resolve(cwd)]
117
- while (queue.length) {
118
- const dir = queue.shift()!
119
- for (const sib of await fileSiblingRoots(dir)) {
120
- if (visited.has(sib)) continue
121
- visited.add(sib)
122
- await visit(sib)
123
- queue.push(sib)
128
+ const order: string[] = []
129
+ let frontier = [path.resolve(cwd)]
130
+ while (frontier.length) {
131
+ const level: string[] = []
132
+ for (const dir of frontier) {
133
+ for (const sib of await fileSiblingRoots(dir)) {
134
+ if (visited.has(sib)) continue // dedupe repos reached via multiple file: paths
135
+ visited.add(sib)
136
+ level.push(sib)
137
+ }
124
138
  }
139
+ await Promise.all(level.map(visit)) // clone this level's siblings in parallel
140
+ order.push(...level)
141
+ frontier = level
125
142
  }
143
+ return order
126
144
  }
127
145
 
128
146
  /**
@@ -160,7 +178,12 @@ const matchRemoteBranch = (url: string, candidates: string[]): string | null =>
160
178
  * `--branch`, so the `ls-remote` probe is skipped entirely when no coordinated branch is set — one
161
179
  * fewer round-trip for local/manual runs (in GitHub Actions GITHUB_REF_NAME is always set).
162
180
  */
163
- const cloneSibling = (url: string, repo: string, sib: string, dest: string): string | null => {
181
+ const cloneSibling = async (
182
+ url: string,
183
+ repo: string,
184
+ sib: string,
185
+ dest: string,
186
+ ): Promise<string | null> => {
164
187
  const coordinated = [
165
188
  process.env.GITHUB_HEAD_REF, // pull_request events
166
189
  process.env.GITHUB_REF_NAME, // push events
@@ -168,7 +191,7 @@ const cloneSibling = (url: string, repo: string, sib: string, dest: string): str
168
191
  const ref = coordinated.length ? matchRemoteBranch(url, coordinated) : null
169
192
 
170
193
  console.info(`linkRemote: cloning ${repo}@${ref ?? `(default)`} -> ${dest}`)
171
- gitInherit([
194
+ await gitAsync([
172
195
  `clone`,
173
196
  `--depth`,
174
197
  `1`,
@@ -189,11 +212,11 @@ const currentRef = (dir: string): string | null => {
189
212
  return branch === `HEAD` ? null : branch
190
213
  }
191
214
 
192
- const clonePinned = (url: string, sha: string, dir: string): void => {
193
- gitInherit([`init`, dir])
194
- gitInherit([`remote`, `add`, `origin`, url], dir)
195
- gitInherit([`fetch`, `--depth`, `1`, `origin`, sha], dir) // GitHub allows fetching a reachable SHA
196
- gitInherit([`checkout`, `FETCH_HEAD`], dir)
215
+ const clonePinned = async (url: string, sha: string, dir: string): Promise<void> => {
216
+ await gitAsync([`init`, dir])
217
+ await gitAsync([`remote`, `add`, `origin`, url], dir)
218
+ await gitAsync([`fetch`, `--depth`, `1`, `origin`, sha], dir) // GitHub allows fetching a reachable SHA
219
+ await gitAsync([`checkout`, `FETCH_HEAD`], dir)
197
220
  }
198
221
 
199
222
  /**
@@ -201,19 +224,18 @@ const clonePinned = (url: string, sha: string, dir: string): void => {
201
224
  * Returns siblings in discovery (BFS) order — dependents before their dependencies.
202
225
  */
203
226
  const cloneClosureBranchFirst = async (cwd: string): Promise<Sibling[]> => {
204
- const order: Sibling[] = []
205
- await bfsSiblings(cwd, async (sib) => {
206
- let ref: string | null
227
+ // Parallel visits resolve out of order; key refs by abs path and reassemble in discovery order.
228
+ const refs = new Map<string, string | null>()
229
+ const discovered = await bfsSiblings(cwd, async (sib) => {
207
230
  if (await exists(sib)) {
208
231
  console.info(`linkRemote: ${path.relative(cwd, sib)} already present; not cloning`)
209
- ref = currentRef(sib)
210
- } else {
211
- const repo = path.basename(sib)
212
- ref = cloneSibling(repoUrl(repo), repo, sib, path.relative(cwd, sib))
232
+ refs.set(sib, currentRef(sib))
233
+ return
213
234
  }
214
- order.push({ abs: sib, ref })
235
+ const repo = path.basename(sib)
236
+ refs.set(sib, await cloneSibling(repoUrl(repo), repo, sib, path.relative(cwd, sib)))
215
237
  })
216
- return order
238
+ return discovered.map((abs) => ({ abs, ref: refs.get(abs) ?? null }))
217
239
  }
218
240
 
219
241
  const writeManifest = async (cwd: string, siblings: Sibling[], out: string): Promise<void> => {
@@ -244,16 +266,20 @@ const linkRemoteFromManifest = async (cwd: string, manifestPath: string): Promis
244
266
  return
245
267
  }
246
268
 
247
- for (const sib of manifest.siblings) {
248
- const abs = path.resolve(cwd, sib.path)
249
- if (await exists(abs)) {
250
- console.info(`linkRemote --from: ${sib.path} already present; leaving as-is`)
251
- continue
252
- }
253
- if (!sib.sha) throw new Error(`Manifest entry "${sib.repo}" has no sha to pin`)
254
- console.info(`linkRemote --from: pinning ${sib.repo}@${sib.sha} -> ${sib.path}`)
255
- clonePinned(repoUrl(sib.repo), sib.sha, abs)
256
- }
269
+ // Pinned clones are mutually independent (each at a recorded SHA, no discovery), so materialize
270
+ // them all in parallel; leaves-first install order is still enforced below.
271
+ await Promise.all(
272
+ manifest.siblings.map(async (sib) => {
273
+ const abs = path.resolve(cwd, sib.path)
274
+ if (await exists(abs)) {
275
+ console.info(`linkRemote --from: ${sib.path} already present; leaving as-is`)
276
+ return
277
+ }
278
+ if (!sib.sha) throw new Error(`Manifest entry "${sib.repo}" has no sha to pin`)
279
+ console.info(`linkRemote --from: pinning ${sib.repo}@${sib.sha} -> ${sib.path}`)
280
+ await clonePinned(repoUrl(sib.repo), sib.sha, abs)
281
+ }),
282
+ )
257
283
 
258
284
  installLeavesFirst(
259
285
  cwd,