-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat: bun package manager support for electron-builder #9313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "app-builder-lib": minor | ||
| --- | ||
|
|
||
| add support for bundling with bun package manager |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -108,6 +108,7 @@ | |
| "@types/hosted-git-info": "3.0.2", | ||
| "@types/js-yaml": "4.0.3", | ||
| "@types/plist": "3.0.5", | ||
| "@types/resolve": "^1.20.6", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this still needed? |
||
| "@types/semver": "7.7.1", | ||
| "@types/tar": "^6.1.3", | ||
| "@types/tiny-async-pool": "^1", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| import { log } from "builder-util" | ||
| import * as path from "path" | ||
| import { NodeModulesCollector } from "./nodeModulesCollector" | ||
| import { PM } from "./packageManager" | ||
| import { BunDependency, BunManifest, Dependencies } from "./types" | ||
| import { createRequire } from "module" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not familiar with this |
||
|
|
||
| export class BunNodeModulesCollector extends NodeModulesCollector<BunDependency, BunDependency> { | ||
| public readonly installOptions = { manager: PM.BUN, lockfile: "bun.lock" } | ||
|
|
||
| private readonly dependencyCacheByPath = new Map<string, BunDependency>() | ||
|
|
||
| protected async getDependenciesTree(): Promise<BunDependency> { | ||
| const rootManifest = require(path.join(this.rootDir, "package.json")) | ||
| const rootName = rootManifest.name ?? "." | ||
|
|
||
| const childMaps = await this.resolveChildren(this.rootDir, { | ||
| manifestDependencies: rootManifest.dependencies ?? {}, | ||
| manifestOptionalDependencies: rootManifest.optionalDependencies ?? {}, | ||
| }) | ||
| return { | ||
| name: rootName, | ||
| version: rootManifest.version ?? "0.0.0", | ||
| path: this.rootDir, | ||
| manifestDependencies: rootManifest.dependencies ?? {}, | ||
| manifestOptionalDependencies: rootManifest.optionalDependencies ?? {}, | ||
| dependencies: Object.keys(childMaps.dependencies ?? {}).length > 0 ? childMaps.dependencies : undefined, | ||
| optionalDependencies: Object.keys(childMaps.optionalDependencies ?? {}).length > 0 ? childMaps.optionalDependencies : undefined, | ||
| } | ||
| } | ||
|
|
||
| protected getArgs(): string[] { | ||
| return [] | ||
| } | ||
|
|
||
| protected collectAllDependencies(tree: BunDependency): void { | ||
| const allDeps = [...Object.values(tree.dependencies || {}), ...Object.values(tree.optionalDependencies || {})] | ||
|
|
||
| for (const dependency of allDeps) { | ||
| const key = `${dependency.name}@${dependency.version}` | ||
| if (!this.allDependencies.has(key)) { | ||
| this.allDependencies.set(key, dependency) | ||
| this.collectAllDependencies(dependency) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| protected extractProductionDependencyGraph(tree: BunDependency, dependencyId: string): void { | ||
| if (this.productionGraph[dependencyId]) { | ||
| return | ||
| } | ||
|
|
||
| const dependencies: string[] = [] | ||
|
|
||
| const processDeps = [ | ||
| { entries: tree.dependencies, manifest: tree.manifestDependencies }, | ||
| { entries: tree.optionalDependencies, manifest: tree.manifestOptionalDependencies }, | ||
| ] | ||
|
|
||
| for (const { entries, manifest } of processDeps) { | ||
| if (!entries) { | ||
| continue | ||
| } | ||
|
|
||
| for (const [alias, dep] of Object.entries(entries)) { | ||
| if (manifest[alias]) { | ||
| const childId = `${dep.name}@${dep.version}` | ||
| dependencies.push(childId) | ||
| this.extractProductionDependencyGraph(dep, childId) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| this.productionGraph[dependencyId] = { dependencies } | ||
| } | ||
|
|
||
| protected parseDependenciesTree(jsonBlob: string): BunDependency { | ||
| return JSON.parse(jsonBlob) | ||
| } | ||
|
|
||
| private async resolveChildren(requesterDir: string, manifest: BunManifest): Promise<Dependencies<BunDependency, BunDependency>> { | ||
| const dependencies: Record<string, BunDependency> = {} | ||
| const optionalDependencies: Record<string, BunDependency> = {} | ||
|
|
||
| for (const alias of Object.keys(manifest.manifestDependencies)) { | ||
| const dependency = await this.loadDependency(alias, requesterDir, false) | ||
| if (dependency) { | ||
| dependencies[alias] = dependency | ||
| } | ||
| } | ||
|
|
||
| for (const alias of Object.keys(manifest.manifestOptionalDependencies)) { | ||
| const dependency = await this.loadDependency(alias, requesterDir, true) | ||
| if (dependency) { | ||
| optionalDependencies[alias] = dependency | ||
| } | ||
| } | ||
|
|
||
| return { dependencies, optionalDependencies } | ||
| } | ||
|
|
||
| private async loadDependency(alias: string, requesterDir: string, isOptional: boolean): Promise<BunDependency | null> { | ||
| const installedPath = this.findInstalledDependency(requesterDir, alias) | ||
| if (!installedPath) { | ||
| if (!isOptional) { | ||
| log.debug({ alias, requesterDir }, "bun collector could not locate dependency") | ||
| } | ||
| return null | ||
| } | ||
|
|
||
| // Use resolved path directly - resolve.sync already handles symlinks with preserveSymlinks: false | ||
| const cached = this.dependencyCacheByPath.get(installedPath) | ||
| if (cached) { | ||
| return cached | ||
| } | ||
|
|
||
| const manifest = require(path.join(installedPath, "package.json")) | ||
| const packageName = manifest.name ?? alias | ||
| const manifestDependencies = manifest.dependencies ?? {} | ||
| const manifestOptionalDependencies = manifest.optionalDependencies ?? {} | ||
|
|
||
| // Create a temporary placeholder to prevent infinite recursion | ||
| const placeholder: BunDependency = { | ||
| name: packageName, | ||
| version: manifest.version ?? "0.0.0", | ||
| path: installedPath, | ||
| manifestDependencies, | ||
| manifestOptionalDependencies, | ||
| } | ||
| this.dependencyCacheByPath.set(installedPath, placeholder) | ||
|
|
||
| const childMaps = await this.resolveChildren(installedPath, { manifestDependencies, manifestOptionalDependencies }) | ||
|
|
||
| const dependency: BunDependency = { | ||
| name: packageName, | ||
| version: manifest.version ?? "0.0.0", | ||
| path: installedPath, | ||
| manifestDependencies, | ||
| manifestOptionalDependencies, | ||
| dependencies: Object.keys(childMaps.dependencies ?? {}).length > 0 ? childMaps.dependencies : undefined, | ||
| optionalDependencies: Object.keys(childMaps.optionalDependencies ?? {}).length > 0 ? childMaps.optionalDependencies : undefined, | ||
| } | ||
|
|
||
| this.dependencyCacheByPath.set(installedPath, dependency) | ||
| return dependency | ||
| } | ||
|
|
||
| private findInstalledDependency(basedir: string, dependencyName: string): string | null { | ||
| try { | ||
| // This is necessary to create a require function that is from the perspective of the basedir | ||
| // | ||
| // It must be an absolute path | ||
| const requireStartingFile = path.join(path.resolve(basedir), "__fake_starting_file__.js") | ||
|
|
||
| const localizedRequire = createRequire(requireStartingFile) | ||
|
|
||
| const packageJsonPath = localizedRequire.resolve(path.join(dependencyName, "package.json")) | ||
|
|
||
| return path.dirname(packageJsonPath) | ||
| } catch (e: any) { | ||
| if (e?.code === "MODULE_NOT_FOUND") { | ||
| return null | ||
| } | ||
| throw e | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ import { AppFileWalker } from "./AppFileWalker" | |
| import { NodeModuleCopyHelper } from "./NodeModuleCopyHelper" | ||
| import { NodeModuleInfo } from "./packageDependencies" | ||
| import { getNodeModules, detectPackageManager } from "../node-module-collector" | ||
| import { getProjectRootPath } from "../electron/search-module" | ||
|
|
||
| const BOWER_COMPONENTS_PATTERN = `${path.sep}bower_components${path.sep}` | ||
| /** @internal */ | ||
|
|
@@ -176,12 +177,42 @@ function validateFileSet(fileSet: ResolvedFileSet): ResolvedFileSet { | |
| return fileSet | ||
| } | ||
|
|
||
| async function getGitRootDir(baseDir: string): Promise<string | null> { | ||
| try { | ||
| const { promisify } = require("util") | ||
| const { exec } = require("child_process") | ||
| const execAsync = promisify(exec) | ||
|
|
||
| const { stdout } = await execAsync("git rev-parse --show-toplevel", { | ||
| cwd: baseDir, | ||
| timeout: 5000, | ||
| }) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this be called numerous times? If so, we could probably use the package |
||
| return stdout.trim() | ||
| } catch (_error) { | ||
| // Git not installed, not in a git repo, or other error | ||
| return null | ||
| } | ||
| } | ||
|
|
||
| /** @internal */ | ||
| export async function computeNodeModuleFileSets(platformPackager: PlatformPackager<any>, mainMatcher: FileMatcher): Promise<Array<ResolvedFileSet>> { | ||
| const projectDir = platformPackager.info.projectDir | ||
| const appDir = platformPackager.info.appDir | ||
|
|
||
| const pm = detectPackageManager(appDir === projectDir ? [appDir] : [appDir, projectDir]) | ||
| const lockfileRootPath = await getProjectRootPath(appDir) | ||
| const gitRootDir = await getGitRootDir(appDir) | ||
|
|
||
| const dirsToCheck = appDir === projectDir ? [appDir] : [appDir, projectDir] | ||
|
|
||
| if (lockfileRootPath && !dirsToCheck.includes(lockfileRootPath)) { | ||
| dirsToCheck.push(lockfileRootPath) | ||
| } | ||
|
|
||
| if (gitRootDir && !dirsToCheck.includes(gitRootDir)) { | ||
| dirsToCheck.push(gitRootDir) | ||
| } | ||
|
|
||
| const pm = detectPackageManager(dirsToCheck) | ||
|
|
||
| let deps = await getNodeModules(pm, appDir, platformPackager.info.tempDirManager) | ||
| if (projectDir !== appDir && deps.length === 0) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we revert this change please? Currently trying to freeze/separate dependency version changes to exclusively their own PRs unless otherwise required.
This should allow us to revert all changes to the
pnpm-lock.yaml