Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/long-keys-smile.md
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
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"@types/node": "^22.7.4",
"@typescript-eslint/eslint-plugin": "8.17.0",
"@typescript-eslint/parser": "8.17.0",
"@vitest/ui": "3.0.4",
"@vitest/ui": "^3.2.2",
Copy link
Collaborator

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

"chalk": "^4.1.2",
"conventional-changelog-cli": "5.0.0",
"depcheck": "1.4.3",
Expand All @@ -80,6 +80,12 @@
"patchedDependencies": {
"@changesets/cli@2.29.7": "patches/@changesets__cli@2.29.7.patch",
"@changesets/assemble-release-plan@6.0.9": "patches/@changesets__assemble-release-plan@6.0.9.patch"
}
},
"onlyBuiltDependencies": [
"@parcel/watcher",
"electron",
"electron-winstaller",
"esbuild"
]
}
}
1 change: 1 addition & 0 deletions packages/app-builder-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Collaborator

Choose a reason for hiding this comment

The 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",
Expand Down
2 changes: 1 addition & 1 deletion packages/app-builder-lib/src/electron/search-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export async function searchForNodeModules(cwd: string, rootPath?: string): Prom
* @param cwd the initial directory to traverse
*/
export async function getProjectRootPath(cwd: string): Promise<string> {
for (const lockFilename of ["yarn.lock", "package-lock.json", "pnpm-lock.yaml"]) {
for (const lockFilename of ["yarn.lock", "package-lock.json", "pnpm-lock.yaml", "bun.lock", "bun.lockb"]) {
const pathGenerator: PathGeneratorFunction = traversedPath => path.join(traversedPath, lockFilename)
const lockPaths = await traverseAncestorDirectories(cwd, pathGenerator, undefined, 1)
if (lockPaths.length > 0) {
Expand Down
2 changes: 1 addition & 1 deletion packages/app-builder-lib/src/fileMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const excludedNames =
".git,.hg,.svn,CVS,RCS,SCCS," +
"__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore," +
".idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci," +
".yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,pnpm-lock.yaml," +
".yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,pnpm-lock.yaml,bun.lock,bun.lockb" +
"appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env"

export const excludedExts =
Expand Down
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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with this createRequire, but 2 quick Qs. What are we using it for here as opposed to directly require(...)?
Can we also change the import to from "node:module"?


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
}
}
}
6 changes: 4 additions & 2 deletions packages/app-builder-lib/src/node-module-collector/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BunNodeModulesCollector } from "./bunNodeModulesCollector"
import { NpmNodeModulesCollector } from "./npmNodeModulesCollector"
import { PnpmNodeModulesCollector } from "./pnpmNodeModulesCollector"
import { YarnNodeModulesCollector } from "./yarnNodeModulesCollector"
Expand All @@ -13,8 +14,9 @@ export async function getCollectorByPackageManager(pm: PM, rootDir: string, temp
}
return new PnpmNodeModulesCollector(rootDir, tempDirManager)
case PM.NPM:
case PM.BUN:
return new NpmNodeModulesCollector(rootDir, tempDirManager)
case PM.BUN:
return new BunNodeModulesCollector(rootDir, tempDirManager)
case PM.YARN:
return new YarnNodeModulesCollector(rootDir, tempDirManager)
default:
Expand Down Expand Up @@ -53,4 +55,4 @@ export function detectPackageManager(dirs: string[]): PM {
return PM.NPM
}

export { PM, getPackageManagerCommand }
export { PM, getPackageManagerCommand, BunNodeModulesCollector }
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export abstract class NodeModulesCollector<T extends Dependency<T, OptionalsType
protected productionGraph: DependencyGraph = {}

constructor(
private readonly rootDir: string,
private readonly tempDirManager: TmpDir
protected readonly rootDir: string,
protected readonly tempDirManager: TmpDir
) {}

public async getNodeModules(): Promise<NodeModuleInfo[]> {
Expand Down
7 changes: 7 additions & 0 deletions packages/app-builder-lib/src/node-module-collector/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export interface NpmDependency extends Dependency<NpmDependency, string> {
}
}

export interface BunManifest {
manifestDependencies: Record<string, string>
manifestOptionalDependencies: Record<string, string>
}

export interface BunDependency extends Dependency<BunDependency, BunDependency>, BunManifest {}

export type Dependency<T, V> = Dependencies<T, V> & ParsedDependencyTree

export type Dependencies<T, V> = {
Expand Down
33 changes: 32 additions & 1 deletion packages/app-builder-lib/src/util/appFileCopier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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,
})
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 lazy-val that is already used in this project for Lazy<string>

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) {
Expand Down
Loading
Loading