Skip to content
Merged
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/warm-toys-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-solana-dapp': minor
---

add bun support
2 changes: 1 addition & 1 deletion src/utils/create-app-task-install-dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function createAppTaskInstallDependencies(args: GetArgsResult): Task {
if (args.verbose) {
log.warn(`Installing via ${pm}`)
}
const deleteLockFiles = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock']
const deleteLockFiles = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock']
// We don't want to delete the lock file for the current package manager
.filter((item) => item !== lockFile)
// We only want to delete the lock file if it exists
Expand Down
2 changes: 1 addition & 1 deletion src/utils/final-note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function finalNote(args: FinalNoteArgs): string {
`That's it!`,
`Change to your new directory and start developing:`,
msg(`cd ${args.target}`),
...(args.skipInstall ? [`Install dependencies:`, cmd(args.packageManager, 'install')] : []),
...(args.skipInstall ? [`Install dependencies:`, msg(`${args.packageManager} install`)] : []),
...(startScript ? [`Start the app:`, cmd(args.packageManager, startScript)] : []),
...args.instructions.map((line) => (line.startsWith('+') ? msg(line.slice(1)) : line)),
]
Expand Down
13 changes: 9 additions & 4 deletions src/utils/get-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export async function getArgs(argv: string[], app: AppInfo, pm: PackageManager =
.option('--pm, --package-manager <package-manager>', help(`Package manager to use (default: npm)`))
.option('--yarn', help(`Use yarn as the package manager`), false)
.option('--pnpm', help(`Use pnpm as the package manager`), false)
.option('--bun', help(`Use bun as the package manager`), false)
.option('-d, --dry-run', help('Dry run (default: false)'))
.option('-t, --template <template-name>', help('Use a template'))
.option('--list-templates', help('List available templates'))
Expand Down Expand Up @@ -55,17 +56,21 @@ Examples:
}
let packageManager = result.packageManager ?? pm

// The 'yarn' and 'pnpm' options are mutually exclusive, and will override the 'packageManager' option
if (result.pnpm && result.yarn) {
log.error(`Both pnpm and yarn were specified. Please specify only one.`)
throw new Error(`Both pnpm and yarn were specified. Please specify only one.`)
// The 'yarn', 'pnpm' and 'bun' options are mutually exclusive, and will override the 'packageManager' option
const managers = [result.pnpm && 'pnpm', result.yarn && 'yarn', result.bun && 'bun'].filter(Boolean)
if (managers.length > 1) {
log.error(`Multiple package managers were specified: ${managers.join(', ')}. Please specify only one.`)
throw new Error(`Multiple package managers were specified: ${managers.join(', ')}. Please specify only one.`)
}
if (result.pnpm) {
packageManager = 'pnpm'
}
if (result.yarn) {
packageManager = 'yarn'
}
if (result.bun) {
packageManager = 'bun'
}

// Display the intro
intro(`${app.name} ${app.version}`)
Expand Down
38 changes: 26 additions & 12 deletions src/utils/vendor/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,28 @@
*/
import { execSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { join } from 'node:path'
import { join, sep } from 'node:path'

export const packageManagers = ['pnpm', 'npm', 'yarn'] as const
export const packageManagers = ['pnpm', 'npm', 'yarn', 'bun'] as const
export type PackageManager = (typeof packageManagers)[number]

export function detectInvokedPackageManager(): PackageManager {
let detectedPackageManager: PackageManager = 'npm'
const invoker = require.main

if (!invoker) {
return detectedPackageManager
if (process.env.npm_config_user_agent) {
for (const pm of packageManagers) {
if (process.env.npm_config_user_agent.startsWith(`${pm}/`)) {
return pm
}
}
}

for (const pkgManager of packageManagers) {
if (invoker.path.includes(pkgManager)) {
detectedPackageManager = pkgManager
break
if (process.env.npm_execpath) {
for (const pm of packageManagers) {
if (process.env.npm_execpath.split(sep).includes(pm)) {
return pm
}
}
}
return detectedPackageManager
return 'npm'
}
Comment on lines 19 to 36
Copy link
Contributor Author

@tobeycodes tobeycodes Jun 13, 2025

Choose a reason for hiding this comment

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

@beeman i am not sure if this function needs to change or not but this is what nx is using now https://github.com/nrwl/nx/blob/master/packages/create-nx-workspace/src/utils/package-manager.ts

is there a way to test bun create with a local build of solana-create-app? i get the error warn: bun create [local file] only supports .jsx and .tsx files currently

currently this code is working for bunx but otherwise untested

Copy link
Collaborator

@beeman beeman Jun 13, 2025

Choose a reason for hiding this comment

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

I'll take a look at the differences between those.

This is what I'm getting with bun create
image
image

Copy link
Contributor Author

@tobeycodes tobeycodes Jun 13, 2025

Choose a reason for hiding this comment

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

it would appear that the old way with require.main has some weird cases oven-sh/bun#8113

For example if I do bun exec ~/Developer/create-solana-dapp/dist/bin/index.cjs then process.env.verisons.bun is undefined but bun ~/Developer/create-solana-dapp/dist/bin/index.cjs (without the exec) has it

Also, if you currently try bun on the current version it thinks it'syarn

> $ bun create solana-dapp --verbose                                                                                                                                                     
  packageManager: 'yarn',

i think the changes i made to the function should work for bun create though oven-sh/bun#11016

Copy link
Collaborator

Choose a reason for hiding this comment

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

I just did some local testing and managed to get a working project with bun.

I reckon we can merge it and see how it works in the wild and fix any issues

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That sounds good to me


/**
Expand Down Expand Up @@ -99,6 +101,15 @@ export function getPackageManagerCommand(
lockFile: 'package-lock.json',
}
}

case 'bun': {
return {
install: `bun install ${silent}`,
exec: 'bun',
globalAdd: 'bun add -g',
lockFile: 'bun.lock',
}
}
}
}
const pmVersionCache = new Map<PackageManager, string>()
Expand All @@ -123,6 +134,9 @@ export function detectPackageManager(dir: string = ''): PackageManager {
if (existsSync(join(dir, 'pnpm-lock.yaml'))) {
return 'pnpm'
}
if (existsSync(join(dir, 'bun.lock'))) {
return 'bun'
}

return 'npm'
}
6 changes: 3 additions & 3 deletions test/final-note.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('finalNote', () => {
const result = finalNote({ ...baseArgs, skipInstall: true })

expect(result).toContain('Install dependencies:')
expect(result).toContain('npm run install')
expect(result).toContain('npm install')
})

it('should not include install instructions when skipInstall is false', () => {
Expand All @@ -44,7 +44,7 @@ describe('finalNote', () => {
const result = finalNote({ ...baseArgs, skipInstall: false })

expect(result).not.toContain('Install dependencies:')
expect(result).not.toContain('npm run install')
expect(result).not.toContain('npm install')
})

it('should use yarn when packageManager is yarn and skipInstall is true', () => {
Expand All @@ -58,6 +58,6 @@ describe('finalNote', () => {
packageManager: 'yarn' as FinalNoteArgs['packageManager'],
})

expect(result).toContain('yarn run install')
expect(result).toContain('yarn install')
})
})