cross-packer 0.1.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.
Files changed (126) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +151 -0
  3. package/README.zh-CN.md +151 -0
  4. package/dist/archive/ar.d.ts +12 -0
  5. package/dist/archive/seven-za.d.ts +32 -0
  6. package/dist/archive/tar.d.ts +30 -0
  7. package/dist/archive/zip-reader.d.ts +17 -0
  8. package/dist/asar/dependency/collect.d.ts +2 -0
  9. package/dist/asar/dependency/detector.d.ts +8 -0
  10. package/dist/asar/dependency/hoister.d.ts +27 -0
  11. package/dist/asar/dependency/utils.d.ts +59 -0
  12. package/dist/asar/fileset/matcher.d.ts +12 -0
  13. package/dist/asar/fileset/platform-fileset.d.ts +36 -0
  14. package/dist/asar/fileset/transformer.d.ts +2 -0
  15. package/dist/asar/fileset/unpack-detector.d.ts +10 -0
  16. package/dist/asar/fileset/walker.d.ts +14 -0
  17. package/dist/asar/pack.d.ts +23 -0
  18. package/dist/asar/packer/app-collector.d.ts +2 -0
  19. package/dist/asar/packer/collector.d.ts +18 -0
  20. package/dist/asar/packer/concurrency.d.ts +1 -0
  21. package/dist/asar/packer/integrity.d.ts +8 -0
  22. package/dist/asar/packer/nm-collector.d.ts +7 -0
  23. package/dist/asar/packer/stream-builder.d.ts +18 -0
  24. package/dist/cli.d.ts +2 -0
  25. package/dist/config/index.d.ts +97 -0
  26. package/dist/electron/dist-zip.d.ts +18 -0
  27. package/dist/electron/download.d.ts +21 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/packer/common/app-update-yml.d.ts +5 -0
  30. package/dist/packer/common/extra-resources.d.ts +39 -0
  31. package/dist/packer/common/staging.d.ts +3 -0
  32. package/dist/packer/common/template.d.ts +2 -0
  33. package/dist/packer/linux/linux.d.ts +2 -0
  34. package/dist/packer/mac/mac.d.ts +23 -0
  35. package/dist/packer/win/nsis.d.ts +24 -0
  36. package/dist/packer/win/resedit.d.ts +5 -0
  37. package/dist/packer/win/toolchain.d.ts +8 -0
  38. package/dist/packer/win/win.d.ts +12 -0
  39. package/dist/pipeline/pipeline.d.ts +41 -0
  40. package/dist/pipeline/targets.d.ts +28 -0
  41. package/dist/shared/hash.d.ts +2 -0
  42. package/dist/shared/logger.d.ts +39 -0
  43. package/dist/shared/platform.d.ts +26 -0
  44. package/dist/shared/types.d.ts +68 -0
  45. package/dist/update/blockmap.d.ts +13 -0
  46. package/dist/update/metadata.d.ts +20 -0
  47. package/dist/update/version.d.ts +7 -0
  48. package/package.json +71 -0
  49. package/src/archive/ar.ts +41 -0
  50. package/src/archive/seven-za.ts +160 -0
  51. package/src/archive/tar.ts +142 -0
  52. package/src/archive/zip-reader.ts +61 -0
  53. package/src/asar/dependency/collect.ts +437 -0
  54. package/src/asar/dependency/detector.ts +73 -0
  55. package/src/asar/dependency/hoister.ts +714 -0
  56. package/src/asar/dependency/utils.ts +225 -0
  57. package/src/asar/fileset/matcher.ts +101 -0
  58. package/src/asar/fileset/platform-fileset.ts +416 -0
  59. package/src/asar/fileset/transformer.ts +154 -0
  60. package/src/asar/fileset/unpack-detector.ts +38 -0
  61. package/src/asar/fileset/walker.ts +115 -0
  62. package/src/asar/pack.ts +186 -0
  63. package/src/asar/packer/app-collector.ts +74 -0
  64. package/src/asar/packer/collector.ts +134 -0
  65. package/src/asar/packer/concurrency.ts +24 -0
  66. package/src/asar/packer/integrity.ts +53 -0
  67. package/src/asar/packer/nm-collector.ts +194 -0
  68. package/src/asar/packer/stream-builder.ts +723 -0
  69. package/src/cli.ts +182 -0
  70. package/src/config/index.ts +365 -0
  71. package/src/electron/dist-zip.ts +65 -0
  72. package/src/electron/download.ts +121 -0
  73. package/src/index.ts +14 -0
  74. package/src/packer/common/app-update-yml.ts +21 -0
  75. package/src/packer/common/extra-resources.ts +83 -0
  76. package/src/packer/common/staging.ts +33 -0
  77. package/src/packer/common/template.ts +24 -0
  78. package/src/packer/linux/linux.ts +329 -0
  79. package/src/packer/mac/mac.ts +495 -0
  80. package/src/packer/win/nsis.ts +244 -0
  81. package/src/packer/win/resedit.ts +60 -0
  82. package/src/packer/win/toolchain.ts +63 -0
  83. package/src/packer/win/win.ts +421 -0
  84. package/src/pipeline/pipeline.ts +149 -0
  85. package/src/pipeline/targets.ts +67 -0
  86. package/src/shared/hash.ts +15 -0
  87. package/src/shared/logger.ts +91 -0
  88. package/src/shared/platform.ts +41 -0
  89. package/src/shared/types.ts +82 -0
  90. package/src/update/blockmap.ts +247 -0
  91. package/src/update/metadata.ts +97 -0
  92. package/src/update/version.ts +59 -0
  93. package/templates/linux/app.desktop +11 -0
  94. package/templates/linux/control +9 -0
  95. package/templates/linux/postinst +10 -0
  96. package/templates/linux/postrm +8 -0
  97. package/templates/linux/preinst +49 -0
  98. package/templates/linux/prerm +43 -0
  99. package/templates/mac/Info.plist +18 -0
  100. package/templates/mac/app-update.yml +4 -0
  101. package/templates/win/nsis/README.md +51 -0
  102. package/templates/win/nsis/THIRD-PARTY-NOTICES.md +20 -0
  103. package/templates/win/nsis/assistedInstaller.nsh +154 -0
  104. package/templates/win/nsis/assistedMessages.yml +368 -0
  105. package/templates/win/nsis/common.nsh +147 -0
  106. package/templates/win/nsis/empty-license.txt +1 -0
  107. package/templates/win/nsis/include/FileAssociation.nsh +123 -0
  108. package/templates/win/nsis/include/StdUtils.nsh +496 -0
  109. package/templates/win/nsis/include/StrContains.nsh +48 -0
  110. package/templates/win/nsis/include/UAC.nsh +299 -0
  111. package/templates/win/nsis/include/allowOnlyOneInstallerInstance.nsh +164 -0
  112. package/templates/win/nsis/include/extractAppPackage.nsh +138 -0
  113. package/templates/win/nsis/include/getProcessInfo.nsh +157 -0
  114. package/templates/win/nsis/include/installUtil.nsh +248 -0
  115. package/templates/win/nsis/include/installer.nsh +247 -0
  116. package/templates/win/nsis/include/nsProcess.nsh +28 -0
  117. package/templates/win/nsis/include/webPackage.nsh +64 -0
  118. package/templates/win/nsis/installSection.nsh +110 -0
  119. package/templates/win/nsis/installer.nsi +132 -0
  120. package/templates/win/nsis/messages.yml +240 -0
  121. package/templates/win/nsis/multiUser.nsh +134 -0
  122. package/templates/win/nsis/multiUserUi.nsh +230 -0
  123. package/templates/win/nsis/oneClick.nsh +19 -0
  124. package/templates/win/nsis/portable.nsi +91 -0
  125. package/templates/win/nsis/uninstaller.nsh +263 -0
  126. package/templates/win/nsis-header.nsh +57 -0
