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.
- package/LICENSE +661 -0
- package/README.md +151 -0
- package/README.zh-CN.md +151 -0
- package/dist/archive/ar.d.ts +12 -0
- package/dist/archive/seven-za.d.ts +32 -0
- package/dist/archive/tar.d.ts +30 -0
- package/dist/archive/zip-reader.d.ts +17 -0
- package/dist/asar/dependency/collect.d.ts +2 -0
- package/dist/asar/dependency/detector.d.ts +8 -0
- package/dist/asar/dependency/hoister.d.ts +27 -0
- package/dist/asar/dependency/utils.d.ts +59 -0
- package/dist/asar/fileset/matcher.d.ts +12 -0
- package/dist/asar/fileset/platform-fileset.d.ts +36 -0
- package/dist/asar/fileset/transformer.d.ts +2 -0
- package/dist/asar/fileset/unpack-detector.d.ts +10 -0
- package/dist/asar/fileset/walker.d.ts +14 -0
- package/dist/asar/pack.d.ts +23 -0
- package/dist/asar/packer/app-collector.d.ts +2 -0
- package/dist/asar/packer/collector.d.ts +18 -0
- package/dist/asar/packer/concurrency.d.ts +1 -0
- package/dist/asar/packer/integrity.d.ts +8 -0
- package/dist/asar/packer/nm-collector.d.ts +7 -0
- package/dist/asar/packer/stream-builder.d.ts +18 -0
- package/dist/cli.d.ts +2 -0
- package/dist/config/index.d.ts +97 -0
- package/dist/electron/dist-zip.d.ts +18 -0
- package/dist/electron/download.d.ts +21 -0
- package/dist/index.d.ts +11 -0
- package/dist/packer/common/app-update-yml.d.ts +5 -0
- package/dist/packer/common/extra-resources.d.ts +39 -0
- package/dist/packer/common/staging.d.ts +3 -0
- package/dist/packer/common/template.d.ts +2 -0
- package/dist/packer/linux/linux.d.ts +2 -0
- package/dist/packer/mac/mac.d.ts +23 -0
- package/dist/packer/win/nsis.d.ts +24 -0
- package/dist/packer/win/resedit.d.ts +5 -0
- package/dist/packer/win/toolchain.d.ts +8 -0
- package/dist/packer/win/win.d.ts +12 -0
- package/dist/pipeline/pipeline.d.ts +41 -0
- package/dist/pipeline/targets.d.ts +28 -0
- package/dist/shared/hash.d.ts +2 -0
- package/dist/shared/logger.d.ts +39 -0
- package/dist/shared/platform.d.ts +26 -0
- package/dist/shared/types.d.ts +68 -0
- package/dist/update/blockmap.d.ts +13 -0
- package/dist/update/metadata.d.ts +20 -0
- package/dist/update/version.d.ts +7 -0
- package/package.json +71 -0
- package/src/archive/ar.ts +41 -0
- package/src/archive/seven-za.ts +160 -0
- package/src/archive/tar.ts +142 -0
- package/src/archive/zip-reader.ts +61 -0
- package/src/asar/dependency/collect.ts +437 -0
- package/src/asar/dependency/detector.ts +73 -0
- package/src/asar/dependency/hoister.ts +714 -0
- package/src/asar/dependency/utils.ts +225 -0
- package/src/asar/fileset/matcher.ts +101 -0
- package/src/asar/fileset/platform-fileset.ts +416 -0
- package/src/asar/fileset/transformer.ts +154 -0
- package/src/asar/fileset/unpack-detector.ts +38 -0
- package/src/asar/fileset/walker.ts +115 -0
- package/src/asar/pack.ts +186 -0
- package/src/asar/packer/app-collector.ts +74 -0
- package/src/asar/packer/collector.ts +134 -0
- package/src/asar/packer/concurrency.ts +24 -0
- package/src/asar/packer/integrity.ts +53 -0
- package/src/asar/packer/nm-collector.ts +194 -0
- package/src/asar/packer/stream-builder.ts +723 -0
- package/src/cli.ts +182 -0
- package/src/config/index.ts +365 -0
- package/src/electron/dist-zip.ts +65 -0
- package/src/electron/download.ts +121 -0
- package/src/index.ts +14 -0
- package/src/packer/common/app-update-yml.ts +21 -0
- package/src/packer/common/extra-resources.ts +83 -0
- package/src/packer/common/staging.ts +33 -0
- package/src/packer/common/template.ts +24 -0
- package/src/packer/linux/linux.ts +329 -0
- package/src/packer/mac/mac.ts +495 -0
- package/src/packer/win/nsis.ts +244 -0
- package/src/packer/win/resedit.ts +60 -0
- package/src/packer/win/toolchain.ts +63 -0
- package/src/packer/win/win.ts +421 -0
- package/src/pipeline/pipeline.ts +149 -0
- package/src/pipeline/targets.ts +67 -0
- package/src/shared/hash.ts +15 -0
- package/src/shared/logger.ts +91 -0
- package/src/shared/platform.ts +41 -0
- package/src/shared/types.ts +82 -0
- package/src/update/blockmap.ts +247 -0
- package/src/update/metadata.ts +97 -0
- package/src/update/version.ts +59 -0
- package/templates/linux/app.desktop +11 -0
- package/templates/linux/control +9 -0
- package/templates/linux/postinst +10 -0
- package/templates/linux/postrm +8 -0
- package/templates/linux/preinst +49 -0
- package/templates/linux/prerm +43 -0
- package/templates/mac/Info.plist +18 -0
- package/templates/mac/app-update.yml +4 -0
- package/templates/win/nsis/README.md +51 -0
- package/templates/win/nsis/THIRD-PARTY-NOTICES.md +20 -0
- package/templates/win/nsis/assistedInstaller.nsh +154 -0
- package/templates/win/nsis/assistedMessages.yml +368 -0
- package/templates/win/nsis/common.nsh +147 -0
- package/templates/win/nsis/empty-license.txt +1 -0
- package/templates/win/nsis/include/FileAssociation.nsh +123 -0
- package/templates/win/nsis/include/StdUtils.nsh +496 -0
- package/templates/win/nsis/include/StrContains.nsh +48 -0
- package/templates/win/nsis/include/UAC.nsh +299 -0
- package/templates/win/nsis/include/allowOnlyOneInstallerInstance.nsh +164 -0
- package/templates/win/nsis/include/extractAppPackage.nsh +138 -0
- package/templates/win/nsis/include/getProcessInfo.nsh +157 -0
- package/templates/win/nsis/include/installUtil.nsh +248 -0
- package/templates/win/nsis/include/installer.nsh +247 -0
- package/templates/win/nsis/include/nsProcess.nsh +28 -0
- package/templates/win/nsis/include/webPackage.nsh +64 -0
- package/templates/win/nsis/installSection.nsh +110 -0
- package/templates/win/nsis/installer.nsi +132 -0
- package/templates/win/nsis/messages.yml +240 -0
- package/templates/win/nsis/multiUser.nsh +134 -0
- package/templates/win/nsis/multiUserUi.nsh +230 -0
- package/templates/win/nsis/oneClick.nsh +19 -0
- package/templates/win/nsis/portable.nsi +91 -0
- package/templates/win/nsis/uninstaller.nsh +263 -0
- 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
|
+
}
|