Boolean Build

Two build gates that make RN quality non-negotiable

2026-10-14

React Native apps don't usually fail loudly. They rot quietly. A refactor leaves a screen that return null and ships a blank page. A "temporary" stub passes lint, type-check, and unit tests because nothing renders it. A business rule everyone agreed on — "the wallet changes when you spend" — is never actually asserted, so it silently breaks three releases later.

Tests don't catch these because tests check logic, and these are integration truths. So I made them impossible to skip, with two build gates that fail npm run check and CI.

Gate 1 — a screens gate: a screen must actually be a screen

A stub screen (returns null, or renders nothing) is invisible to logic tests. This gate walks src/ui/screens and asserts every file is a real, default-exported component that renders markup:

// src/__tests__/screens.gate.test.tsx
import fs from 'fs';
import path from 'path';

const SCREENS_DIR = path.join(__dirname, '..', 'ui', 'screens');
const MIN_LINES = 20;
const files = fs.existsSync(SCREENS_DIR)
  ? fs.readdirSync(SCREENS_DIR).filter((f) => f.endsWith('Screen.tsx'))
  : [];

describe('every screen is a real implementation (not a stub)', () => {
  it('has screen files', () => expect(files.length).toBeGreaterThan(0));

  files.forEach((file) => {
    it(`${file} is implemented`, () => {
      const src = fs.readFileSync(path.join(SCREENS_DIR, file), 'utf8');
      expect(src.split('\n').length).toBeGreaterThan(MIN_LINES);
      expect(src).not.toMatch(/return\s+null\s*;/);
      expect(src).toMatch(/export\s+default/);
      const mod = require(path.join(SCREENS_DIR, file));
      expect(typeof mod.default).toBe('function');
    });
  });
});

It's ~25 lines, and it has caught a black-screen launch more than once.

Gate 2 — an acceptance-contract gate: no unproven invariants

Instead of hoping people remember to test the important things, you declare them as a machine-checkable contract (contract/acceptance_spec.json):

{
  "invariants": [
    { "id": "INV-PERSIST-1", "assertion": "settings round-trip through storage unchanged" },
    { "id": "INV-STATE-1",   "assertion": "changing a setting leaves unrelated fields untouched" }
  ],
  "journeys": [
    { "id": "J-SETTINGS-1", "name": "settings persist, reload, and reset" }
  ]
}

Then a gate test fails the build unless every id is referenced by at least one test — and it rejects fake coverage: tests that only assert on literals they defined themselves don't count, and contract-tagged test files must import real code.

The point

Both gates are cheap and boring — which is exactly why they work. They turn quality from a habit into a property of the build. npm run check (strict TS + ESLint + Jest) stays green because it has to.

The free baseline is on GitHub.

Typed navigation, deep links, theme tokens, MMKV persistence, native modules mocked for tests.

Get the repo

The gates above, plus CI and local iOS/Android release scripts, are packaged in the Expo Production Starter.