Skip to content

Commit

Permalink
feat(cli): add bun support (#2497 by @frankcalise @jamonholmgren)
Browse files Browse the repository at this point in the history
* chore(release): 8.8.7 [skip ci]

## [8.8.7](v8.8.6...v8.8.7) (2023-08-08)

### Bug Fixes

* **cli:** correct cd command when targetPath has changed ([#2488](#2488) by bradherman) ([19a161e](19a161e))

* feat(cli): add bun support

* fix: Specifies version of @expo/webpack-config to avoid dependency failure. (#2502 by @jamonholmgren)

* chore(release): 8.8.8 [skip ci]

## [8.8.8](v8.8.7...v8.8.8) (2023-09-19)

### Bug Fixes

* Specifies version of @expo/webpack-config to avoid dependency failure. ([#2502](#2502) by [@jamonholmgren](https://github.com/jamonholmgren)) ([94ddb85](94ddb85))

* docs: Update README.md -- add Getting Started video link (#2501 by @jamonholmgren)

[skip ci]

* Switch to bun for boilerplate scripts

* Use bun for CI tests

* Bump for ci

* remove demo app

* Bump bun-orb version

* bump bun-orb

* fix: removed ios remnants

merge from main probably

* fix(cli): display bun run command

---------

Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
Co-authored-by: Jamon Holmgren <jamonholmgren@gmail.com>
Co-authored-by: Jamon Holmgren <code@jamon.dev>
  • Loading branch information
4 people authored Oct 1, 2023
1 parent eecac88 commit 7d78fc6
Show file tree
Hide file tree
Showing 14 changed files with 2,940 additions and 3,199 deletions.
6 changes: 5 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@
defaults: &defaults
docker:
# Choose the version of Node you want here
- image: cimg/node:18.15.0
- image: cimg/node:18.17.1
working_directory: /mnt/ramdisk/repo

version: 2.1
orbs:
# https://circleci.com/developer/orbs/orb/cmgriffing/bun-orb
bun-orb: cmgriffing/bun-orb@0.0.26
jobs:
tests:
<<: *defaults
resource_class: large
steps:
- checkout
- bun-orb/setup
- restore_cache:
name: Restore node modules
keys:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ coverage
# newly spun-up apps, but we do want to ignore it in
# Ignite's source repo.
boilerplate/yarn.lock
boilerplate/bun.lockb
boilerplate/.gitignore.template

# flame CLI
Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@
![Twitter Follow](https://img.shields.io/twitter/follow/ir_ignite)
[![CircleCI](https://dl.circleci.com/status-badge/img/gh/infinitered/ignite/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/infinitered/ignite/tree/master)

## Battle-tested React Native boilerplate
## Proven React Native boilerplate

The culmination of over six years of constant React Native development, Ignite is the most popular React Native app boilerplate for both Expo and bare React Native.
The culmination of over seven years of constant React Native development, Ignite is the most popular React Native app boilerplate for both Expo and bare React Native.

This is the React Native boilerplate that the [Infinite Red](https://infinite.red) team uses on a day-to-day basis to build client apps. Developers who use Ignite report that it saves them two to four weeks of time on average off the beginning of their React Native project!

## [Full Documentation](https://github.com/infinitered/ignite/blob/master/docs)

We've put great effort into the documentation as a team, please [read through it here](https://github.com/infinitered/ignite/blob/master/docs). If you're unsure why a certain decision was made related to this boilerplate or how to proceed with a particular feature, it's likely documented. If it still isn't clear, go through the proper [help channels](#reporting-bugs--getting-help) and we always welcome PRs to improve the docs!

## Intro Video

Check out the [Getting Started with Ignite](https://www.youtube.com/watch?v=KOSvDlFyg20) video for a 13 minute overview!

## Tech Stack

Nothing makes it into Ignite unless it's been proven on projects that Infinite Red works on. Ignite apps include the following rock-solid technical decisions out of the box:
Expand Down Expand Up @@ -103,14 +107,14 @@ If you need battle-tested solutions from Infinite Red experts on everything from

## No time to learn React Native? Hire Infinite Red for your next project

We get it – sometimes there just isn’t enough time on a project to learn the ins and outs of a new framework. Infinite Red’s here to help! Our experienced team of React Native engineers have worked with companies like GasBuddy, Zoom, and Mercari to bring even some of the most complex projects to life.
We get it – sometimes there just isn’t enough time on a project to learn the ins and outs of a new framework. Infinite Red’s here to help! Our experienced team of React Native engineers have worked with companies like Microsoft, GasBuddy, Zoom, and Mercari to bring some of the most complex React Native projects to life.

Whether it’s running a full project or training a team on React Native, we can help you solve your company’s toughest engineering challenges – and make it a great experience at the same time.

Ready to see how we can work together? [Send us a message](mailto:hello@infinite.red)
Ready to see how we can work together? [Send us a message](https://infinite.red/contact)

## Further Reading

- Watch Jamon Holmgren's talk at React Live Amsterdam where he uses Ignite to build an app in less than 30 minutes: [https://www.youtube.com/watch?v=OgiFKMd_TeY](https://www.youtube.com/watch?v=OgiFKMd_TeY)
- Watch Jamon Holmgren's talk at React Live Amsterdam 2019 where he uses Ignite to build an app in less than 30 minutes: [https://www.youtube.com/watch?v=OgiFKMd_TeY](https://www.youtube.com/watch?v=OgiFKMd_TeY)
- Prior art includes [Ignite Andross](https://github.com/infinitered/ignite-andross) and [Ignite Bowser](https://github.com/infinitered/ignite-bowser) (which is very similar to the current version of Ignite).
- [Who are We?](https://infinite.red) - Learn More About Infinite Red
Binary file added boilerplate/bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions boilerplate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"dependencies": {
"@expo-google-fonts/space-grotesk": "^0.2.2",
"@react-native-async-storage/async-storage": "1.18.2",
"@expo/metro-config": "^0.7.1",
"@expo/webpack-config": "18.1.2",
"@react-navigation/bottom-tabs": "^6.3.2",
"@react-navigation/native": "^6.0.2",
"@react-navigation/native-stack": "^6.0.2",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ignite-cli",
"version": "8.8.6",
"version": "8.8.8",
"description": "Infinite Red's hottest boilerplate for React Native.",
"bin": {
"ignite": "bin/ignite",
Expand Down Expand Up @@ -32,7 +32,7 @@
"format:write": "yarn format --write",
"format:check": "yarn format --check",
"lint": "eslint 'src/**' 'test/**'",
"test": "jest",
"test": "TS_JEST_DISABLE_VER_CHECKER=true jest",
"watch": "jest --watch",
"watch:debug": "yarn watch --runInBand --verbose",
"coverage": "jest --coverage",
Expand Down
15 changes: 10 additions & 5 deletions src/commands/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export interface Options {
*
* Input Source: `prompt.ask`| `parameter.option`
*/
packager?: "npm" | "yarn" | "pnpm"
packager?: "npm" | "yarn" | "pnpm" | "bun"
/**
* The target directory where the project will be created.
*
Expand Down Expand Up @@ -133,7 +133,7 @@ export interface Options {
workflow?: Workflow
}

export default {
module.exports = {
run: async (toolbox: GluegunToolbox) => {
// #region Toolbox
const { print, filesystem, system, meta, parameters, strings, prompt } = toolbox
Expand Down Expand Up @@ -335,15 +335,20 @@ export default {
// we pass in expo because we can't use pnpm if we're using expo

const availablePackagers = packager.availablePackagers()
log(`availablePackagers: ${availablePackagers}`)
const defaultPackagerName = availablePackagers.includes("yarn") ? "yarn" : "npm"
let packagerName = useDefault(options.packager) ? defaultPackagerName : options.packager

const validatePackagerName = (input: unknown): input is PackagerName =>
typeof input === "string" && ["npm", "yarn", "pnpm"].includes(input)
typeof input === "string" && ["npm", "yarn", "pnpm", "bun"].includes(input)

if (packagerName !== undefined && validatePackagerName(packagerName) === false) {
p()
p(yellow(`Error: Invalid packager: "${packagerName}". Valid packagers are npm, yarn, pnpm.`))
p(
yellow(
`Error: Invalid packager: "${packagerName}". Valid packagers are npm, yarn, pnpm, bun.`,
),
)
process.exit(1)
}

Expand Down Expand Up @@ -471,7 +476,7 @@ export default {
await copyBoilerplate(toolbox, {
boilerplatePath,
targetPath,
excluded: [".vscode", "node_modules", "yarn.lock"],
excluded: [".vscode", "node_modules", "yarn.lock", "bun.lockb", "package-lock.json"],
overwrite,
})
stopSpinner(" 3D-printing a new React Native app", "🖨")
Expand Down
1 change: 1 addition & 0 deletions src/tools/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const lockFile = {
yarn: "yarn.lock",
pnpm: "pnpm-lock.yaml",
npm: "package-lock.json",
bun: "bun.lockb",
} as const

const MAC: NodeJS.Platform = "darwin"
Expand Down
40 changes: 38 additions & 2 deletions src/tools/packager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { spawnProgress } from "./spawn"
// in the meantime, we'll use this hacked together version

// Expo doesn't support pnpm, so we'll use yarn or npm
export type PackagerName = "npm" | "yarn" | "pnpm"
export type PackagerName = "npm" | "yarn" | "pnpm" | "bun"
type PackageOptions = {
packagerName?: PackagerName
dev?: boolean
Expand Down Expand Up @@ -39,11 +39,20 @@ function pnpmAvailable() {
return isPnpm
}

let isBun
function bunAvailable() {
if (isBun !== undefined) return isBun
isBun = Boolean(system.which("bun"))
return isBun
}

function detectPackager(): PackagerName {
if (yarnAvailable()) {
return "yarn"
} else if (pnpmAvailable()) {
return "pnpm"
} else if (bunAvailable()) {
return "bun"
} else {
return "npm"
}
Expand All @@ -59,6 +68,10 @@ function availablePackagers(): PackagerName[] {
packagers.push("pnpm")
}

if (bunAvailable()) {
packagers.push("bun")
}

return packagers
}

Expand All @@ -80,6 +93,8 @@ function addCmd(pkg: string, options: PackageRunOptions = packageInstallOptions)
cmd = `yarn add`
} else if (options.packagerName === "npm") {
cmd = `npm install`
} else if (options.packagerName === "bun") {
cmd = `bun add`
} else {
// neither expo nor a packagerName was provided, so let's detect one
return addCmd(pkg, { ...options, packagerName: detectPackager() })
Expand All @@ -106,6 +121,8 @@ function removeCmd(pkg: string, options: PackageOptions = packageInstallOptions)
cmd = `yarn remove`
} else if (options.packagerName === "npm") {
cmd = `npm uninstall`
} else if (options.packagerName === "bun") {
cmd = `bun remove`
} else {
// neither expo nor a packagerName was provided, so let's detect one
return removeCmd(pkg, { ...options, packagerName: detectPackager() })
Expand All @@ -130,6 +147,8 @@ function installCmd(options: PackageRunOptions) {
return `yarn install${silent}`
} else if (options.packagerName === "npm") {
return `npm install${silent}`
} else if (options.packagerName === "bun") {
return `bun install`
} else {
return installCmd({ ...options, packagerName: detectPackager() })
}
Expand All @@ -140,6 +159,20 @@ export function list(options: PackageOptions = packageListOptions): PackageListO
if (options.packagerName === "pnpm") {
// TODO: pnpm list?
throw new Error("pnpm list is not supported yet")
} else if (options.packagerName === "bun") {
return [
// TODO do we need to add --global here?
`bun pm ls`,
(output: string): [string, string][] => {
// Parse yarn's human-readable output
return output
.split("\n")
.reduce((acc: [string, string][], line: string): [string, string][] => {
const match = line.match(/info "([^@]+)@([^"]+)" has binaries/)
return match ? [...acc, [match[1], match[2]]] : acc
}, [])
},
]
} else if (
options.packagerName === "yarn" ||
(options.packagerName === undefined && yarnAvailable())
Expand Down Expand Up @@ -181,6 +214,8 @@ function runCmd(command: string, options: PackageOptions) {
return `pnpm run ${command}${silent}`
} else if (options.packagerName === "yarn") {
return `yarn ${command}${silent}`
} else if (options.packagerName === "bun") {
return `bun run ${command}`
} else {
// defaults to npm run
return `npm run ${command}${silent}`
Expand Down Expand Up @@ -209,9 +244,10 @@ export const packager = {
const [cmd, parseFn] = list(options)
return parseFn(await spawnProgress(cmd, {}))
},
has: (packageManager: "yarn" | "npm" | "pnpm"): boolean => {
has: (packageManager: "yarn" | "npm" | "pnpm" | "bun"): boolean => {
if (packageManager === "yarn") return yarnAvailable()
if (packageManager === "pnpm") return pnpmAvailable()
if (packageManager === "bun") return bunAvailable()
return true
},
detectPackager,
Expand Down
1 change: 1 addition & 0 deletions src/tools/pretty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export const pkgColor = (packagerName: PackagerName) => {
npm: "red",
yarn: "blue",
pnpm: "yellow",
bun: "cyan",
}
return print.colors[packagerColors[packagerName]] as (text: string) => string
}
Expand Down
6 changes: 5 additions & 1 deletion test/vanilla/ignite-generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ const setup = (): { TEMP_DIR: string } => {
const TEMP_DIR = tempy.directory({ prefix: "ignite-" })

beforeEach(() => {
filesystem.copy(BOILERPLATE_PATH, TEMP_DIR, { overwrite: true })
// create the destination directory
filesystem.dir(TEMP_DIR)
// copy the relevant folders
filesystem.copy(BOILERPLATE_PATH + "/app", TEMP_DIR + "/app", { overwrite: true })
filesystem.copy(BOILERPLATE_PATH + "/ignite", TEMP_DIR + "/ignite", { overwrite: true })
})

afterEach(() => {
Expand Down
40 changes: 24 additions & 16 deletions test/vanilla/ignite-new.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,19 @@ describe("ignite new", () => {
})
})

describe(`ignite new ${APP_NAME} --debug --packager=npm --yes --use-cache`, () => {
describe(`ignite new ${APP_NAME} --debug --packager=bun --yes`, () => {
let tempDir: string
let result: string
let appPath: string

beforeAll(async () => {
tempDir = tempy.directory({ prefix: "ignite-" })
result = await runIgnite(`new ${APP_NAME} --debug --packager=npm --yes --use-cache`, {

result = await runIgnite(`new ${APP_NAME} --debug --packager=bun --yes`, {
pre: `cd ${tempDir}`,
post: `cd ${originalDir}`,
})

appPath = filesystem.path(tempDir, APP_NAME)
})

Expand All @@ -53,6 +55,7 @@ describe("ignite new", () => {
expect(dirs).not.toContain("ios")
expect(dirs).not.toContain("android")
expect(dirs).toContain("app")
expect(dirs).toContain("bun.lockb")

// check the contents of ignite/templates
const templates = filesystem.list(`${appPath}/ignite/templates`)
Expand Down Expand Up @@ -82,7 +85,7 @@ describe("ignite new", () => {
expect(appJS).toContain("RootStore")
})

it("should be able to use `generate` command and have pass output pass npm run test, npm run lint, and npm run compile scripts", async () => {
it("should be able to use `generate` command and have pass output pass bun run test, bun run lint, and bun run compile scripts", async () => {
// other common test operations
const runOpts = {
pre: `cd ${appPath}`,
Expand All @@ -92,7 +95,7 @@ describe("ignite new", () => {
// #region Assert Typescript Compiles With No Errors
let resultTS: string
try {
resultTS = await run(`npm run compile`, runOpts)
resultTS = await run(`bun run compile`, runOpts)
} catch (e) {
resultTS = e.stdout
console.error(resultTS) // This will only show if you run in --verbose mode.
Expand Down Expand Up @@ -247,30 +250,27 @@ describe("ignite new", () => {

// #region Assert package.json Scripts Can Be Run
// run the tests; if they fail, run will raise and this test will fail
await run(`npm run test`, runOpts)
await run(`npm run lint`, runOpts)
await run(`npm run compile`, runOpts)
await run(`bun run test`, runOpts)
await run(`bun run lint`, runOpts)
await run(`bun run compile`, runOpts)
expect(await run("git diff HEAD", runOpts)).toContain("+ Bowser: undefined")
// #endregion

// we're done!
})
})

describe(`ignite new ${APP_NAME} --debug --packager=npm --workflow=prebuild --yes --use-cache`, () => {
describe(`ignite new ${APP_NAME} --debug --packager=bun --workflow=prebuild --yes`, () => {
let tempDir: string
let result: string
let appPath: string

beforeAll(async () => {
tempDir = tempy.directory({ prefix: "ignite-" })
result = await runIgnite(
`new ${APP_NAME} --debug --packager=npm --workflow=prebuild --yes --use-cache`,
{
pre: `cd ${tempDir}`,
post: `cd ${originalDir}`,
},
)
result = await runIgnite(`new ${APP_NAME} --debug --packager=bun --workflow=prebuild --yes`, {
pre: `cd ${tempDir}`,
post: `cd ${originalDir}`,
})
appPath = filesystem.path(tempDir, APP_NAME)
})

Expand Down Expand Up @@ -320,7 +320,15 @@ describe("ignite new", () => {
})

async function checkForLeftoverHelloWorld(filePath: string) {
const ignoreFolders = ["/xcuserdata", ".git", "node_modules", "Pods", "/build", ".expo"]
const ignoreFolders = [
"/xcuserdata",
"bun.lockb",
".git",
"node_modules",
"Pods",
"/build",
".expo",
]
// ignore some folders
if (!ignoreFolders.every((f) => !filePath.includes(f))) return

Expand Down
Loading

0 comments on commit 7d78fc6

Please sign in to comment.