Skip to content

Commit 7214602

Browse files
committed
feat: bun package manager support for electron-builder
1 parent 0835fbc commit 7214602

File tree

18 files changed

+1096
-60
lines changed

18 files changed

+1096
-60
lines changed

package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"@types/node": "^22.7.4",
5757
"@typescript-eslint/eslint-plugin": "8.17.0",
5858
"@typescript-eslint/parser": "8.17.0",
59-
"@vitest/ui": "3.0.4",
59+
"@vitest/ui": "^3.2.2",
6060
"chalk": "^4.1.2",
6161
"conventional-changelog-cli": "5.0.0",
6262
"depcheck": "1.4.3",
@@ -83,6 +83,12 @@
8383
"patchedDependencies": {
8484
"@changesets/cli@2.29.7": "patches/@changesets__cli@2.29.7.patch",
8585
"@changesets/assemble-release-plan@6.0.9": "patches/@changesets__assemble-release-plan@6.0.9.patch"
86-
}
86+
},
87+
"onlyBuiltDependencies": [
88+
"@parcel/watcher",
89+
"electron",
90+
"electron-winstaller",
91+
"esbuild"
92+
]
8793
}
8894
}

packages/app-builder-lib/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"minimatch": "^10.0.3",
7777
"plist": "3.1.0",
7878
"resedit": "^1.7.0",
79+
"resolve": "^1.22.10",
7980
"semver": "7.7.2",
8081
"tar": "^6.1.12",
8182
"temp-file": "^3.4.0",
@@ -108,6 +109,7 @@
108109
"@types/hosted-git-info": "3.0.2",
109110
"@types/js-yaml": "4.0.3",
110111
"@types/plist": "3.0.5",
112+
"@types/resolve": "^1.20.6",
111113
"@types/semver": "7.7.1",
112114
"@types/tar": "^6.1.3",
113115
"@types/tiny-async-pool": "^1",

