React Build
react-build@1.0.0 Template Install and build a React web app for production, with env vars baked in and the bundle checked before it ships.
Start from it
flightstack template use react-build What it does
Install dependencies and build a React web app (Vite, Next or CRA) for production in a single job, with build-time environment variables injected and the resulting bundle asserted before anything downstream can use it.
Supports npm, pnpm, yarn and bun, picking the one whose lockfile is committed, and installs lockfile-faithfully by default.
Inputs
-
workingDirstring default. - Directory holding package.json. For a workspace monorepo this is the REPO ROOT, not the app - the root install is what links the workspaces together.
-
packageManagerauto | npm | pnpm | yarn | bun defaultauto - Which package manager to use.
autopicks the one whose lockfile is committed. -
installboolean defaulttrue - Install dependencies first. Leave on: a job gets a fresh workspace, so there is nothing in node_modules to reuse.
-
frozenLockfileboolean defaulttrue - Install exactly what the lockfile says and fail if it is out of date (
npm ci). Turn off only for a repo that does not commit a lockfile. -
buildScriptstring defaultbuild - The npm script that produces the production bundle.
-
outputDirstring defaultdist - Directory the build writes to, relative to workingDir. Vite and CRA use
distandbuild; Next static export usesout. -
envobject - Build-time environment variables (VITE_*, NEXT_PUBLIC_*). These are compiled INTO the bundle, so they are public - never put a secret here.
-
mustContainarray - Strings that must appear in the built JavaScript. Use this to prove the bundle points at the right API.
-
mustNotContainarray - Strings that must NOT appear in the built JavaScript, e.g. "localhost:". A production bundle that resolves to a local address builds cleanly and fails only in someone's browser.
-
includestring default*.js - Filename pattern the assertions are checked against.
Definition
name: React Build
description: >-
Install dependencies and build a React web app for production, baking in
build-time environment variables and asserting the bundle is the one you
meant to ship.
triggers:
- type: manual
jobs:
build:
name: Build console
type: plugin/react-build
with:
workingDir: .
outputDir: apps/console/dist
env:
VITE_API_URL: https://api.postwire.ai
mustContain:
- api.postwire.ai
mustNotContain:
- "localhost:"
- 127.0.0.1
plugin:
name: react-build
displayName: React Build
description: >-
Install dependencies and build a React web app (Vite, Next or CRA) for
production, with build-time env vars injected and the resulting bundle
checked before anything downstream can use it.
category: build
requiredTools:
- name: nodejs
version: ">=20.0.0"
supportedPlatforms:
- linux
- macos
- windows
parameters:
type: object
properties:
workingDir:
type: string
default: "."
description: >-
Directory holding package.json. For a workspace monorepo this is the
REPO ROOT, not the app - the root install is what links the
workspaces together.
packageManager:
type: string
enum: [auto, npm, pnpm, yarn, bun]
default: auto
description: >-
Which package manager to use. `auto` picks the one whose lockfile is
committed.
install:
type: boolean
default: true
description: >-
Install dependencies first. Leave on: a job gets a fresh workspace,
so there is nothing in node_modules to reuse.
frozenLockfile:
type: boolean
default: true
description: >-
Install exactly what the lockfile says and fail if it is out of date
(`npm ci`). Turn off only for a repo that does not commit a lockfile.
buildScript:
type: string
default: build
description: The npm script that produces the production bundle.
outputDir:
type: string
default: dist
description: >-
Directory the build writes to, relative to workingDir. Vite and CRA
use `dist` and `build`; Next static export uses `out`.
env:
type: object
description: >-
Build-time environment variables (VITE_*, NEXT_PUBLIC_*). These are
compiled INTO the bundle, so they are public - never put a secret
here.
mustContain:
type: array
description: >-
Strings that must appear in the built JavaScript. Use this to prove
the bundle points at the right API.
mustNotContain:
type: array
description: >-
Strings that must NOT appear in the built JavaScript, e.g.
"localhost:". A production bundle that resolves to a local address
builds cleanly and fails only in someone's browser.
include:
type: string
default: "*.js"
description: Filename pattern the assertions are checked against.
script: |
set -euo pipefail
cd "{{workingDir}}"
pm="{{packageManager}}"
if [ "$pm" = "auto" ]; then
if [ -f pnpm-lock.yaml ]; then pm=pnpm
elif [ -f yarn.lock ]; then pm=yarn
elif [ -f bun.lockb ] || [ -f bun.lock ]; then pm=bun
else pm=npm
fi
fi
echo "package manager: $pm"
# pnpm and yarn ship with Node but only once corepack is switched on. A
# failure here is not fatal: the tool may already be on PATH.
case "$pm" in
pnpm|yarn) corepack enable >/dev/null 2>&1 || true ;;
esac
{{#install}}
echo "--- installing dependencies"
case "$pm" in
npm)
if [ "{{frozenLockfile}}" = "true" ] && [ -f package-lock.json ]; then
npm ci
else
npm install
fi
;;
pnpm)
if [ "{{frozenLockfile}}" = "true" ]; then pnpm install --frozen-lockfile; else pnpm install; fi
;;
yarn)
# --immutable is Yarn 2+; --frozen-lockfile is the classic spelling.
if [ "{{frozenLockfile}}" = "true" ]; then
yarn install --immutable || yarn install --frozen-lockfile
else
yarn install
fi
;;
bun)
if [ "{{frozenLockfile}}" = "true" ]; then bun install --frozen-lockfile; else bun install; fi
;;
esac
{{/install}}
echo "--- building"
{{#each env}}
export {{@key}}="{{this}}"
{{/each}}
"$pm" run "{{buildScript}}"
out="{{outputDir}}"
if [ ! -d "$out" ]; then
echo "build finished but '$out' does not exist - check outputDir" >&2
exit 1
fi
# Assertions run against what was actually produced, not against the
# config that was supposed to produce it.
failed=0
{{#each mustContain}}
if grep -rqF --include="{{include}}" -- '{{this}}' "$out"; then
echo "ok: bundle contains '{{this}}'"
else
echo "FAIL: '{{this}}' is missing from the bundle" >&2
failed=1
fi
{{/each}}
{{#each mustNotContain}}
if grep -rqF --include="{{include}}" -- '{{this}}' "$out"; then
echo "FAIL: '{{this}}' appears in the bundle and must not" >&2
failed=1
else
echo "ok: bundle is free of '{{this}}'"
fi
{{/each}}
[ "$failed" = "0" ] || exit 1
echo "--- built $(cd "$out" && pwd)"
du -sh "$out" 2>/dev/null || true