@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.
- package/package.json +1 -1
- package/src/link-remote.ts +94 -56
package/package.json
CHANGED
package/src/link-remote.ts
CHANGED
|
@@ -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
|
|
43
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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 (
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 = (
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
244
|
+
const repo = path.basename(sib)
|
|
245
|
+
refs.set(sib, await cloneSibling(repoUrl(repo), repo, sib, path.relative(cwd, sib)))
|
|
215
246
|
})
|
|
216
|
-
return
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
|
266
|
-
*
|
|
267
|
-
*
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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`
|
|
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
|
-
|
|
335
|
+
await installSiblings(
|
|
298
336
|
cwd,
|
|
299
337
|
siblings.map((s) => s.abs),
|
|
300
338
|
)
|