Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
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/[email protected]
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
1 change: 1 addition & 0 deletions PizzaBunApp
Submodule PizzaBunApp added at a178db
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
![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!

Expand Down
Binary file added boilerplate/bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
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
38 changes: 36 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 @@ -209,9 +242,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
7 changes: 6 additions & 1 deletion test/vanilla/ignite-remove-demo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ 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 + "/.maestro", TEMP_DIR + "/.maestro", { overwrite: true })
filesystem.copy(BOILERPLATE_PATH + "/assets", TEMP_DIR + "/assets", { overwrite: true })
})

afterEach(() => {
Expand Down
Loading