@10stars/config 15.6.1 → 15.6.2

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": "15.6.1",
3
+ "version": "15.6.2",
4
4
  "private": false,
5
5
  "bin": {
6
6
  "config": "./src/index.ts"
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, linkPackages } from "./link-packages"
7
+ import { checkIsMonorepo, dedupeSingletons, linkPackages } from "./link-packages"
8
8
  import { setupOverrides } from "./manage-overrides"
9
9
  import { runConcurrently, type CommandConfig } from "./runner"
10
10
  import { setupVSCode } from "./vscode-config"
@@ -13,6 +13,7 @@ const cwd = process.env.PWD!
13
13
 
14
14
  interface Config {
15
15
  linkPackages: string[]
16
+ dedupeSingletons?: boolean | string[]
16
17
  }
17
18
 
18
19
  const exec = (cmd: string) => {
@@ -55,7 +56,15 @@ const actions = {
55
56
  await setupOverrides(cwd) // sync standardized package.json overrides
56
57
 
57
58
  const config = await getConfig()
58
- if (config?.linkPackages) await linkPackages(cwd, config.linkPackages)
59
+ if (config?.linkPackages) {
60
+ const linkedPaths = await linkPackages(cwd, config.linkPackages)
61
+ if (config.dedupeSingletons !== false) {
62
+ const singletons = Array.isArray(config.dedupeSingletons)
63
+ ? config.dedupeSingletons
64
+ : undefined
65
+ await dedupeSingletons(cwd, config.linkPackages, linkedPaths, singletons)
66
+ }
67
+ }
59
68
  },
60
69
  },
