@10stars/config 16.2.0 → 17.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@10stars/config",
3
- "version": "16.2.0",
3
+ "version": "17.0.0",
4
4
  "private": false,
5
5
  "bin": {
6
6
  "config": "./src/index.ts"
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import path from "node:path"
5
5
 
6
6
  import { resetColorIndex } from "./colors"
7
7
  import { checkIsMonorepo, dedupeSingletons, linkPackages } from "./link-packages"
8
+ import { dedupeRemote, linkRemote } from "./link-remote"
8
9
  import { setupOverrides } from "./manage-overrides"
9
10
  import { runConcurrently, type CommandConfig } from "./runner"
10
11
  import { setupSideEffects } from "./vanilla-extract/manage-side-effects"
@@ -61,15 +62,25 @@ const actions = {
61
62
  const config = await getConfig()
62
63
  if (config?.linkPackages) {
63
64
  const linkedPaths = await linkPackages(cwd, config.linkPackages)
64
- if (config.dedupeSingletons !== false) {
65
- const singletons = Array.isArray(config.dedupeSingletons)
66
- ? config.dedupeSingletons
67
- : undefined
68
- await dedupeSingletons(cwd, config.linkPackages, linkedPaths, singletons)
69
- }
65
+ await dedupeSingletons(cwd, config.linkPackages, linkedPaths, config.dedupeSingletons)
66
+ }
67
+ },
68
+ },
69
+ linkRemote: {
70
+ info: `CI: clone the transitive file: sibling graph before "bun install" (branch-first; --from <manifest> to pin/revert, --emit-manifest <path> to record)`,
71
+ action: () => {
72
+ const args = process.argv.slice(3)
73
+ const flag = (name: string) => {
74
+ const i = args.indexOf(name)
75
+ return i >= 0 ? args[i + 1] : undefined
70
76
  }
77
+ return linkRemote(cwd, { manifestOut: flag(`--emit-manifest`), fromManifest: flag(`--from`) })
71
78
  },
72
79
  },
80
+ dedupe: {
81
+ info: `CI: dedupe type singletons across linked siblings onto the consumer, after "bun install"`,
82
+ action: () => dedupeRemote(cwd),
83
+ },
73
84
  ts: {
74
85
  info: `Typecheck code (in watch mode)`,
75
86
  action: () => exec(`tsc --project ./tsconfig.json --noEmit --watch`),
@@ -32,9 +32,8 @@ export const linkPackages = async (cwd: string, packagesToLink: string[]) => {
32
32
  )
33
33
  if (!pkgJson) return
34
34
 
35
- const [orgName, pkgName] = pkgJson.name!.split(`/`)
36
-
37
- const pathSymlink = path.join(cwd, `node_modules`, orgName, pkgName)
35
+ // path.join normalizes the `/` in a scoped name (`@common/env`) and handles bare names (`frontend-toolkit`)
36
+ const pathSymlink = path.join(cwd, `node_modules`, pkgJson.name!)
38
37
  await symlink(pathPkg, pathSymlink)
39
38
  linkedPaths.push(pathPkg)
40
39
  }
