@10stars/config 17.0.3 → 17.0.5

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 +94 -56
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@10stars/config",
3
- "version": "17.0.3",
3
+ "version": "17.0.5",
4
4
  "private": false,
5
5
  "bin": {
6
6
  "config": "./src/index.ts"
@@ -1,10 +1,11 @@
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
 
7
- import { dedupeSingletons } from "./link-packages"
8
+ import { dedupeSingletons, linkSelf } from "./link-packages"
8
9
 
9
10
  interface Config {
10
11
  dedupeSingletons?: boolean | string[]
@@ -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 {
@@ -51,8 +57,16 @@ const tryGit = (args: string[], cwd?: string): string | null => {
51
57
  }
52
58
  }
53
59
 
54
- const bunInstall = (cwd: string): void =>
55
- void execFileSync(`bun`, [`install`], { cwd, stdio: `inherit` })
60
+ // --ignore-scripts: a sibling is only a dependency here, so skip its postinstall (`config prepare`)
61
+ // — its Vanilla-Extract patch isn't needed (nothing builds it; typecheck doesn't run VE) and the
62
+ // cross-repo `@types/*` dedupe runs once via the consumer's `dedupe` afterwards. We still restore
63
+ // the one thing a sibling's own src needs: its self-symlink, so `<self>/src/…` self-imports resolve
64
+ // at realpath in the flat-peer layout. (Workspace-root siblings, e.g. backend-api, get their
65
+ // subpackage self-links from bun's own workspace linking, so linkSelf no-ops on them.)
66
+ const bunInstall = async (cwd: string): Promise<void> => {
67
+ await execFileAsync(`bun`, [`install`, `--ignore-scripts`], { cwd })
68
+ await linkSelf(cwd)
69
+ }
56
70
 
57
71
  const exists = (p: string): Promise<boolean> =>
58
72
  fs
@@ -107,22 +121,35 @@ const fileSiblingRoots = async (dir: string): Promise<string[]> => {
107
121
  }
108
122
 
109
123
  /**
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.
124
+ * BFS the transitive `file:` sibling graph from `cwd` in dependents-first order, invoking `visit`
125
+ * on each level's newly discovered siblings concurrently (`Promise.all`). Cloning a sibling in
126
+ * `visit` makes its own `file:` deps discoverable when the next level expands from it. Levels stay
127
+ * sequential (a child is only found after its parent is cloned) but siblings within a level are
128
+ * independent and clone in parallel. The `visited` set dedupes repos reached by more than one path,
129
+ * both across and within a level. Returns the deduped siblings in discovery (dependents-first) order.
113
130
  */
114
- const bfsSiblings = async (cwd: string, visit: (sib: string) => Promise<void>): Promise<void> => {
131
+ const bfsSiblings = async (
132
+ cwd: string,
133
+ visit: (sib: string) => Promise<void>,
134
+ ): Promise<string[]> => {
115
135
  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)
136
+ const order: string[] = []
137
+ let frontier = [path.resolve(cwd)]
138
+ while (frontier.length) {
139
+ const level: string[] = []
140
+ for (const dir of frontier) {
141
+ for (const sib of await fileSiblingRoots(dir)) {
142
+ if (visited.has(sib)) continue // dedupe repos reached via multiple file: paths
143
+ visited.add(sib)
144
+ level.push(sib)
145
+ }
124
146
  }
147
+ // clone this level's siblings in parallel; unbounded fan-out, fine for the handful in practice
148
+ await Promise.all(level.map(visit))
149
+ order.push(...level)
150
+ frontier = level
125
151
  }
152
+ return order
126
153
  }
127
154
 
128
155
  /**
@@ -160,7 +187,12 @@ const matchRemoteBranch = (url: string, candidates: string[]): string | null =>
160
187
  * `--branch`, so the `ls-remote` probe is skipped entirely when no coordinated branch is set — one
161
188
  * fewer round-trip for local/manual runs (in GitHub Actions GITHUB_REF_NAME is always set).
162
189
  */
163
- const cloneSibling = (url: string, repo: string, sib: string, dest: string): string | null => {
190
+ const cloneSibling = async (
191
+ url: string,
192
+ repo: string,
193
+ sib: string,
194
+ dest: string,
195
+ ): Promise<string | null> => {
164
196
  const coordinated = [
165
197
  process.env.GITHUB_HEAD_REF, // pull_request events
166
198
  process.env.GITHUB_REF_NAME, // push events
@@ -168,7 +200,7 @@ const cloneSibling = (url: string, repo: string, sib: string, dest: string): str
168
200
  const ref = coordinated.length ? matchRemoteBranch(url, coordinated) : null
169
201
 
170
202
  console.info(`linkRemote: cloning ${repo}@${ref ?? `(default)`} -> ${dest}`)
171
- gitInherit([
203
+ await gitAsync([
172
204
  `clone`,
173
205
  `--depth`,
174
206
  `1`,
@@ -189,11 +221,11 @@ const currentRef = (dir: string): string | null => {
189
221
  return branch === `HEAD` ? null : branch
190
222
  }
191
223
 
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)
224
+ const clonePinned = async (url: string, sha: string, dir: string): Promise<void> => {
225
+ await gitAsync([`init`, dir])
226
+ await gitAsync([`remote`, `add`, `origin`, url], dir)
227
+ await gitAsync([`fetch`, `--depth`, `1`, `origin`, sha], dir) // GitHub allows fetching a reachable SHA
228
+ await gitAsync([`checkout`, `FETCH_HEAD`], dir)
197
229
  }
198
230
 
199
231
  /**
@@ -201,19 +233,18 @@ const clonePinned = (url: string, sha: string, dir: string): void => {
201
233
  * Returns siblings in discovery (BFS) order — dependents before their dependencies.
202
234
  */
203
235
  const cloneClosureBranchFirst = async (cwd: string): Promise<Sibling[]> => {
204
- const order: Sibling[] = []
205
- await bfsSiblings(cwd, async (sib) => {
206
- let ref: string | null
236
+ // Parallel visits resolve out of order; key refs by abs path and reassemble in discovery order.
237
+ const refs = new Map<string, string | null>()
238
+ const discovered = await bfsSiblings(cwd, async (sib) => {
207
239
  if (await exists(sib)) {
208
240
  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))
241
+ refs.set(sib, currentRef(sib))
242
+ return
213
243
  }
214
- order.push({ abs: sib, ref })
244
+ const repo = path.basename(sib)
245
+ refs.set(sib, await cloneSibling(repoUrl(repo), repo, sib, path.relative(cwd, sib)))
215
246
  })
216
- return order
247
+ return discovered.map((abs) => ({ abs, ref: refs.get(abs) ?? null }))
217
248
  }
218
249
 
219
250
  const writeManifest = async (cwd: string, siblings: Sibling[], out: string): Promise<void> => {
@@ -244,40 +275,47 @@ const linkRemoteFromManifest = async (cwd: string, manifestPath: string): Promis
244
275
  return
245
276
  }
246
277
 
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
- }
278
+ // Pinned clones are mutually independent (each at a recorded SHA, no discovery), so materialize
279
+ // them all in parallel; the installs below are likewise parallel and order-independent.
280
+ await Promise.all(
281
+ manifest.siblings.map(async (sib) => {
282
+ const abs = path.resolve(cwd, sib.path)
283
+ if (await exists(abs)) {
284
+ console.info(`linkRemote --from: ${sib.path} already present; leaving as-is`)
285
+ return
286
+ }
287
+ if (!sib.sha) throw new Error(`Manifest entry "${sib.repo}" has no sha to pin`)
288
+ console.info(`linkRemote --from: pinning ${sib.repo}@${sib.sha} -> ${sib.path}`)
289
+ await clonePinned(repoUrl(sib.repo), sib.sha, abs)
290
+ }),
291
+ )
257
292
 
258
- installLeavesFirst(
293
+ await installSiblings(
259
294
  cwd,
260
295
  manifest.siblings.map((s) => path.resolve(cwd, s.path)),
261
296
  )
262
297
  }
263
298
 
264
299
  /**
265
- * Install siblings leaves-first (reverse of dependents-first discovery order) so a hoisted
266
- * sibling (e.g. `common`) is installed before a repo that links into it.
267
- * Assumes an acyclic, mostly-linear `file:` graph; a diamond could need a real topological sort.
300
+ * Install every cloned sibling, in parallel. `--ignore-scripts` (in bunInstall) removes the
301
+ * postinstall that used to make this order-sensitive, so ordering no longer matters: each install
302
+ * is independent it resolves its own `file:` deps from their on-disk package.json and hoists into
303
+ * its OWN node_modules, writing nobody else's tree. (Bun's global cache is concurrency-safe.)
268
304
  */
269
- const installLeavesFirst = (cwd: string, siblingsInDiscoveryOrder: string[]): void => {
270
- for (const abs of [...siblingsInDiscoveryOrder].reverse()) {
271
- console.info(`linkRemote: bun install in ${path.relative(cwd, abs)}`)
272
- bunInstall(abs)
273
- }
305
+ const installSiblings = async (cwd: string, siblings: string[]): Promise<void> => {
306
+ await Promise.all(
307
+ siblings.map(async (abs) => {
308
+ console.info(`linkRemote: bun install in ${path.relative(cwd, abs)}`)
309
+ await bunInstall(abs)
310
+ }),
311
+ )
274
312
  }
275
313
 
276
314
  /**
277
315
  * CI: materialize the transitive `file:` sibling graph that a standalone checkout lacks.
278
316
  *
279
317
  * Default (branch-first): BFS the graph, shallow single-branch clone each missing sibling at
280
- * its resolved ref, then `bun install` leaves-first. `--emit-manifest` records the exact
318
+ * its resolved ref, then `bun install --ignore-scripts` each in parallel. `--emit-manifest` records the exact
281
319
  * {repo, ref, sha, version} set (a cross-repo lockfile snapshot for revert). `--from` skips
282
320
  * branch-first and pin-clones each sibling at the manifest's recorded SHA.
283
321
  *
@@ -294,7 +332,7 @@ export const linkRemote = async (cwd: string, opts: LinkRemoteOptions = {}): Pro
294
332
  return
295
333
  }
296
334
 
297
- installLeavesFirst(
335
+ await installSiblings(
298
336
  cwd,
299
337
  siblings.map((s) => s.abs),
300
338
  )