Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
e6ff5e8
draft initial CI/script setup with AI
chrispader Sep 11, 2025
a835266
fix: ts and eslint errors
chrispader Sep 11, 2025
3cfa7bd
docs: add empty line
chrispader Sep 11, 2025
c91375a
chore: simplify GH workflow
chrispader Sep 11, 2025
b6f2af5
feat: include reason for compilation skip
chrispader Sep 11, 2025
04568f8
fix: compiler failure reason extraction
chrispader Sep 11, 2025
434345a
run full check for testing
chrispader Sep 11, 2025
dc85cb3
fix: ts errors
chrispader Sep 11, 2025
2c95f19
add glob pattern matching for excluded files and use in babel config
chrispader Sep 11, 2025
9631868
chore: update scripts
chrispader Sep 11, 2025
82e6ab9
refactor: improve and re-structure tracker tool
chrispader Sep 11, 2025
c177a15
fix: re-use code from tracker in devtool
chrispader Sep 11, 2025
6f4bfd1
chore: update compliance GH workflow triggers
chrispader Sep 11, 2025
01c6d6f
refactor: move files and update script
chrispader Sep 11, 2025
3de444a
update import
chrispader Sep 11, 2025
43eb4c6
chore: print reason for check-changed
chrispader Sep 11, 2025
c6dc1ac
update file path
chrispader Sep 11, 2025
da065cb
refactor: remove outputs to files and rename
chrispader Sep 11, 2025
507a541
refactor compliance checker
chrispader Sep 11, 2025
0f2e8dd
improve compliance checker
chrispader Sep 11, 2025
474bfd2
improve logs
chrispader Sep 11, 2025
9a1deb9
run full check
chrispader Sep 11, 2025
0d34384
update docs
chrispader Sep 11, 2025
19dfb8a
feat: use CLI tool for compliance checker CLI
chrispader Sep 12, 2025
300bb5b
feat: allow multiple files in check command
chrispader Sep 12, 2025
6617f12
remove devtool
chrispader Sep 12, 2025
d3ffce5
refactor: move compliance checker tool
chrispader Sep 12, 2025
b9b5a85
chore: remove unused directories from react-compiler-config.json
chrispader Sep 12, 2025
db68821
simplify script
chrispader Sep 12, 2025
0c0432b
fix: produce unique failure results
chrispader Sep 12, 2025
9db64d7
improve output formatting
chrispader Sep 12, 2025
c5c2532
update back to check-changed workflow script
chrispader Sep 12, 2025
ff6a490
fix: improve filtering and output
chrispader Sep 12, 2025
53ec8e9
fix: improve changed files check
chrispader Sep 12, 2025
26b4906
fix: logger emoji sticking to text
chrispader Sep 12, 2025
48e7f57
Merge branch 'main' into @chrispader/react-compiler-rules-of-react-ci…
chrispader Sep 12, 2025
35159ee
Merge branch 'main' into @chrispader/react-compiler-rules-of-react-ci…
chrispader Sep 20, 2025
917431f
dummy change to trigger chefcks
chrispader Sep 20, 2025
dce2092
revert dummy change
chrispader Sep 20, 2025
36eefcb
chore: update node version in workflow
chrispader Sep 20, 2025
64f36e3
chore: setup-node action and checkout in GH workflow
chrispader Sep 20, 2025
50609d7
fix: remove invalid character
chrispader Sep 20, 2025
2e58a4e
fix: show and throw error if changed files could not be found
chrispader Sep 20, 2025
51cb808
fix: improve remote/branch detection
chrispader Sep 20, 2025
b509c73
fix: add --remote flag and improve remote handling
chrispader Sep 20, 2025
dfcbce6
fix: make script work in CI
chrispader Sep 21, 2025
a00fa51
fix: run git fetch quietly
chrispader Sep 21, 2025
49cdaa5
fix: add empty lines in log
chrispader Sep 21, 2025
a8ca87a
fix: logging
chrispader Sep 21, 2025
e28514f
feat: extract and improve react-compiler-config glob pattern handling
chrispader Sep 21, 2025
b7ec871
remove comments
chrispader Sep 21, 2025
d3e6838
remove react compiler error
chrispader Sep 21, 2025
092b94c
fix: logging empty lines
chrispader Sep 21, 2025
d73655d
fix: convert shouldReactCompilerProcessFile script to CommonJS
chrispader Sep 22, 2025
e703d6b
Merge branch 'main' into @chrispader/react-compiler-rules-of-react-ci…
chrispader Sep 23, 2025
1e62ca7
fix: improve CLI
chrispader Sep 23, 2025
7f91e2e
add .mjs to React Compiler include patterns
chrispader Sep 23, 2025
b7030b3
Revert "fix: convert shouldReactCompilerProcessFile script to CommonJS"
chrispader Sep 23, 2025
0144f0a
fix: simplify pattern matching for react-compiler
chrispader Sep 23, 2025
ae1f0ff
remove console.log
chrispader Sep 23, 2025
f2879fa
remove log
chrispader Sep 23, 2025
51eb6f9
update docs
chrispader Sep 23, 2025
9735c0a
Merge branch 'main' into @chrispader/react-compiler-rules-of-react-ci…
chrispader Sep 25, 2025
fc544d9
chore: remove `react-compiler-config.json` leftovers and only check `…
chrispader Sep 25, 2025
2ec574b
chore: remove `workflow_call` trigger and add to `preDeploy` checks
chrispader Sep 25, 2025
166095a
fix: remove redundant `npm ci` call in check
chrispader Sep 25, 2025
b18abc0
fix: annotate GH checkout action
chrispader Sep 25, 2025
ad51f79
chore: remove `IS_DESKTOP_BUILD` flag on `setupNode` action
chrispader Sep 25, 2025
5ed0d71
fix: remove unnecessary comments
chrispader Sep 25, 2025
c3c83dd
fix: Logger emoji and line alignment
chrispader Sep 25, 2025
a71fa1f
Update .gitignore
chrispader Sep 25, 2025
4763b85
refactor: simplify `runCompilerHealthcheck` function by removing `det…
chrispader Sep 25, 2025
791d201
refactor: inline cli command check
chrispader Sep 25, 2025
60db217
refactor: simplify failure summary
chrispader Sep 25, 2025
e589cf6
add dummy react compiler error change
chrispader Sep 25, 2025
266b0c0
fix: improve glob pattern matching for react-compiler-healthcheck
chrispader Sep 25, 2025
14c52c1
fix: use Set and Map and simplify logic alltogether
chrispader Sep 25, 2025
3fb620b
refactor: extract git related functions to utils/Git.ts
chrispader Sep 25, 2025
4e342fc
fix: invalid string validation
chrispader Sep 25, 2025
ba415b5
feat: use GithubUtils tool for fetching changed files
chrispader Sep 25, 2025
35432c8
temp: log changed files
chrispader Sep 25, 2025
1496faa
fix: promise/async handling
chrispader Sep 25, 2025
7f18974
fix: add GITHUB_TOKEN and CI env vars
chrispader Sep 25, 2025
c116c0d
chore: add --fetch=1 flag to git fetch
chrispader Sep 25, 2025
0ff90b0
fix: react-compiler-healthcheck source argument invalid
chrispader Sep 25, 2025
5b7e7a2
fix: remove console.log
chrispader Sep 25, 2025
5ccda6a
fix: remove react compiler error
chrispader Sep 25, 2025
2c1a3fb
fix: add back `workflow_call` to allow using in `preDeploy` workflow
chrispader Sep 25, 2025
0f1a81f
fix: add "subprocessors" word to `cspell.json`
chrispader Sep 25, 2025
083d338
Merge branch 'main' into @chrispader/react-compiler-rules-of-react-ci…
chrispader Sep 25, 2025
9ab4a8f
chore: add `secrets: inherit` to workflow call
chrispader Sep 25, 2025
136c6f0
fix: markdown lint errors
chrispader Sep 25, 2025
85684af
fix: TS errors
chrispader Sep 25, 2025
bb35f3b
Merge branch 'main' into @chrispader/react-compiler-rules-of-react-ci…
chrispader Sep 30, 2025
9edd299
fix: add compiler compliance check to dependency of preDeploy `confir…
chrispader Sep 30, 2025
c9f3183
chore: gh-actions-build diff
chrispader Sep 30, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -12528,14 +12528,26 @@ function isEmptyObject(obj) {
/***/ }),

/***/ 7037:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {

"use strict";

var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.GIT_ERRORS = void 0;
const github_1 = __nccwpck_require__(5438);
const child_process_1 = __nccwpck_require__(2081);
const util_1 = __nccwpck_require__(3837);
const GithubUtils_1 = __importDefault(__nccwpck_require__(9296));
const Logger_1 = __nccwpck_require__(8891);
const exec = (0, util_1.promisify)(child_process_1.exec);
const IS_CI = process.env.CI === 'true';
const GIT_ERRORS = {
FAILED_TO_FETCH_FROM_REMOTE: 'Failed to fetch from remote',
};
exports.GIT_ERRORS = GIT_ERRORS;
/**
* Utility class for git operations.
*/
Expand Down Expand Up @@ -12774,10 +12786,146 @@ class Git {
throw new Error(`Failed to fetch git reference ${ref}: ${error instanceof Error ? error.message : String(error)}`);
}
}
static getMainBaseCommitHash(remote) {
// Fetch the main branch from the specified remote to ensure it's available
try {
(0, child_process_1.execSync)(`git fetch ${remote} main --no-tags --depth=1 -q`, { encoding: 'utf8' });
}
catch (error) {
throw new Error(GIT_ERRORS.FAILED_TO_FETCH_FROM_REMOTE);
}
// In CI, use a simpler approach - just use the remote main branch directly
// This avoids issues with shallow clones and merge-base calculations
if (IS_CI) {
try {
const mergeBaseHash = (0, child_process_1.execSync)(`git rev-parse ${remote}/main`, { encoding: 'utf8' }).trim();
// Validate the output is a proper SHA hash
if (!mergeBaseHash || !/^[a-fA-F0-9]{40}$/.test(mergeBaseHash)) {
throw new Error(`git rev-parse returned unexpected output: ${mergeBaseHash}`);
}
return mergeBaseHash;
}
catch (error) {
(0, Logger_1.error)(`Failed to get commit hash for ${remote}/main:`, error);
throw new Error(`Could not get commit hash for ${remote}/main`);
}
}
// For local development, try to find the actual merge base
let mergeBaseHash;
try {
mergeBaseHash = (0, child_process_1.execSync)(`git merge-base ${remote}/main HEAD`, { encoding: 'utf8' }).trim();
}
catch (error) {
// If merge-base fails locally, fall back to using the remote main branch
try {
mergeBaseHash = (0, child_process_1.execSync)(`git rev-parse ${remote}/main`, { encoding: 'utf8' }).trim();
(0, Logger_1.error)(`Warning: Could not find merge base between ${remote}/main and HEAD. Using ${remote}/main as base.`);
}
catch (fallbackError) {
(0, Logger_1.error)(`Failed to find merge base with ${remote}/main:`, error);
(0, Logger_1.error)(`Fallback also failed:`, fallbackError);
throw new Error(`Could not determine merge base with ${remote}/main`);
}
}
// Validate the output is a proper SHA hash
if (!mergeBaseHash || !/^[a-fA-F0-9]{40}$/.test(mergeBaseHash)) {
throw new Error(`git merge-base returned unexpected output: ${mergeBaseHash}`);
}
return mergeBaseHash;
}
static async getChangedFiles(remote) {
if (IS_CI) {
const { data: changedFiles } = await GithubUtils_1.default.octokit.pulls.listFiles({
owner: 'Expensify',
repo: 'App',
// eslint-disable-next-line @typescript-eslint/naming-convention
pull_number: github_1.context.payload.pull_request?.number ?? 0,
});
return changedFiles.map((file) => file.filename);
}
try {
// Get files changed in the current branch/commit
const mainBaseCommitHash = this.getMainBaseCommitHash(remote);
// Get the diff output and check status
const gitDiffOutput = (0, child_process_1.execSync)(`git diff --diff-filter=AMR --name-only ${mainBaseCommitHash} HEAD`, {
encoding: 'utf8',
});
const files = gitDiffOutput.trim().split('\n');
return files;
}
catch (error) {
if (error instanceof Error && error.message === GIT_ERRORS.FAILED_TO_FETCH_FROM_REMOTE) {
throw error;
}
(0, Logger_1.error)('Could not determine changed files:', error);
throw error;
}
}
}
exports["default"] = Git;


/***/ }),

/***/ 8891:
/***/ ((__unused_webpack_module, exports) => {

"use strict";

Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.bold = exports.formatLink = exports.success = exports.error = exports.note = exports.warn = exports.info = exports.log = void 0;
const COLOR_DIM = '\x1b[2m';
const COLOR_RESET = '\x1b[0m';
const COLOR_YELLOW = '\x1b[33m';
const COLOR_RED = '\x1b[31m';
const COLOR_GREEN = '\x1b[32m';
const COLOR_BOLD = '\x1b[1m';
const EMOJIS = {
// One column emojis need to be rendered with an extra space after to align with two column emojis
INFO: '▶️ ',
// Two column emojis can be rendered as-is
SUCCESS: '✅',
WARN: '⚠️',
ERROR: '🔴',
};
const log = (...args) => {
console.debug(...args);
};
exports.log = log;
const info = (...args) => {
const lines = [EMOJIS.INFO, ...args];
log(...lines);
};
exports.info = info;
const bold = (...args) => {
const lines = [COLOR_BOLD, ...args, COLOR_RESET];
log(...lines);
};
exports.bold = bold;
const success = (...args) => {
const lines = [`${EMOJIS.SUCCESS}${COLOR_GREEN}`, ...args, COLOR_RESET];
log(...lines);
};
exports.success = success;
const warn = (...args) => {
const lines = [`${EMOJIS.WARN}${COLOR_YELLOW}`, ...args, COLOR_RESET];
log(...lines);
};
exports.warn = warn;
const note = (...args) => {
const lines = [COLOR_DIM, ...args, COLOR_RESET];
log(...lines);
};
exports.note = note;
const error = (...args) => {
const lines = [`${EMOJIS.ERROR}${COLOR_RED}`, ...args, COLOR_RESET];
log(...lines);
};
exports.error = error;
const formatLink = (name, url) => `\x1b]8;;${url}\x1b\\${name}\x1b]8;;\x1b\\`;
exports.formatLink = formatLink;


/***/ }),

/***/ 9491:
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/preDeploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ jobs:
prettier:
uses: ./.github/workflows/prettier.yml

react-compiler-compliance:
uses: ./.github/workflows/react-compiler-compliance.yml
secrets: inherit

test:
uses: ./.github/workflows/test.yml
secrets: inherit

confirmPassingBuild:
runs-on: ubuntu-latest
needs: [typecheck, lint, test]
needs: [typecheck, lint, react-compiler-compliance, test]
if: ${{ always() }}

steps:
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/react-compiler-compliance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: React Compiler Compliance Check

on:
workflow_call:
pull_request:
types: [opened, synchronize]
branches-ignore: [staging, production]
paths: ['**.tsx']

concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-react-compiler-compliance
cancel-in-progress: true
jobs:
react-compiler-compliance:
name: React Compiler Compliance
if: ${{ github.actor != 'OSBotify' }}
runs-on: ubuntu-latest

steps:
# v4
- name: Checkout
Comment thread
chrispader marked this conversation as resolved.
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608

- name: Setup Node
uses: ./.github/actions/composite/setupNode

- name: Run React Compiler Compliance Check
run: npm run react-compiler-compliance-check check-changed
env:
CI: true
GITHUB_TOKEN: ${{ github.token }}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ web-build/
# Jeykll
docs/.bundle

# Output of react compiler healthcheck dev script
# React Compiler Compliance Check output
react-compiler-report.json
react-compiler-output.txt

# Rock Framework
Expand Down
1 change: 0 additions & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const ReactCompilerConfig = {
environment: {
enableTreatRefLikeIdentifiersAsRefs: true,
},
// We exclude 'tests' directory from compilation, but still compile components imported in test files.
sources: (filename) => !filename.includes('tests/') && !filename.includes('node_modules/'),
};

Expand Down
44 changes: 41 additions & 3 deletions contributingGuides/REACT_COMPILER.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,46 @@

At Expensify, we are early adopters of this tool and aim to fully leverage its capabilities.

## React Compiler compatibility check
## React Compiler compliance checker

To check if your code can be compiled by React Compiler and hence gets all its optimizations "for free", you can run the `npm run react-compiler-healthcheck-test` locally and analyze the output.
We provide a script, `scripts/react-compiler-compliance-check.ts`, which checks for "Rules of React" compliance locally and enforces these in PRs adding or changing React code through a CI check.

### What it does

Runs `react-compiler-healthcheck` in verbose mode, parses output, and summarizes which files compiled and which failed, including file, line, column, and reason. It can:

- Check all files or a specific file/glob
- Check only files changed relative to a base branch
- Optionally generate a machine-readable report `react-compiler-report.json`
- Exit with non-zero code when failures are found (useful for CI)

### Usage

> [!NOTE]
> This script uses `origin` as the base remote by default. If your GH remote is named differently, use the `--remote <name>` flag.

#### Check entire codebase or a specific file/glob

```bash
npm run react-compiler-compliance-check check # Check all files
npm run react-compiler-compliance-check check src/path/Component.tsx # Check specific file
npm run react-compiler-compliance-check check "src/**/*.tsx" # Check glob pattern
```

#### Check only changed files against a remote main (default remote is `origin`)

```bash
npm run react-compiler-compliance-check check-changed
```

#### Generate a detailed report (saved as `./react-compiler-report.json`)

You can use the `--report` flag with both of the above commands:

```bash
npm run react-compiler-compliance-check check --report
npm run react-compiler-compliance-check check-changed --report
```

## How can I check what exactly prevents file from successful optimization or whether my fix for passing `react-compiler` actually works?

Expand Down Expand Up @@ -36,6 +73,7 @@ If you encounter this error, you need to add the `Ref` postfix to the variable n
If you added a modification to `SharedValue`, you'll likely encounter this error. You can ignore this error for now because the current `react-native-reanimated` API is not compatible with `react-compiler` rules. Once [this PR](https://github.com/software-mansion/react-native-reanimated/pull/6312) is merged, we'll rewrite the code to be compatible with `react-compiler`. Until then, you can ignore this error.

### Existing manual memoization could not be preserved. [...]

These types of errors usually occur when the calls to `useMemo` that were made manually are too complex for react-compiler to understand. React compiler is still experimental so unfortunately this can happen.

Some specific cases of this error are described below.
Expand Down Expand Up @@ -96,7 +134,7 @@ return (
<ToggleSettingOptionRow
onToggle={item.onToggle}
// ❌ such code triggers the error - `qboConfig?.pendingFields` is an external variable from the closure
// so this code is pretty complicated for `react-compiler` optimizations
// so this code is pretty complicated for `react-compiler` optimizations
pendingAction={settingsPendingAction([item.subscribedSetting], qboConfig?.pendingFields)}
// ❌ such code triggers the error - `qboConfig` is an external variable from the closure
errors={ErrorUtils.getLatestErrorField(qboConfig, item.subscribedSetting)}
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@
"gh-actions-unused-styles": "npx ts-node scripts/findUnusedStyles.ts",
"setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1",
"e2e-test-runner-build": "node --max-old-space-size=8192 node_modules/.bin/ncc build tests/e2e/testRunner.ts -o tests/e2e/dist/",
"react-compiler-healthcheck": "react-compiler-healthcheck --verbose",
"react-compiler-healthcheck-test": "react-compiler-healthcheck --verbose &> react-compiler-output.txt",
"react-compiler-compliance-check": "ts-node scripts/react-compiler-compliance-check.ts",
"generate-search-parser": "peggy --format es -o src/libs/SearchParser/searchParser.js src/libs/SearchParser/searchParser.peggy src/libs/SearchParser/baseRules.peggy",
"generate-autocomplete-parser": "peggy --format es -o src/libs/SearchParser/autocompleteParser.js src/libs/SearchParser/autocompleteParser.peggy src/libs/SearchParser/baseRules.peggy && ./scripts/parser-workletization.sh src/libs/SearchParser/autocompleteParser.js",
"web:prod": "http-server ./dist --cors",
Expand Down
Loading
Loading