@@ -82,13 +81,18 @@ export const dedupeSingletons = async (
82
81
  cwd: string,
83
82
  packagesToLink: string[],
84
83
  linkedPaths: string[],
85
- singletons: string[] = DEFAULT_SINGLETONS,
84
+ singletons: boolean | string[] = true,
85
+ canonicalRoot?: string,
86
86
  ) => {
87
- // siblings share cwd's parent; the formula reduces to path.dirname(cwd) for `../pkg` entries
87
+ if (singletons === false) return
88
+ const packages = Array.isArray(singletons) ? singletons : DEFAULT_SINGLETONS
89
+
90
+ // siblings share cwd's parent; the formula reduces to path.dirname(cwd) for `../pkg` entries.
91
+ // canonicalRoot overrides this for standalone CI checkouts, where the consumer (cwd) holds the canonical copy.
88
92
  const roots = new Set(packagesToLink.map((rel) => path.resolve(cwd, rel, `..`)))
89
- const workspaceRoot = roots.size === 1 ? [...roots][0]! : path.dirname(cwd)
93
+ const workspaceRoot = canonicalRoot ?? (roots.size === 1 ? [...roots][0]! : path.dirname(cwd))
90
94
 
91
- for (const pkg of singletons) {
95
+ for (const pkg of packages) {
92
96
  const canonical = path.join(workspaceRoot, `node_modules`, pkg)
93
97
  const canonicalVersion = await readPkgVersion(canonical)
94
98
  if (!canonicalVersion) {
@@ -0,0 +1,288 @@
1
+ import { execFileSync } from "node:child_process"
2
+ import fs from "node:fs/promises"
3
+ import path from "node:path"
4
+
5
+ import type * as TF from "type-fest"
6
+
7
+ import { dedupeSingletons } from "./link-packages"
8
+
9
+ interface Config {
10
+ dedupeSingletons?: boolean | string[]
11
+ }
12
+
13
+ interface ManifestSibling {
14
+ repo: string
15
+ path: string // relative to the consumer, e.g. "../common"
16
+ ref: string | null
17
+ sha: string | null
18
+ version: string | null // nearest git tag, if any
19
+ }
20
+
21
+ interface Manifest {
22
+ schema: 1
23
+ consumer: { repo: string; ref: string | null; sha: string | null }
24
+ createdAt: string
25
+ siblings: ManifestSibling[]
26
+ }
27
+
28
+ interface LinkRemoteOptions {
29
+ /** Write the resolved transitive closure ({repo, ref, sha, version}) to this path. */
30
+ manifestOut?: string
31
+ /** Pin-clone each sibling at the SHA recorded in this manifest (revert), instead of branch-first. */
32
+ fromManifest?: string
33
+ }
34
+
35
+ /** A discovered sibling repo root and the ref it resolved to (branch name, or null if detached/pinned). */
36
+ type Sibling = { abs: string; ref: string | null }
37
+
38
+ // execFile (not a shell) so branch names / refs / SHAs can't inject shell syntax.
39
+ const git = (args: string[], cwd?: string): string =>
40
+ execFileSync(`git`, args, { cwd, encoding: `utf8` }).trim()
41
+
42
+ const gitInherit = (args: string[], cwd?: string): void => {
43
+ execFileSync(`git`, args, { cwd, stdio: `inherit` })
44
+ }
45
+
46
+ const tryGit = (args: string[], cwd?: string): string | null => {
47
+ try {
48
+ return git(args, cwd)
49
+ } catch {
50
+ return null
51
+ }
52
+ }
53
+
54
+ const bunInstall = (cwd: string): void =>
55
+ void execFileSync(`bun`, [`install`], { cwd, stdio: `inherit` })
56
+
57
+ const exists = (p: string): Promise<boolean> =>
58
+ fs
59
+ .stat(p)
60
+ .then(() => true)
61
+ .catch(() => false)
62
+
63
+ const readPkg = async (dir: string): Promise<TF.PackageJson | null> => {
64
+ try {
65
+ return JSON.parse(await fs.readFile(path.join(dir, `package.json`), `utf8`))
66
+ } catch {
67
+ return null
68
+ }
69
+ }
70
+
71
+ const getConfig = async (cwd: string): Promise<Config | null> =>
72
+ import(path.join(cwd, `10stars.config`)).catch(() => null)
73
+
74
+ const getOrg = (): string => {
75
+ const org = process.env.GITHUB_ORG ?? process.env.GITHUB_REPOSITORY?.split(`/`)[0]
76
+ if (!org) {
77
+ throw new Error(
78
+ `Cannot determine org: set GITHUB_ORG, or run inside GitHub Actions (GITHUB_REPOSITORY)`,
79
+ )
80
+ }
81
+ return org
82
+ }
83
+
84
+ const repoUrl = (repo: string) => `https://github.com/${getOrg()}/${repo}.git`
85
+
86
+ /**
87
+ * Absolute repo roots referenced by one dir's `file:` deps.
88
+ * `file:../common/env` → `<dir>/../common`; whole-repo `file:../frontend-toolkit` → itself.
89
+ * Self-refs (`file:.`) and non-parent paths are skipped, so registry deps (incl.
90
+ * `@anime.club/translations`) are never included.
91
+ */
92
+ const fileSiblingRoots = async (dir: string): Promise<string[]> => {
93
+ const pkg = await readPkg(dir)
94
+ if (!pkg) return []
95
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
96
+ const roots = new Set<string>()
97
+ for (const spec of Object.values(allDeps)) {
98
+ if (typeof spec !== `string` || !spec.startsWith(`file:`)) continue
99
+ const rel = spec.slice(`file:`.length)
100
+ if (!rel.startsWith(`..`)) continue
101
+ const segments = rel.split(`/`)
102
+ const firstNamed = segments.findIndex((s) => s !== `..`)
103
+ if (firstNamed < 0) continue
104
+ roots.add(path.resolve(dir, segments.slice(0, firstNamed + 1).join(`/`)))
105
+ }
106
+ return [...roots]
107
+ }
108
+
109
+ /**
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.
113
+ */
114
+ const bfsSiblings = async (cwd: string, visit: (sib: string) => Promise<void>): Promise<void> => {
115
+ 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)
124
+ }
125
+ }
126
+ }
127
+
128
+ const branchExists = (url: string, ref: string): boolean =>
129
+ Boolean(tryGit([`ls-remote`, `--heads`, url, ref]))
130
+
131
+ /** Branch-first: current PR branch, then push branch, then `develop`, then `production`. */
132
+ const resolveRef = (url: string, repo: string): string => {
133
+ const candidates = [
134
+ process.env.GITHUB_HEAD_REF, // pull_request events
135
+ process.env.GITHUB_REF_NAME, // push events
136
+ `develop`,
137
+ `production`,
138
+ ].filter((v): v is string => Boolean(v))
139
+
140
+ for (const ref of candidates) if (branchExists(url, ref)) return ref
141
+ throw new Error(`No branch found for ${repo}; tried: ${candidates.join(`, `)}`)
142
+ }
143
+
144
+ const headSha = (dir: string): string | null => tryGit([`rev-parse`, `HEAD`], dir)
145
+
146
+ const nearestTag = (dir: string): string | null => tryGit([`describe`, `--tags`, `--abbrev=0`], dir)
147
+
148
+ const currentRef = (dir: string): string | null => {
149
+ const branch = tryGit([`rev-parse`, `--abbrev-ref`, `HEAD`], dir)
150
+ return branch === `HEAD` ? null : branch
151
+ }
152
+
153
+ const clonePinned = (url: string, sha: string, dir: string): void => {
154
+ gitInherit([`init`, dir])
155
+ gitInherit([`remote`, `add`, `origin`, url], dir)
156
+ gitInherit([`fetch`, `--depth`, `1`, `origin`, sha], dir) // GitHub allows fetching a reachable SHA
157
+ gitInherit([`checkout`, `FETCH_HEAD`], dir)
158
+ }
159
+
160
+ /**
161
+ * Walk the transitive `file:` graph from `cwd`, cloning each missing sibling as a flat peer.
162
+ * Returns siblings in discovery (BFS) order — dependents before their dependencies.
163
+ */
164
+ const cloneClosureBranchFirst = async (cwd: string): Promise<Sibling[]> => {
165
+ const order: Sibling[] = []
166
+ await bfsSiblings(cwd, async (sib) => {
167
+ let ref: string | null
168
+ if (await exists(sib)) {
169
+ console.info(`linkRemote: ${path.relative(cwd, sib)} already present; not cloning`)
170
+ ref = currentRef(sib)
171
+ } else {
172
+ const repo = path.basename(sib)
173
+ const url = repoUrl(repo)
174
+ ref = resolveRef(url, repo)
175
+ console.info(`linkRemote: cloning ${repo}@${ref} -> ${path.relative(cwd, sib)}`)
176
+ gitInherit([`clone`, `--depth`, `1`, `--single-branch`, `--branch`, ref, url, sib])
177
+ }
178
+ order.push({ abs: sib, ref })
179
+ })
180
+ return order
181
+ }
182
+
183
+ const writeManifest = async (cwd: string, siblings: Sibling[], out: string): Promise<void> => {
184
+ const manifest: Manifest = {
185
+ schema: 1,
186
+ consumer: {
187
+ repo: path.basename(path.resolve(cwd)),
188
+ ref: process.env.GITHUB_HEAD_REF ?? process.env.GITHUB_REF_NAME ?? currentRef(cwd),
189
+ sha: headSha(cwd),
190
+ },
191
+ createdAt: new Date().toISOString(),
192
+ siblings: siblings.map(({ abs, ref }) => ({
193
+ repo: path.basename(abs),
194
+ path: path.relative(cwd, abs),
195
+ ref,
196
+ sha: headSha(abs),
197
+ version: nearestTag(abs),
198
+ })),
199
+ }
200
+ await fs.writeFile(out, JSON.stringify(manifest, null, 2) + `\n`)
201
+ console.info(`linkRemote: wrote manifest -> ${out}`)
202
+ }
203
+
204
+ const linkRemoteFromManifest = async (cwd: string, manifestPath: string): Promise<void> => {
205
+ const manifest: Manifest = JSON.parse(await fs.readFile(manifestPath, `utf8`))
206
+ if (!manifest.siblings?.length) {
207
+ console.info(`linkRemote --from: manifest has no siblings; nothing to do`)
208
+ return
209
+ }
210
+
211
+ for (const sib of manifest.siblings) {
212
+ const abs = path.resolve(cwd, sib.path)
213
+ if (await exists(abs)) {
214
+ console.info(`linkRemote --from: ${sib.path} already present; leaving as-is`)
215
+ continue
216
+ }
217
+ if (!sib.sha) throw new Error(`Manifest entry "${sib.repo}" has no sha to pin`)
218
+ console.info(`linkRemote --from: pinning ${sib.repo}@${sib.sha} -> ${sib.path}`)
219
+ clonePinned(repoUrl(sib.repo), sib.sha, abs)
220
+ }
221
+
222
+ installLeavesFirst(
223
+ cwd,
224
+ manifest.siblings.map((s) => path.resolve(cwd, s.path)),
225
+ )
226
+ }
227
+
228
+ /**
229
+ * Install siblings leaves-first (reverse of dependents-first discovery order) so a hoisted
230
+ * sibling (e.g. `common`) is installed before a repo that links into it.
231
+ * Assumes an acyclic, mostly-linear `file:` graph; a diamond could need a real topological sort.
232
+ */
233
+ const installLeavesFirst = (cwd: string, siblingsInDiscoveryOrder: string[]): void => {
234
+ for (const abs of [...siblingsInDiscoveryOrder].reverse()) {
235
+ console.info(`linkRemote: bun install in ${path.relative(cwd, abs)}`)
236
+ bunInstall(abs)
237
+ }
238
+ }
239
+
240
+ /**
241
+ * CI: materialize the transitive `file:` sibling graph that a standalone checkout lacks.
242
+ *
243
+ * Default (branch-first): BFS the graph, shallow single-branch clone each missing sibling at
244
+ * its resolved ref, then `bun install` leaves-first. `--emit-manifest` records the exact
245
+ * {repo, ref, sha, version} set (a cross-repo lockfile snapshot for revert). `--from` skips
246
+ * branch-first and pin-clones each sibling at the manifest's recorded SHA.
247
+ *
248
+ * Linking is done by the consumer's own `bun install` resolving the `file:` specs afterwards;
249
+ * run `dedupe` after that. No-op for repos with no `file:` siblings, and skips siblings already
250
+ * present (local dev).
251
+ */
252
+ export const linkRemote = async (cwd: string, opts: LinkRemoteOptions = {}): Promise<void> => {
253
+ if (opts.fromManifest) return linkRemoteFromManifest(cwd, opts.fromManifest)
254
+
255
+ const siblings = await cloneClosureBranchFirst(cwd)
256
+ if (!siblings.length) {
257
+ console.info(`linkRemote: no file: siblings; nothing to do`)
258
+ return
259
+ }
260
+
261
+ installLeavesFirst(
262
+ cwd,
263
+ siblings.map((s) => s.abs),
264
+ )
265
+
266
+ if (opts.manifestOut) await writeManifest(cwd, siblings, opts.manifestOut)
267
+ }
268
+
269
+ /** Present transitive `file:` siblings (no cloning) — used for post-install dedupe. */
270
+ const collectPresentSiblings = async (cwd: string): Promise<string[]> => {
271
+ const found: string[] = []
272
+ await bfsSiblings(cwd, async (sib) => {
273
+ if (await exists(sib)) found.push(sib)
274
+ })
275
+ return found
276
+ }
277
+
278
+ /**
279
+ * CI, run AFTER the consumer's `bun install`: collapse type singletons (`@types/react`, …)
280
+ * across the transitive linked graph onto the consumer's own copy. A standalone CI checkout
281
+ * has no hoisted workspace root, so the canonical is the consumer (`cwd`).
282
+ */
283
+ export const dedupeRemote = async (cwd: string): Promise<void> => {
284
+ const config = await getConfig(cwd)
285
+ const siblings = await collectPresentSiblings(cwd)
286
+ if (!siblings.length) return
287
+ await dedupeSingletons(cwd, [], siblings, config?.dedupeSingletons, cwd)
288
+ }