61
70
  typecheckWatch: {
@@ -24,6 +24,8 @@ export const checkIsMonorepo = async (pathPkg: string) => {
24
24
  }
25
25
 
26
26
  export const linkPackages = async (cwd: string, packagesToLink: string[]) => {
27
+ const linkedPaths: string[] = []
28
+
27
29
  const linkPackage = async (pathPkg: string) => {
28
30
  const pkgJson: TF.PackageJson = await import(path.join(pathPkg, `package.json`)).catch(
29
31
  () => null,
@@ -33,42 +35,104 @@ export const linkPackages = async (cwd: string, packagesToLink: string[]) => {
33
35
  const [orgName, pkgName] = pkgJson.name!.split(`/`)
34
36
 
35
37
  const pathSymlink = path.join(cwd, `node_modules`, orgName, pkgName)
36
- return symlink(pathPkg, pathSymlink)
38
+ await symlink(pathPkg, pathSymlink)
39
+ linkedPaths.push(pathPkg)
37
40
  }
38
41
 
39
- const promises: Promise<unknown>[] = []
40
- for (const relativePath of packagesToLink) {
41
- const pathPkg = path.join(cwd, relativePath)
42
- promises.push(
43
- checkIsMonorepo(pathPkg)
44
- .catch(() => null) // relative path might point to a non-existing directory for a particular developer; skip linking a particular package in that case
45
- .then(async (isMonorepo) => {
46
- if (!isMonorepo) return promises.push(linkPackage(pathPkg))
47
-
48
- const files = await fs.readdir(relativePath, { withFileTypes: true }).catch(() => null)
49
- if (!files) return
50
-
51
- const directories = files.filter((v) => v.isDirectory()).map((v) => v.name)
52
- const packages = (
53
- await Promise.all(
54
- directories.map(async (v) => {
55
- const pathPkg = path.join(cwd, relativePath, v)
56
- const pkgJson: TF.PackageJson = await import(
57
- path.join(pathPkg, `package.json`)
58
- ).catch(() => null)
59
- if (pkgJson) return v
60
- }),
61
- )
62
- ).filter(Boolean)
63
- for (const pkg of packages) {
64
- const relativePathToPkg = path.join(cwd, relativePath, pkg!)
65
- promises.push(linkPackage(relativePathToPkg))
66
- }
67
- }),
68
- )
69
- }
42
+ await Promise.all(
43
+ packagesToLink.map(async (relativePath) => {
44
+ const pathPkg = path.join(cwd, relativePath)
70
45
 
71
- await Promise.all(promises)
46
+ // relative path might point to a non-existing directory for a particular developer; skip linking that package in that case
47
+ const isMonorepo = await checkIsMonorepo(pathPkg).catch(() => null)
48
+ if (!isMonorepo) return linkPackage(pathPkg)
49
+
50
+ const files = await fs.readdir(relativePath, { withFileTypes: true }).catch(() => null)
51
+ if (!files) return
52
+
53
+ const directories = files.filter((v) => v.isDirectory()).map((v) => v.name)
54
+ await Promise.all(directories.map((dir) => linkPackage(path.join(cwd, relativePath, dir))))
55
+ }),
56
+ )
72
57
 
73
58
  console.info(`Successfully linked local packages!`)
59
+
60
+ return linkedPaths
61
+ }
62
+
63
+ const DEFAULT_SINGLETONS = [`@types/react`, `@types/react-dom`]
64
+
65
+ const readPkgVersion = async (pathPkg: string): Promise<string | null> => {
66
+ const pkgJson: TF.PackageJson = await import(path.join(pathPkg, `package.json`)).catch(() => null)
67
+ return pkgJson?.version ?? null
68
+ }
69
+
70
+ const majorOf = (version: string) => version.split(`.`)[0]
71
+
72
+ /**
73
+ * Collapse React type singletons to one physical copy for locally-linked graphs.
74
+ *
75
+ * tsgo resolves symlinked deps at their realpath and ignores the consumer's tsconfig
76
+ * `paths`, so multiple physical `@types/react` copies become distinct type identities.
77
+ * Pointing every copy at the workspace-root copy replicates a hoisted monorepo (one type
78
+ * identity). Best-effort and idempotent: a plain `bun install` re-materializes real dirs and
79
+ * the next `prepare` re-establishes the symlinks.
80
+ */
81
+ export const dedupeSingletons = async (
82
+ cwd: string,
83
+ packagesToLink: string[],
84
+ linkedPaths: string[],
85
+ singletons: string[] = DEFAULT_SINGLETONS,
86
+ ) => {
87
+ // siblings share cwd's parent; the formula reduces to path.dirname(cwd) for `../pkg` entries
88
+ const roots = new Set(packagesToLink.map((rel) => path.resolve(cwd, rel, `..`)))
89
+ const workspaceRoot = roots.size === 1 ? [...roots][0]! : path.dirname(cwd)
90
+
91
+ for (const pkg of singletons) {
92
+ const canonical = path.join(workspaceRoot, `node_modules`, pkg)
93
+ const canonicalVersion = await readPkgVersion(canonical)
94
+ if (!canonicalVersion) {
95
+ console.warn(`Dedupe ${pkg}: canonical copy not found at ${canonical}; skipping`)
96
+ continue
97
+ }
98
+ const canonicalReal = await fs.realpath(canonical).catch(() => canonical)
99
+
100
+ const targets = [
101
+ path.join(cwd, `node_modules`, pkg),
102
+ ...linkedPaths.map((pathPkg) => path.join(pathPkg, `node_modules`, pkg)),
103
+ ]
104
+
105
+ let relinked = 0
106
+ let skipped = 0
107
+
108
+ for (const target of targets) {
109
+ if (path.resolve(target) === path.resolve(canonical)) continue
110
+
111
+ const stats = await fs.lstat(target).catch(() => null)
112
+ if (!stats) continue
113
+
114
+ if (stats.isSymbolicLink()) {
115
+ const real = await fs.realpath(target).catch(() => null)
116
+ if (real === canonicalReal) continue // already deduped
117
+ }
118
+
119
+ const targetVersion = await readPkgVersion(target)
120
+ if (!targetVersion) continue
121
+
122
+ if (majorOf(targetVersion) !== majorOf(canonicalVersion)) {
123
+ skipped++
124
+ console.warn(
125
+ `Dedupe ${pkg}: skipping ${target} (v${targetVersion}); major differs from canonical v${canonicalVersion}`,
126
+ )
127
+ continue
128
+ }
129
+
130
+ await symlink(canonicalReal, target)
131
+ relinked++
132
+ }
133
+
134
+ console.info(
135
+ `Dedupe ${pkg}@${canonicalVersion}: ${relinked} copy(ies) relinked, ${skipped} skipped`,
136
+ )
137
+ }
74
138
  }