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

workingDir string 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.
packageManager auto | npm | pnpm | yarn | bun default auto
Which package manager to use. auto picks the one whose lockfile is committed.
install boolean default true
Install dependencies first. Leave on: a job gets a fresh workspace, so there is nothing in node_modules to reuse.
frozenLockfile boolean default true
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 string default build
The npm script that produces the production bundle.
outputDir string default dist
Directory the build writes to, relative to workingDir. Vite and CRA use dist and build; Next static export uses out.
env object
Build-time environment variables (VITE_*, NEXT_PUBLIC_*). These are compiled INTO the bundle, so they are public - never put a secret here.
mustContain array
Strings that must appear in the built JavaScript. Use this to prove the bundle points at the right API.
mustNotContain array
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 string 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