@10stars/config 17.0.1 → 17.0.3

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": "17.0.1",
3
+ "version": "17.0.3",
4
4
  "private": false,
5
5
  "bin": {
6
6
  "config": "./src/index.ts"
@@ -30,7 +30,7 @@
30
30
  "oxfmt": "0.58.0",
31
31
  "oxlint": "^1.73.0",
32
32
  "oxlint-tsgolint": "^0.24.0",
33
- "tsx": "^4.23.0",
33
+ "tsx": "^4.23.1",
34
34
  "type-fest": "^5.8.0",
35
35
  "typescript": "7.0.2"
36
36
  }
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import fs from "node:fs/promises"
4
4
  import path from "node:path"
5
5
 
6
6
  import { resetColorIndex } from "./colors"
7
- import { checkIsMonorepo, dedupeSingletons, linkPackages } from "./link-packages"
7
+ import { checkIsMonorepo, dedupeSingletons, linkPackages, linkSelf } from "./link-packages"
8
8
  import { dedupeRemote, linkRemote } from "./link-remote"
9
9
  import { setupOverrides } from "./manage-overrides"
10
10
  import { runConcurrently, type CommandConfig } from "./runner"
@@ -47,6 +47,8 @@ const actions = {
47
47
  action: async () => {
48
48
  const veInstalled = await patchVanillaExtract(cwd) // patch VE before the CI guard: builds (incl. CI) need the `css.ts`-only filter in node_modules
49
49
 
50
+ await linkSelf(cwd) // before the CI guard: CI app builds also self-import `<pkg>/src/…`
51
+
50
52
  if (process.env.CI) return
51
53
 
52
54
  // we must copy it first, because the following steps might have formatting cmds
@@ -1,8 +1,20 @@
1
+ import { execFile } from "node:child_process"
1
2
  import fs from "node:fs/promises"
2
3
  import path from "node:path"
4
+ import { promisify } from "node:util"
3
5
 
4
6
  import type * as TF from "type-fest"
5
7
 
8
+ const execFileAsync = promisify(execFile)
9
+
10
+ // bun materializes a `file:.` self-dep sibling as a dir of self-referential symlinks (name -> name)
11
+ // over a nested node_modules; `fs.rm(recursive)` follows that into an ELOOP and hangs. Coreutils
12
+ // `rm -rf` uses cycle-detecting fts and clears it instantly. Windows keeps fs.rm (force skips ELOOP).
13
+ const removeDir = async (dir: string) => {
14
+ if (process.platform === `win32`) return fs.rm(dir, { recursive: true, force: true })
15
+ await execFileAsync(`rm`, [`-rf`, dir])
16
+ }
17
+
6
18
  const symlink = async (pathTarget: string, pathSymlink: string) => {
7
19
  const stats = await fs.lstat(pathSymlink).catch(() => null)
8
20
  if (!stats) return
@@ -12,12 +24,39 @@ const symlink = async (pathTarget: string, pathSymlink: string) => {
12
24
  if (pathTarget === symlinkRealpath) return
13
25
  }
14
26
 
15
- if (stats.isDirectory()) await fs.rm(pathSymlink, { recursive: true })
27
+ if (stats.isDirectory()) await removeDir(pathSymlink)
16
28
  else await fs.unlink(pathSymlink)
17
29
 
18
30
  await fs.symlink(pathTarget, pathSymlink)
19
31
  }
20
32
 
33
+ /**
34
+ * Create `node_modules/<self-name> -> <repo root>` so a single-package repo can import its own
35
+ * files by package name (`frontend-toolkit/src/…`) at runtime — replacing the old `file:.`
36
+ * self-dependency. A `file:.` self-dep makes bun's file: resolver recurse over the package's whole
37
+ * subtree on every consumer's install (multi-minute CPU spin, then a stack-overflow crash). This
38
+ * runs in postinstall (after `bun install`), so bun never resolves a self-dep, yet every tool
39
+ * (bun/node, Vite, Ladle, Vitest) still resolves self-imports via node_modules. tsconfig `paths`
40
+ * cover the same specifiers for typecheck. Unlike `symlink()` above this CREATES the link when
41
+ * absent (there's no self-dep for `bun install` to have materialized first). Skips workspace roots
42
+ * (their subpackages are separate) and unnamed packages.
43
+ */
44
+ export const linkSelf = async (cwd: string) => {
45
+ const pkgJson: TF.PackageJson = await import(path.join(cwd, `package.json`)).catch(() => null)
46
+ if (!pkgJson?.name || pkgJson.workspaces) return
47
+
48
+ const pathSymlink = path.join(cwd, `node_modules`, pkgJson.name)
49
+ await fs.mkdir(path.dirname(pathSymlink), { recursive: true })
50
+ const relTarget = path.relative(path.dirname(pathSymlink), cwd) // `..` (bare) / `../..` (scoped)
51
+
52
+ const stats = await fs.lstat(pathSymlink).catch(() => null)
53
+ if (stats?.isSymbolicLink() && (await fs.realpath(pathSymlink).catch(() => null)) === cwd) return
54
+ if (stats?.isDirectory()) await removeDir(pathSymlink)
55
+ else if (stats) await fs.unlink(pathSymlink)
56
+
57
+ await fs.symlink(relTarget, pathSymlink)
58
+ }
59
+
21
60
  export const checkIsMonorepo = async (pathPkg: string) => {
22
61
  const pkgJson = await import(path.join(pathPkg, `package.json`))
23
62
  return Boolean(pkgJson.workspaces)
@@ -126,23 +126,15 @@ const bfsSiblings = async (cwd: string, visit: (sib: string) => Promise<void>):
126
126
  }
127
127
 
128
128
  /**
129
- * Branch-first: current PR branch, then push branch, then `develop`, then `production`. A single
130
- * `ls-remote --heads` lists every head in one network round-trip; candidates are matched locally
131
- * (avoids one request per ref). A non-zero exit means the repo is unreadable (private + missing/
132
- * unauthorized clone token → "Repository not found") — surfaced immediately rather than masquerading
133
- * as "No branch found".
129
+ * The first of `candidates` (priority order) that exists as a branch on the remote, else null.
130
+ * Queries only those specific refs in one `ls-remote` no full head listing. A non-zero exit means
131
+ * the repo is unreadable (private + missing/unauthorized clone token → "Repository not found") —
132
+ * surfaced immediately rather than masquerading as a missing branch.
134
133
  */
135
- const resolveRef = (url: string, repo: string): string => {
136
- const candidates = [
137
- process.env.GITHUB_HEAD_REF, // pull_request events
138
- process.env.GITHUB_REF_NAME, // push events
139
- `develop`,
140
- `production`,
141
- ].filter((v): v is string => Boolean(v))
142
-
143
- let heads: string
134
+ const matchRemoteBranch = (url: string, candidates: string[]): string | null => {
135
+ let out: string
144
136
  try {
145
- heads = git([`ls-remote`, `--heads`, url])
137
+ out = git([`ls-remote`, url, ...candidates.map((c) => `refs/heads/${c}`)])
146
138
  } catch (err) {
147
139
  const stderr = (err as { stderr?: string | Buffer })?.stderr
148
140
  throw new Error(
@@ -151,18 +143,41 @@ const resolveRef = (url: string, repo: string): string => {
151
143
  `with contents:read.\n${(stderr ?? (err as Error)?.message ?? ``).toString().trim()}`,
152
144
  )
153
145
  }
154
-
155
- // Each line is "<sha>\trefs/heads/<branch>"; a branch may itself contain slashes (e.g. alice/x).
146
+ // Lines are "<sha>\trefs/heads/<branch>"; a branch may itself contain slashes (e.g. alice/x).
156
147
  const present = new Set(
157
- heads
148
+ out
158
149
  .split(`\n`)
159
150
  .map((line) => line.split(`refs/heads/`)[1])
160
151
  .filter(Boolean),
161
152
  )
162
- for (const ref of candidates) if (present.has(ref)) return ref
163
- throw new Error(
164
- `No branch found for ${repo}; tried: ${candidates.join(`, `)} (repo reachable, refs absent)`,
165
- )
153
+ return candidates.find((c) => present.has(c)) ?? null
154
+ }
155
+
156
+ /**
157
+ * Clone a missing sibling as a flat peer, returning the branch it landed on. Prefers a branch
158
+ * matching the consumer's coordinated branch (PR head, then push branch) when the sibling has one;
159
+ * otherwise clones the sibling's own default branch directly. A default-branch clone needs no
160
+ * `--branch`, so the `ls-remote` probe is skipped entirely when no coordinated branch is set — one
161
+ * fewer round-trip for local/manual runs (in GitHub Actions GITHUB_REF_NAME is always set).
162
+ */
163
+ const cloneSibling = (url: string, repo: string, sib: string, dest: string): string | null => {
164
+ const coordinated = [
165
+ process.env.GITHUB_HEAD_REF, // pull_request events
166
+ process.env.GITHUB_REF_NAME, // push events
167
+ ].filter((v): v is string => Boolean(v))
168
+ const ref = coordinated.length ? matchRemoteBranch(url, coordinated) : null
169
+
170
+ console.info(`linkRemote: cloning ${repo}@${ref ?? `(default)`} -> ${dest}`)
171
+ gitInherit([
172
+ `clone`,
173
+ `--depth`,
174
+ `1`,
175
+ `--single-branch`,
176
+ ...(ref ? [`--branch`, ref] : []),
177
+ url,
178
+ sib,
179
+ ])
180
+ return ref ?? currentRef(sib)
166
181
  }
167
182
 
168
183
  const headSha = (dir: string): string | null => tryGit([`rev-parse`, `HEAD`], dir)
@@ -194,10 +209,7 @@ const cloneClosureBranchFirst = async (cwd: string): Promise<Sibling[]> => {
194
209
  ref = currentRef(sib)
195
210
  } else {
196
211
  const repo = path.basename(sib)
197
- const url = repoUrl(repo)
198
- ref = resolveRef(url, repo)
199
- console.info(`linkRemote: cloning ${repo}@${ref} -> ${path.relative(cwd, sib)}`)
200
- gitInherit([`clone`, `--depth`, `1`, `--single-branch`, `--branch`, ref, url, sib])
212
+ ref = cloneSibling(repoUrl(repo), repo, sib, path.relative(cwd, sib))
201
213
  }
202
214
  order.push({ abs: sib, ref })
203
215
  })
@@ -22,15 +22,29 @@ const PATCHED = String.raw`/(?:^|[/\\])css\.(js|cjs|mjs|jsx|ts|tsx)(\?used)?$/`
22
22
  */
23
23
  export const patchVanillaExtract = async (cwd: string): Promise<boolean> => {
24
24
  const nodeModules = path.join(cwd, `node_modules`)
25
- const glob = new Bun.Glob(`**/@vanilla-extract/integration/dist/*.js`)
25
+
26
+ // Recursively collect `@vanilla-extract/integration/dist/*.js` WITHOUT following directory
27
+ // symlinks. A `file:` self-dep (e.g. frontend-toolkit's `frontend-toolkit: file:.` creates
28
+ // node_modules/frontend-toolkit -> .) makes symlink-following recurse forever — Bun.Glob does,
29
+ // even with `followSymlinks: false`. A linked sibling patches its own VE during its own install,
30
+ // and this repo's own copy is hoisted as a real dir, so real-dir-only recursion is sufficient.
31
+ const integrationDist = /[/\\]@vanilla-extract[/\\]integration[/\\]dist[/\\][^/\\]+\.js$/
32
+ const findFiles = async (dir: string, out: string[] = []): Promise<string[]> => {
33
+ for (const entry of await fs.readdir(dir, { withFileTypes: true }).catch(() => [])) {
34
+ if (entry.isSymbolicLink()) continue
35
+ const full = path.join(dir, entry.name)
36
+ if (entry.isDirectory()) await findFiles(full, out)
37
+ else if (entry.isFile() && integrationDist.test(full)) out.push(full)
38
+ }
39
+ return out
40
+ }
26
41
 
27
42
  let integrationFound = false
28
43
  let definitionFound = false
29
44
  const patched: string[] = []
30
45
 
31
- for await (const rel of glob.scan({ cwd: nodeModules, onlyFiles: true })) {
46
+ for (const file of await findFiles(nodeModules)) {
32
47
  integrationFound = true
33
- const file = path.join(nodeModules, rel)
34
48
  const content = await fs.readFile(file, `utf8`)
35
49
 
36
50
  if (content.includes(PATCHED)) {
@@ -41,7 +55,7 @@ export const patchVanillaExtract = async (cwd: string): Promise<boolean> => {
41
55
 
42
56
  definitionFound = true
43
57
  await fs.writeFile(file, content.replaceAll(ORIGINAL, PATCHED))
44
- patched.push(rel)
58
+ patched.push(path.relative(nodeModules, file))
45
59
  }
46
60
 
47
61
  if (!integrationFound) return false // VE not installed here