packages/app-builder-lib/src/electron/search-module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export async function searchForNodeModules(cwd: string, rootPath?: string): Prom
7070
* @param cwd the initial directory to traverse
7171
*/
7272
export async function getProjectRootPath(cwd: string): Promise<string> {
73-
for (const lockFilename of ["yarn.lock", "package-lock.json", "pnpm-lock.yaml"]) {
73+
for (const lockFilename of ["yarn.lock", "package-lock.json", "pnpm-lock.yaml", "bun.lock", "bun.lockb"]) {
7474
const pathGenerator: PathGeneratorFunction = traversedPath => path.join(traversedPath, lockFilename)
7575
const lockPaths = await traverseAncestorDirectories(cwd, pathGenerator, undefined, 1)
7676
if (lockPaths.length > 0) {

packages/app-builder-lib/src/fileMatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const excludedNames =
1515
".git,.hg,.svn,CVS,RCS,SCCS," +
1616
"__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore," +
1717
".idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci," +
18-
".yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,pnpm-lock.yaml," +
18+
".yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,pnpm-lock.yaml,bun.lock,bun.lockb" +
1919
"appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env"
2020

2121
export const excludedExts =
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { log } from "builder-util"
2+
import * as path from "path"
3+
import * as resolve from "resolve"
4+
import { NodeModulesCollector } from "./nodeModulesCollector"
5+
import { PM } from "./packageManager"
6+
import { Dependency } from "./types"
7+
8+
interface BunDependency extends Dependency<BunDependency, BunDependency> {
9+
manifestDependencies: Record<string, string>
10+
manifestOptionalDependencies: Record<string, string>
11+
}
12+
13+
export class BunNodeModulesCollector extends NodeModulesCollector<BunDependency, BunDependency> {
14+
public readonly installOptions = { manager: PM.BUN, lockfile: "bun.lock" }
15+
16+
private readonly dependencyCacheByPath = new Map<string, BunDependency>()
17+
18+
protected async getDependenciesTree(): Promise<BunDependency> {
19+
const rootManifest = require(path.join(this.rootDir, "package.json"))
20+
const rootName = rootManifest.name ?? "."
21+
22+
const childMaps = await this.resolveChildren(this.rootDir, rootManifest.dependencies ?? {}, rootManifest.optionalDependencies ?? {})
23+
return {
24+
name: rootName,
25+
version: rootManifest.version ?? "0.0.0",
26+
path: this.rootDir,
27+
manifestDependencies: rootManifest.dependencies ?? {},
28+
manifestOptionalDependencies: rootManifest.optionalDependencies ?? {},
29+
dependencies: Object.keys(childMaps.dependencies).length > 0 ? childMaps.dependencies : undefined,
30+
optionalDependencies: Object.keys(childMaps.optionalDependencies).length > 0 ? childMaps.optionalDependencies : undefined,
31+
}
32+
}
33+
34+
protected getArgs(): string[] {
35+
return []
36+
}
37+
38+
protected collectAllDependencies(tree: BunDependency): void {
39+
const allDeps = [...Object.values(tree.dependencies || {}), ...Object.values(tree.optionalDependencies || {})]
40+
41+
for (const dependency of allDeps) {
42+
const key = `${dependency.name}@${dependency.version}`
43+
if (!this.allDependencies.has(key)) {
44+
this.allDependencies.set(key, dependency)
45+
this.collectAllDependencies(dependency)
46+
}
47+
}
48+
}
49+
50+
protected extractProductionDependencyGraph(tree: BunDependency, dependencyId: string): void {
51+
if (this.productionGraph[dependencyId]) {
52+
return
53+
}
54+
55+
const dependencies: string[] = []
56+
57+
const processDeps = [
58+
{ entries: tree.dependencies, manifest: tree.manifestDependencies },
59+
{ entries: tree.optionalDependencies, manifest: tree.manifestOptionalDependencies },
60+
]
61+
62+
for (const { entries, manifest } of processDeps) {
63+
if (!entries) continue
64+
65+
for (const [alias, dep] of Object.entries(entries)) {
66+
if (manifest[alias]) {
67+
const childId = `${dep.name}@${dep.version}`
68+
dependencies.push(childId)
69+
this.extractProductionDependencyGraph(dep, childId)
70+
}
71+
}
72+
}
73+
74+
this.productionGraph[dependencyId] = { dependencies }
75+
}
76+
77+
protected parseDependenciesTree(_jsonBlob: string): BunDependency {
78+
throw new Error("BunNodeModulesCollector does not parse external dependency trees")
79+
}
80+
81+
private async resolveChildren(
82+
requesterDir: string,
83+
manifestDependencies: Record<string, string>,
84+
manifestOptionalDependencies: Record<string, string>
85+
): Promise<{
86+
dependencies: Record<string, BunDependency>
87+
optionalDependencies: Record<string, BunDependency>
88+
}> {
89+
const dependencies: Record<string, BunDependency> = {}
90+
const optionalDependencies: Record<string, BunDependency> = {}
91+
92+
const loadPromises: Promise<void>[] = []
93+
94+
for (const alias of Object.keys(manifestDependencies)) {
95+
loadPromises.push(
96+
this.loadDependency(alias, requesterDir, false).then(dep => {
97+
if (dep) dependencies[alias] = dep
98+
})
99+
)
100+
}
101+
102+
for (const alias of Object.keys(manifestOptionalDependencies)) {
103+
loadPromises.push(
104+
this.loadDependency(alias, requesterDir, true).then(dep => {
105+
if (dep) optionalDependencies[alias] = dep
106+
})
107+
)
108+
}
109+
110+
await Promise.all(loadPromises)
111+
return { dependencies, optionalDependencies }
112+
}
113+
114+
private async loadDependency(alias: string, requesterDir: string, isOptional: boolean): Promise<BunDependency | null> {
115+
const installedPath = this.findInstalledDependency(requesterDir, alias)
116+
if (!installedPath) {
117+
if (!isOptional) {
118+
log.debug({ alias, requesterDir }, "bun collector could not locate dependency")
119+
}
120+
return null
121+
}
122+
123+
// Use resolved path directly - resolve.sync already handles symlinks with preserveSymlinks: false
124+
const cached = this.dependencyCacheByPath.get(installedPath)
125+
if (cached) {
126+
return cached
127+
}
128+
129+
const manifest = require(path.join(installedPath, "package.json"))
130+
const packageName = manifest.name ?? alias
131+
const manifestDependencies = manifest.dependencies ?? {}
132+
const manifestOptionalDependencies = manifest.optionalDependencies ?? {}
133+
134+
// Create a temporary placeholder to prevent infinite recursion
135+
const placeholder: BunDependency = {
136+
name: packageName,
137+
version: manifest.version ?? "0.0.0",
138+
path: installedPath,
139+
manifestDependencies,
140+
manifestOptionalDependencies,
141+
}
142+
this.dependencyCacheByPath.set(installedPath, placeholder)
143+
144+
const childMaps = await this.resolveChildren(installedPath, manifestDependencies, manifestOptionalDependencies)
145+
146+
const dependency: BunDependency = {
147+
name: packageName,
148+
version: manifest.version ?? "0.0.0",
149+
path: installedPath,
150+
manifestDependencies,
151+
manifestOptionalDependencies,
152+
dependencies: Object.keys(childMaps.dependencies).length > 0 ? childMaps.dependencies : undefined,
153+
optionalDependencies: Object.keys(childMaps.optionalDependencies).length > 0 ? childMaps.optionalDependencies : undefined,
154+
}
155+
156+
this.dependencyCacheByPath.set(installedPath, dependency)
157+
return dependency
158+
}
159+
160+
private findInstalledDependency(basedir: string, dependencyName: string): string | null {
161+
try {
162+
const packageJsonPath = resolve.sync(`${dependencyName}${path.sep}package.json`, {
163+
basedir,
164+
preserveSymlinks: false,
165+
includeCoreModules: false,
166+
})
167+
return path.dirname(packageJsonPath)
168+
} catch (e: any) {
169+
if (e && e.code === "MODULE_NOT_FOUND") {
170+
return null
171+
}
172+
throw e
173+
}
174+
}
175+
}

packages/app-builder-lib/src/node-module-collector/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BunNodeModulesCollector } from "./bunNodeModulesCollector"
12
import { NpmNodeModulesCollector } from "./npmNodeModulesCollector"
23
import { PnpmNodeModulesCollector } from "./pnpmNodeModulesCollector"
34
import { YarnNodeModulesCollector } from "./yarnNodeModulesCollector"
@@ -13,8 +14,9 @@ export async function getCollectorByPackageManager(pm: PM, rootDir: string, temp
1314
}
1415
return new PnpmNodeModulesCollector(rootDir, tempDirManager)
1516
case PM.NPM:
16-
case PM.BUN:
1717
return new NpmNodeModulesCollector(rootDir, tempDirManager)
18+
case PM.BUN:
19+
return new BunNodeModulesCollector(rootDir, tempDirManager)
1820
case PM.YARN:
1921
return new YarnNodeModulesCollector(rootDir, tempDirManager)
2022
default:
@@ -53,4 +55,4 @@ export function detectPackageManager(dirs: string[]): PM {
5355
return PM.NPM
5456
}
5557

56-
export { PM, getPackageManagerCommand }
58+
export { PM, getPackageManagerCommand, BunNodeModulesCollector }

packages/app-builder-lib/src/node-module-collector/nodeModulesCollector.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export abstract class NodeModulesCollector<T extends Dependency<T, OptionalsType
1616
protected productionGraph: DependencyGraph = {}
1717

1818
constructor(
19-
private readonly rootDir: string,
20-
private readonly tempDirManager: TmpDir
19+
protected readonly rootDir: string,
20+
protected readonly tempDirManager: TmpDir
2121
) {}
2222

2323
public async getNodeModules(): Promise<NodeModuleInfo[]> {

packages/app-builder-lib/src/util/appFileCopier.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { AppFileWalker } from "./AppFileWalker"
1414
import { NodeModuleCopyHelper } from "./NodeModuleCopyHelper"
1515
import { NodeModuleInfo } from "./packageDependencies"
1616
import { getNodeModules, detectPackageManager } from "../node-module-collector"
17+
import { getProjectRootPath } from "../electron/search-module"
1718

1819
const BOWER_COMPONENTS_PATTERN = `${path.sep}bower_components${path.sep}`
1920
/** @internal */
@@ -176,12 +177,42 @@ function validateFileSet(fileSet: ResolvedFileSet): ResolvedFileSet {
176177
return fileSet
177178
}
178179

180+
async function getGitRootDir(baseDir: string): Promise<string | null> {
181+
try {
182+
const { promisify } = require("util")
183+
const { exec } = require("child_process")
184+
const execAsync = promisify(exec)
185+
186+
const { stdout } = await execAsync("git rev-parse --show-toplevel", {
187+
cwd: baseDir,
188+
timeout: 5000,
189+
})
190+
return stdout.trim()
191+
} catch (_error) {
192+
// Git not installed, not in a git repo, or other error
193+
return null
194+
}
195+
}
196+
179197
/** @internal */
180198
export async function computeNodeModuleFileSets(platformPackager: PlatformPackager<any>, mainMatcher: FileMatcher): Promise<Array<ResolvedFileSet>> {
181199
const projectDir = platformPackager.info.projectDir
182200
const appDir = platformPackager.info.appDir
183201

184-
const pm = detectPackageManager(appDir === projectDir ? [appDir] : [appDir, projectDir])
202+
const lockfileRootPath = await getProjectRootPath(appDir)
203+
const gitRootDir = await getGitRootDir(appDir)
204+
205+
const dirsToCheck = appDir === projectDir ? [appDir] : [appDir, projectDir]
206+
207+
if (lockfileRootPath && !dirsToCheck.includes(lockfileRootPath)) {
208+
dirsToCheck.push(lockfileRootPath)
209+
}
210+
211+
if (gitRootDir && !dirsToCheck.includes(gitRootDir)) {
212+
dirsToCheck.push(gitRootDir)
213+
}
214+
215+
const pm = detectPackageManager(dirsToCheck)
185216

186217
let deps = await getNodeModules(pm, appDir, platformPackager.info.tempDirManager)
187218
if (projectDir !== appDir && deps.length === 0) {

0 commit comments

Comments
 (0)