@@ -0,0 +1,437 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { logger } from '../../shared/logger.ts'
4
+ import { detectPackageManager, PackageManager } from './detector.ts'
5
+ import {
6
+ buildProductionGraphFromFlat,
7
+ collectFromObjectTree,
8
+ execCommand,
9
+ extractJsonFromOutput,
10
+ extractProdGraphFromObjectTree,
11
+ type FlatDependency,
12
+ type HoistedNodeModule,
13
+ type RawDependencyInfo,
14
+ readRootPkgJson,
15
+ resolvePackageDir,
16
+ runHoistPipeline,
17
+ } from './utils.ts'
18
+
19
+ interface PkgJson {
20
+ name?: string
21
+ version?: string
22
+ dependencies?: Record<string, string>
23
+ optionalDependencies?: Record<string, string>
24
+ }
25
+
26
+ interface CollectorResult {
27
+ nodeModules: HoistedNodeModule[]
28
+ }
29
+
30
+ /** A "bun pm ls --json" tree node. */
31
+ interface BunDep {
32
+ name?: string
33
+ version?: string
34
+ dependencies?: BunDep[]
35
+ }
36
+
37
+ /** A package.json on disk whose version field is required. */
38
+ interface VersionedPkgJson extends PkgJson {
39
+ version: string
40
+ }
41
+
42
+ type ProductionGraph = Record<string, { dependencies: string[] }>
43
+
44
+ export async function collectProductionDeps(
45
+ projectDir: string,
46
+ workspaceRoot: string | undefined,
47
+ ): Promise<HoistedNodeModule[]> {
48
+ const searchDirectories = Array.from(new Set([projectDir, workspaceRoot].filter(Boolean)))
49
+
50
+ for (const searchDir of searchDirectories) {
51
+ if (!searchDir) continue
52
+ const pm = await detectPackageManager(searchDir)
53
+ logger.info(` Detected package manager: ${pm} (searchDir: ${searchDir})`)
54
+
55
+ let collectorResult: CollectorResult | null = null
56
+
57
+ const collectorFor: Record<PackageManager, (dir: string) => Promise<CollectorResult>> = {
58
+ [PackageManager.NPM]: collectFromNpm,
59
+ [PackageManager.YARN]: (dir) => collectFromYarn(dir, false),
60
+ [PackageManager.YARN_BERRY]: (dir) => collectFromYarn(dir, true),
61
+ [PackageManager.PNPM]: collectFromPnpm,
62
+ [PackageManager.BUN]: collectFromBun,
63
+ }
64
+ collectorResult = await tryCollect(pm, () => collectorFor[pm](searchDir))
65
+
66
+ if (collectorResult && collectorResult.nodeModules.length > 0)
67
+ return collectorResult.nodeModules
68
+
69
+ logger.info(` No node modules found in ${searchDir}, trying next search directory`)
70
+ }
71
+
72
+ logger.info(' Falling back to traversal-based dependency collection')
73
+ const collectorResult = await collectFromTraversal(projectDir)
74
+ return collectorResult.nodeModules
75
+ }
76
+
77
+ /** Run a package-manager collector with shared error handling. */
78
+ async function tryCollect(
79
+ pm: string,
80
+ collect: () => Promise<CollectorResult>,
81
+ ): Promise<CollectorResult> {
82
+ try {
83
+ return await collect()
84
+ } catch (e) {
85
+ logger.info(` ${pm} collection failed: ${e instanceof Error ? e.message : String(e)}`)
86
+ return { nodeModules: [] }
87
+ }
88
+ }
89
+
90
+ /** Collect production dependencies via `npm list --json`. */
91
+ async function collectFromNpm(projectDir: string): Promise<CollectorResult> {
92
+ const { stdout, error } = await execCommand(
93
+ 'npm',
94
+ [
95
+ 'list',
96
+ '-a',
97
+ '--include',
98
+ 'prod',
99
+ '--include',
100
+ 'optional',
101
+ '--omit',
102
+ 'dev',
103
+ '--json',
104
+ '--long',
105
+ '--silent',
106
+ '--loglevel=error',
107
+ ],
108
+ projectDir,
109
+ )
110
+
111
+ if (error && !stdout) {
112
+ logger.info(` npm list failed: ${error.message}`)
113
+ return { nodeModules: [] }
114
+ }
115
+
116
+ const parsed = extractJsonFromOutput(stdout)
117
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
118
+ logger.info(' npm list returned no dependency tree')
119
+ return { nodeModules: [] }
120
+ }
121
+ const tree = parsed as RawDependencyInfo & {
122
+ dependencies?: Record<string, RawDependencyInfo>
123
+ optionalDependencies?: Record<string, RawDependencyInfo>
124
+ }
125
+ const { rootKey } = await readRootPkgJson(projectDir)
126
+
127
+ const allDependencies = new Map<string, FlatDependency>()
128
+ collectFromObjectTree(tree.dependencies, allDependencies)
129
+
130
+ const productionGraph: ProductionGraph = {}
131
+ extractProdGraphFromObjectTree(tree, rootKey, productionGraph, allDependencies)
132
+
133
+ return runHoistPipeline(allDependencies, productionGraph, rootKey)
134
+ }
135
+
136
+ /** Collect production dependencies via `pnpm list --json`. */
137
+ async function collectFromPnpm(projectDir: string): Promise<CollectorResult> {
138
+ const { stdout, error } = await execCommand(
139
+ 'pnpm',
140
+ ['list', '--prod', '--json', '--depth', 'Infinity', '--silent'],
141
+ projectDir,
142
+ )
143
+
144
+ if (error && !stdout) {
145
+ logger.info(` pnpm list failed: ${error.message}`)
146
+ return { nodeModules: [] }
147
+ }
148
+
149
+ const trees = extractJsonFromOutput(stdout)
150
+ const arr = (Array.isArray(trees) ? trees : [trees]).filter(
151
+ (
152
+ it,
153
+ ): it is RawDependencyInfo & {
154
+ dependencies?: Record<string, RawDependencyInfo>
155
+ optionalDependencies?: Record<string, RawDependencyInfo>
156
+ } => !!it && typeof it === 'object',
157
+ )
158
+ const { rootKey } = await readRootPkgJson(projectDir)
159
+
160
+ const allDependencies = new Map<string, FlatDependency>()
161
+ const productionGraph: ProductionGraph = {}
162
+ for (const tree of arr as (RawDependencyInfo & {
163
+ dependencies?: Record<string, RawDependencyInfo>
164
+ optionalDependencies?: Record<string, RawDependencyInfo>
165
+ })[]) {
166
+ collectFromObjectTree(tree.dependencies, allDependencies)
167
+ extractProdGraphFromObjectTree(tree, rootKey, productionGraph, allDependencies)
168
+ }
169
+
170
+ return runHoistPipeline(allDependencies, productionGraph, rootKey)
171
+ }
172
+
173
+ async function collectFromYarn(projectDir: string, isBerry: boolean): Promise<CollectorResult> {
174
+ const allDependencies = new Map<string, FlatDependency>()
175
+ const productionGraph: ProductionGraph = {}
176
+
177
+ if (isBerry) {
178
+ const { stdout, error } = await execCommand(
179
+ 'yarn',
180
+ ['workspaces', 'list', '--json'],
181
+ projectDir,
182
+ )
183
+ if (error && !stdout) {
184
+ logger.info(` yarn workspaces list failed: ${error.message}`)
185
+ return { nodeModules: [] }
186
+ }
187
+ const { rootKey } = await readRootPkgJson(projectDir)
188
+ collectAllDependenciesFromYarnBerry(stdout, projectDir, allDependencies)
189
+ const rootPkg = JSON.parse(
190
+ readFileSync(path.join(projectDir, 'package.json'), 'utf8'),
191
+ ) as PkgJson
192
+ extractProductionGraphForPackage(
193
+ rootPkg,
194
+ rootKey,
195
+ productionGraph,
196
+ allDependencies,
197
+ path.join(projectDir, 'node_modules'),
198
+ )
199
+ return runHoistPipeline(allDependencies, productionGraph, rootKey)
200
+ }
201
+
202
+ const { stdout, error } = await execCommand(
203
+ 'yarn',
204
+ ['list', '--production', '--json', '--silent'],
205
+ projectDir,
206
+ )
207
+ if (error && !stdout) {
208
+ logger.info(` yarn list failed: ${error.message}`)
209
+ return { nodeModules: [] }
210
+ }
211
+ const { rootKey } = await readRootPkgJson(projectDir)
212
+ collectAllDependenciesFromYarnClassic(stdout, projectDir, allDependencies)
213
+ buildProductionGraphFromFlat(allDependencies, productionGraph, rootKey)
214
+ return runHoistPipeline(allDependencies, productionGraph, rootKey)
215
+ }
216
+
217
+ function collectAllDependenciesFromYarnClassic(
218
+ stdout: string,
219
+ projectDir: string,
220
+ allDependencies: Map<string, FlatDependency>,
221
+ ): void {
222
+ const nmDir = path.join(projectDir, 'node_modules')
223
+ try {
224
+ const lines = stdout.trim().split('\n')
225
+ for (const line of lines) {
226
+ const entry = JSON.parse(line) as {
227
+ type?: string
228
+ data?: { trees?: { name: string }[] }
229
+ }
230
+ if (entry.type !== 'tree') continue
231
+ const trees = entry.data?.trees || []
232
+ for (const tree of trees) {
233
+ const name = tree.name
234
+ const color = name.lastIndexOf('@')
235
+ const pkgName = color > 0 ? name.substring(0, color) : name
236
+ const version = color > 0 ? name.substring(color + 1) : ''
237
+ const pkgDir = resolvePackageDir(nmDir, pkgName)
238
+ if (pkgDir) {
239
+ allDependencies.set(`${pkgName}@${version}`, { name: pkgName, version, path: pkgDir })
240
+ }
241
+ }
242
+ }
243
+ } catch {}
244
+ }
245
+
246
+ function collectAllDependenciesFromYarnBerry(
247
+ stdout: string,
248
+ projectDir: string,
249
+ allDependencies: Map<string, FlatDependency>,
250
+ ): void {
251
+ try {
252
+ const lines = stdout.trim().split('\n')
253
+ for (const line of lines) {
254
+ const entry = JSON.parse(line) as {
255
+ location?: string
256
+ name?: string
257
+ version?: string
258
+ }
259
+ if (entry.location && entry.name) {
260
+ const pkgDir = path.resolve(projectDir, entry.location)
261
+ if (existsSync(pkgDir)) {
262
+ const key = `${entry.name}@${entry.version || 'unknown'}`
263
+ allDependencies.set(key, {
264
+ name: entry.name,
265
+ version: entry.version || '',
266
+ path: pkgDir,
267
+ })
268
+ }
269
+ }
270
+ }
271
+ } catch {}
272
+ }
273
+
274
+ /**
275
+ * Recursively build the production dependency graph for a package: walk
276
+ * its production deps on disk, resolve each child's versioned key, and
277
+ * recurse. Shared by the yarn-berry collector (root + transitive packages).
278
+ */
279
+ function extractProductionGraphForPackage(
280
+ pkg: PkgJson,
281
+ dependencyId: string,
282
+ productionGraph: ProductionGraph,
283
+ allDependencies: Map<string, FlatDependency>,
284
+ nmDir: string,
285
+ ): void {
286
+ if (productionGraph[dependencyId]) return
287
+ productionGraph[dependencyId] = { dependencies: [] }
288
+ const collectedDependencies: string[] = []
289
+ const prodDeps = { ...(pkg.dependencies || {}), ...(pkg.optionalDependencies || {}) }
290
+ for (const name of Object.keys(prodDeps)) {
291
+ const pkgDir = resolvePackageDir(nmDir, name)
292
+ if (!pkgDir) continue
293
+ const pkgJsonPath = path.join(pkgDir, 'package.json')
294
+ if (!existsSync(pkgJsonPath)) continue
295
+ try {
296
+ const childPkg = readVersionedPkgJson(pkgJsonPath)
297
+ const childKey = `${name}@${childPkg.version}`
298
+ if (!productionGraph[dependencyId] || allDependencies.has(childKey)) {
299
+ extractProductionGraphForPackage(
300
+ childPkg,
301
+ childKey,
302
+ productionGraph,
303
+ allDependencies,
304
+ nmDir,
305
+ )
306
+ }
307
+ collectedDependencies.push(childKey)
308
+ } catch {}
309
+ }
310
+ productionGraph[dependencyId] = { dependencies: collectedDependencies }
311
+ }
312
+
313
+ /** Collect production dependencies via `bun pm ls --json`. */
314
+ async function collectFromBun(projectDir: string): Promise<CollectorResult> {
315
+ const { stdout, error } = await execCommand('bun', ['pm', 'ls', '--json'], projectDir)
316
+
317
+ if (error && !stdout) {
318
+ logger.info(` bun pm ls failed: ${error.message}`)
319
+ return { nodeModules: [] }
320
+ }
321
+
322
+ const tree = extractJsonFromOutput(stdout)
323
+ const { rootKey } = await readRootPkgJson(projectDir)
324
+
325
+ const allDependencies = new Map<string, FlatDependency>()
326
+ collectAllDependenciesFromBunTree(tree, path.join(projectDir, 'node_modules'), allDependencies)
327
+
328
+ const productionGraph: ProductionGraph = {}
329
+ buildProductionGraphFromFlat(allDependencies, productionGraph, rootKey)
330
+
331
+ return runHoistPipeline(allDependencies, productionGraph, rootKey)
332
+ }
333
+
334
+ function isBunDep(value: unknown): value is BunDep {
335
+ return typeof value === 'object' && value !== null
336
+ }
337
+
338
+ function collectAllDependenciesFromBunTree(
339
+ deps: unknown,
340
+ nmDir: string,
341
+ allDependencies: Map<string, FlatDependency>,
342
+ ): void {
343
+ if (!deps || !Array.isArray(deps)) return
344
+ for (const dep of deps) {
345
+ if (!isBunDep(dep) || !dep.name) continue
346
+ const pkgDir = resolvePackageDir(nmDir, dep.name)
347
+ if (pkgDir) {
348
+ const key = `${dep.name}@${dep.version || 'unknown'}`
349
+ allDependencies.set(key, { name: dep.name, version: dep.version || '', path: pkgDir })
350
+ }
351
+ if (dep.dependencies) {
352
+ collectAllDependenciesFromBunTree(dep.dependencies, nmDir, allDependencies)
353
+ }
354
+ }
355
+ }
356
+
357
+ /** Fallback: collect dependencies by traversing node_modules on disk. */
358
+ async function collectFromTraversal(projectDir: string): Promise<CollectorResult> {
359
+ const nmDir = path.join(projectDir, 'node_modules')
360
+ if (!existsSync(nmDir)) {
361
+ return { nodeModules: [] }
362
+ }
363
+
364
+ const allDependencies = new Map<string, FlatDependency>()
365
+ const productionGraph: ProductionGraph = {}
366
+ const visited = new Set<string>()
367
+ const queue: string[] = []
368
+
369
+ const rootPkg = readVersionedPkgJson(path.join(projectDir, 'package.json'))
370
+ const rootKey = `${rootPkg.name}@${rootPkg.version}`
371
+ const rootProdDeps: string[] = []
372
+ const rootDeps = { ...(rootPkg.dependencies || {}), ...(rootPkg.optionalDependencies || {}) }
373
+
374
+ for (const name of Object.keys(rootDeps)) {
375
+ queue.push(name)
376
+ }
377
+
378
+ while (queue.length > 0) {
379
+ const name = queue.shift()
380
+ if (!name || visited.has(name)) continue
381
+ visited.add(name)
382
+
383
+ const pkgDir = resolvePackageDir(nmDir, name)
384
+ if (!pkgDir || !existsSync(pkgDir)) continue
385
+
386
+ const pkgJsonPath = path.join(pkgDir, 'package.json')
387
+ if (!existsSync(pkgJsonPath)) continue
388
+
389
+ try {
390
+ const pkg = readVersionedPkgJson(pkgJsonPath)
391
+ const key = `${name}@${pkg.version}`
392
+ allDependencies.set(key, { name, version: pkg.version, path: pkgDir })
393
+
394
+ const prodDeps = { ...(pkg.dependencies || {}), ...(pkg.optionalDependencies || {}) }
395
+ for (const dep of Object.keys(prodDeps)) {
396
+ if (!visited.has(dep)) queue.push(dep)
397
+ }
398
+ productionGraph[key] = { dependencies: [] }
399
+
400
+ if (rootDeps[name] !== undefined) {
401
+ rootProdDeps.push(key)
402
+ }
403
+ } catch {}
404
+ }
405
+
406
+ for (const [key, info] of allDependencies) {
407
+ if (!productionGraph[key]) continue
408
+ const pkgJsonPath = path.join(info.path, 'package.json')
409
+ try {
410
+ const pkg = readVersionedPkgJson(pkgJsonPath)
411
+ const childKeys: string[] = []
412
+ const prodDeps = { ...(pkg.dependencies || {}), ...(pkg.optionalDependencies || {}) }
413
+ for (const depName of Object.keys(prodDeps)) {
414
+ const childDir = resolvePackageDir(nmDir, depName)
415
+ if (!childDir) continue
416
+ const childPkgJsonPath = path.join(childDir, 'package.json')
417
+ if (!existsSync(childPkgJsonPath)) continue
418
+ const childPkg = readVersionedPkgJson(childPkgJsonPath)
419
+ childKeys.push(`${depName}@${childPkg.version}`)
420
+ }
421
+ productionGraph[key] = { dependencies: childKeys }
422
+ } catch {}
423
+ }
424
+
425
+ productionGraph[rootKey] = { dependencies: rootProdDeps }
426
+
427
+ return runHoistPipeline(allDependencies, productionGraph, rootKey)
428
+ }
429
+
430
+ /** Read and parse a package.json, requiring the version field. */
431
+ function readVersionedPkgJson(pkgJsonPath: string): VersionedPkgJson {
432
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) as PkgJson
433
+ if (!pkg.version) {
434
+ throw new Error(`package.json missing version: ${pkgJsonPath}`)
435
+ }
436
+ return pkg as VersionedPkgJson
437
+ }
@@ -0,0 +1,73 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { readFile } from 'node:fs/promises'
3
+ import path from 'node:path'
4
+
5
+ export enum PackageManager {
6
+ NPM = 'npm',
7
+ YARN = 'yarn',
8
+ YARN_BERRY = 'yarn-berry',
9
+ PNPM = 'pnpm',
10
+ BUN = 'bun',
11
+ }
12
+
13
+ export async function detectPackageManager(projectDir: string): Promise<PackageManager> {
14
+ const pkgJsonPath = path.join(projectDir, 'package.json')
15
+ if (existsSync(pkgJsonPath)) {
16
+ try {
17
+ const pkg = JSON.parse(await readFile(pkgJsonPath, 'utf8')) as {
18
+ packageManager?: string
19
+ }
20
+ if (pkg.packageManager) {
21
+ const [pm, version] = pkg.packageManager.split('@')
22
+ if (pm === PackageManager.YARN && version && isYarnBerry(version)) {
23
+ return PackageManager.YARN_BERRY
24
+ }
25
+ if (
26
+ pm === PackageManager.NPM ||
27
+ pm === PackageManager.YARN ||
28
+ pm === PackageManager.PNPM ||
29
+ pm === PackageManager.BUN
30
+ ) {
31
+ return pm
32
+ }
33
+ }
34
+ } catch {}
35
+ }
36
+
37
+ if (existsSync(path.join(projectDir, 'pnpm-lock.yaml'))) return PackageManager.PNPM
38
+ if (existsSync(path.join(projectDir, 'yarn.lock'))) {
39
+ if (await isYarnBerryByLockfile(projectDir)) return PackageManager.YARN_BERRY
40
+ return PackageManager.YARN
41
+ }
42
+ if (
43
+ existsSync(path.join(projectDir, 'bun.lockb')) ||
44
+ existsSync(path.join(projectDir, 'bun.lock'))
45
+ )
46
+ return PackageManager.BUN
47
+ if (existsSync(path.join(projectDir, 'package-lock.json'))) return PackageManager.NPM
48
+
49
+ const userAgent = process.env.npm_config_user_agent || ''
50
+ if (userAgent.includes('pnpm')) return PackageManager.PNPM
51
+ if (userAgent.includes('yarn')) return PackageManager.YARN
52
+ if (userAgent.includes('bun')) return PackageManager.BUN
53
+
54
+ return PackageManager.NPM
55
+ }
56
+
57
+ /** Check whether a yarn version is Berry (v2+). */
58
+ function isYarnBerry(version: string): boolean {
59
+ if (!version) return false
60
+ const major = Number.parseInt(version.split('.')[0], 10)
61
+ return major >= 2
62
+ }
63
+
64
+ /** Check the yarn.lock content for Berry's __metadata header. */
65
+ async function isYarnBerryByLockfile(projectDir: string): Promise<boolean> {
66
+ const lockPath = path.join(projectDir, 'yarn.lock')
67
+ try {
68
+ const content = await readFile(lockPath, 'utf8')
69
+ return content.includes('__metadata:')
70
+ } catch {
71
+ return false
72
+ }
73
+ }