Skip to content

Commit 3caf684

Browse files
authored
feat(react-integration-tester): implement rit CLI (microsoft#35172)
1 parent 4d8f87a commit 3caf684

39 files changed

+3061
-1
lines changed

.nxignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
; build output
22
**/dist/**
3+
tmp
34

45
; known directories to not incorporate into the workspace graph creation
56
**/fixtures/**

nx.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@
110110
"codeCoverage": true
111111
}
112112
}
113+
},
114+
"@nx/js:swc": {
115+
"cache": true,
116+
"dependsOn": ["^build"],
117+
"inputs": ["production", "^production"]
113118
}
114119
},
115120
"namedInputs": {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@
237237
"jest-axe": "8.0.0",
238238
"jest-cli": "29.7.0",
239239
"jest-environment-jsdom": "29.7.0",
240+
"jest-environment-node": "^29.7.0",
240241
"jest-environment-node-single-context": "29.1.0",
241242
"jest-snapshot": "29.7.0",
242243
"jest-watch-typeahead": "2.2.2",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"extends": ["plugin:@fluentui/eslint-plugin/node"],
3+
"root": true,
4+
"ignorePatterns": ["!**/*", "**/__fixtures__/**"],
5+
"overrides": [
6+
{
7+
"files": ["**/__tests__/**/*.{ts,tsx,js,jsx}"],
8+
"rules": {
9+
"@typescript-eslint/no-empty-function": "off",
10+
"@typescript-eslint/no-explicit-any": "off",
11+
"@typescript-eslint/no-shadow": "off",
12+
"dot-notation": "off"
13+
}
14+
},
15+
{
16+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
17+
"rules": {
18+
"import/no-extraneous-dependencies": [
19+
"error",
20+
{
21+
"packageDir": [".", "../../"]
22+
}
23+
]
24+
}
25+
},
26+
{
27+
"files": ["*.ts", "*.tsx"],
28+
"rules": {}
29+
},
30+
{
31+
"files": ["*.js", "*.jsx"],
32+
"rules": {}
33+
},
34+
{
35+
"files": ["*.json"],
36+
"parser": "jsonc-eslint-parser",
37+
"rules": {}
38+
}
39+
]
40+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"jsc": {
3+
"target": "es2017",
4+
"parser": {
5+
"syntax": "typescript",
6+
"decorators": true,
7+
"dynamicImport": true
8+
},
9+
"transform": {
10+
"decoratorMetadata": true,
11+
"legacyDecorator": true
12+
},
13+
"keepClassNames": true,
14+
"externalHelpers": true,
15+
"loose": true
16+
},
17+
"module": {
18+
"type": "commonjs"
19+
},
20+
"sourceMaps": true,
21+
"exclude": [
22+
"jest.config.ts",
23+
".*\\.spec.tsx?$",
24+
".*\\.test.tsx?$",
25+
"./src/jest-setup.ts$",
26+
"./**/jest-setup.ts$",
27+
".*.js$"
28+
]
29+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# react-integration-tester (rit)
2+
3+
CLI to scaffold and run integration checks for multiple React majors (17/18/19) using YOUR project's existing test tooling (Jest / Cypress / TypeScript). It re‑uses a single shared dependency install per React major and creates cheap throw‑away (or reusable) scaffold projects exposing exactly three scripts: `test`, `type-check`, `e2e`.
4+
5+
## Essentials
6+
7+
React majors support: 17, 18, 19
8+
Runners support: `test` (Jest), `type-check` (TypeScript), `e2e` (Cypress)
9+
Shared root: `<repoRoot>/tmp/rit/react-<major>/` (deps installed once)
10+
Prepared project naming:
11+
12+
```
13+
<repoRoot>/tmp/rit/react-<major>/<origin-project-name>-react-<major>-<project-id>
14+
```
15+
16+
When you omit `--project-id`, a random suffix is used instead of `<project-id>`, e.g.
17+
18+
```
19+
button-react-18-7423891
20+
```
21+
22+
This deterministic pattern (when you pass `--project-id`) lets you reliably re‑use a scaffold across multiple `--run` invocations without re‑preparing.
23+
24+
## How it works
25+
26+
```mermaid
27+
flowchart LR
28+
START((Start)) --> A["A: Install dependencies<br/>shared react-<major> root<br/><code>tmp/rit/react-<major></code>"]
29+
A --> B["B: Scaffold project<br/><code><origin>-react-<major>-<id></code>"]
30+
B --> C{C: Run selected scripts}
31+
C -->|test| T["test (Jest)"]
32+
C -->|type-check| TC["type-check (tsc)"]
33+
C -->|e2e| E2E["e2e (Cypress)"]
34+
T --> END((End))
35+
TC --> END
36+
E2E --> END
37+
classDef phase fill:#eef,stroke:#447,stroke-width:1px;
38+
class A,B,C phase;
39+
```
40+
41+
Legend:
42+
43+
- A runs when you use `--install-deps`, or implicitly during a prepare/run unless you pass `--no-install` with prior deps installed.
44+
- B runs on `--prepare-only` or any one‑shot run without `--project-id` (temporary) or with `--project-id` (reusable).
45+
- C executes only the scripts you explicitly list via repeated `--run` flags, sequentially.
46+
47+
## Quick start
48+
49+
```bash
50+
# Prepare once (install deps -> generate scaffold)
51+
rit --react 18 --prepare-only --project-id demo
52+
53+
# Run scripts (re-uses scaffold + node_modules -> run)
54+
rit --react 18 --project-id demo --run test --run type-check
55+
```
56+
57+
One‑shot run (install -> prepare -> run -> cleanup):
58+
59+
```bash
60+
rit --react 19 --run test
61+
```
62+
63+
Install deps only (CI cache warm):
64+
65+
```bash
66+
rit --react 17 --install-deps
67+
```
68+
69+
## Usage
70+
71+
```bash
72+
rit --react <17|18|19> \
73+
[--project-id <id>] [--config <file>] [--cwd <path>] \
74+
[--prepare-only [--no-install] [--force]] \
75+
[--install-deps] \
76+
[--run <test|type-check|e2e> ...] \
77+
[--cleanup|--no-cleanup] [--verbose]
78+
```
79+
80+
Flag reference (implementation accurate):
81+
82+
- `--react` (required for every invocation) React major.
83+
- `--project-id` stable suffix enabling scaffold reuse. Required when using `--prepare-only`. Optional otherwise (random suffix used if omitted).
84+
- `--config` path to a config module (CommonJS or ESM). Defaults to `./rit.config.js` if present.
85+
- `--prepare-only` create/update a scaffold (and install unless `--no-install`). Does NOT run scripts.
86+
- `--no-install` (only valid with `--prepare-only`) skips dependency installation. Requires that dependencies have already been installed earlier via `--install-deps` for that React major; otherwise it fails fast.
87+
- `--install-deps` install/update shared deps for the React major root and exit (no scaffold created).
88+
- `--run <script>` select one or more scripts to execute sequentially. Repeat the flag for multiple (e.g. `--run test --run type-check`). You must list all you want; there is no implicit “run all”.
89+
- `--force` (with `--prepare-only`) delete existing scaffold with the same computed name before recreating it.
90+
- `--cleanup` / `--no-cleanup` (default: cleanup) remove the temporary project after running. When reusing via `--run` + `--project-id`, cleanup is effectively a no‑op; the scaffold remains unless you prepared and then ran in one shot without reuse.
91+
- `--verbose` extra logging including resolved metadata.
92+
- `--cwd` working directory for discovering the origin project (defaults to process cwd).
93+
94+
Rules & guardrails:
95+
96+
- `--run` cannot be combined with `--prepare-only` or `--no-install`.
97+
- `--prepare-only` requires `--project-id` (deterministic reuse is a core feature).
98+
- You must pass at least one `--run` OR use `--prepare-only` OR `--install-deps`.
99+
- Reusing (`--run ... --project-id <id>`) requires that dependencies are already installed for that React root (`rit --install-deps --react <major>` or a prior prepare/run that installed them).
100+
101+
## Configuration (`rit.config.js`)
102+
103+
If you need to tweak defaults you have 2 options:
104+
105+
- create `rit.config.js` within your project root
106+
- create any js module that follows RIT Config API and point `rit` to it via `--config='<path_to_your_config_module>'`
107+
108+
Override per‑major React version commands and add/override dependencies. Merge is shallow: template defaults + your overrides.
109+
110+
**Config API:**
111+
112+
```ts
113+
export interface ReactOverrides {
114+
commands?: {
115+
test?: string; // maps to script "test"
116+
typeCheck?: string; // maps to script "type-check"
117+
e2e?: string; // maps to script "e2e"
118+
};
119+
dependencies?: Record<string, string>; // added to shared root
120+
}
121+
122+
export interface Config {
123+
react: {
124+
17?: ReactOverrides;
125+
18?: ReactOverrides;
126+
19?: ReactOverrides;
127+
};
128+
}
129+
```
130+
131+
**Minimal config example:**
132+
133+
In your project root create `rit.config.js`
134+
135+
```js
136+
/** @type {import('@fluentui/react-integration-tester').Config} */
137+
module.exports = {
138+
react: {
139+
17: {
140+
commands: {
141+
// maps to package.json script "test"
142+
test: 'jest --passWithNoTests -u --testPathIgnorePatterns test-file-that-wont-work-in-react-17.test.tsx',
143+
},
144+
// installed once under ./tmp/rit/react-17/node_modules
145+
dependencies: { 'some-package': '^1.2.3' },
146+
},
147+
},
148+
};
149+
```
150+
151+
Script key mapping (config camelCase → scaffold script name): `test``test`, `typeCheck``type-check`, `e2e``e2e`.
152+
153+
Only these three script names are runnable (`--run` choices). Additional scripts you add to the template are ignored by the CLI selector.
154+
155+
## Typical CI flow
156+
157+
Install dependencies for your React major under test (cache warm; safe to run repeatedly—it only rewrites/reacts when deps change):
158+
159+
```bash
160+
rit --react 18 --install-deps
161+
```
162+
163+
From project root:
164+
165+
```bash
166+
# prepare project with known suffix so we can target it against --run (depends already installed)
167+
rit --react 18 --prepare-only --no-install --project-id ci
168+
169+
# execute following in parallel via your task runner for best performance
170+
rit --react 18 --project-id ci --run test
171+
rit --react 18 --project-id ci --run type-check
172+
```
173+
174+
## Multiple runners
175+
176+
```bash
177+
rit --react 19 --project-id all --run test --run type-check --run e2e # runs sequentially
178+
# Partial subset
179+
rit --react 19 --project-id fast --run type-check
180+
```
181+
182+
## Project naming & reuse
183+
184+
Given a source package `@scope/button` and `--react 18 --project-id demo` the scaffold will live at:
185+
186+
```text
187+
<repoRoot>/tmp/rit/react-18/button-react-18-demo
188+
```
189+
190+
Reusing later:
191+
192+
```bash
193+
rit --react 18 --project-id demo --run test
194+
```
195+
196+
If you omit `--project-id` the name ends with a random number and is NOT intended for reuse.
197+
198+
## Troubleshooting
199+
200+
- Reset cache (all scaffolds + deps for a major): delete `<repoRoot>/tmp/rit/react-<major>/`.
201+
- Template or dependency change: run `rit --react <major> --install-deps` (updates root package.json & installs if needed).
202+
- Missing script: confirm it exists in merged template AND origin project tooling (e.g. Jest/Cypress present). You must invoke with kebab-case (`type-check`).
203+
- Using `--prepare-only --no-install` without prior `--install-deps` will fail intentionally to prevent unusable scaffolds.
204+
- Keep a scaffold for iterative local debugging: run with `--no-cleanup` (except when reusing via `--project-id`, cleanup is already a no-op).
205+
- Force a clean re‑prepare (discard old scaffold): add `--force`.
206+
207+
## Contributing
208+
209+
Inside this repo only:
210+
211+
- Build: `nx run react-integration-tester:build`
212+
- Tests: `nx test react-integration-tester:test`
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env node
2+
3+
// @ts-check
4+
5+
const { joinPathFragments } = require('@nx/devkit');
6+
const { registerTsProject } = require('@nx/js/src/internal');
7+
8+
registerTsProject(joinPathFragments(__dirname, '..', 'tsconfig.lib.json'));
9+
10+
const { cli } = require('../src/cli');
11+
12+
cli().catch(err => {
13+
console.error(err instanceof Error ? err.message : err);
14+
process.exit(1);
15+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* eslint-disable */
2+
import { readFileSync } from 'fs';
3+
4+
// Reading the SWC compilation config and remove the "exclude"
5+
// for the test files to be compiled by SWC
6+
const { exclude: _, ...swcJestConfig } = JSON.parse(readFileSync(`${__dirname}/.swcrc`, 'utf-8'));
7+
8+
// disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves.
9+
// If we do not disable this, SWC Core will read .swcrc and won't transform our test files due to "exclude"
10+
if (swcJestConfig.swcrc === undefined) {
11+
swcJestConfig.swcrc = false;
12+
}
13+
14+
// Uncomment if using global setup/teardown files being transformed via swc
15+
// https://nx.dev/nx-api/jest/documents/overview#global-setupteardown-with-nx-libraries
16+
// jest needs EsModule Interop to find the default exported setup/teardown functions
17+
// swcJestConfig.module.noInterop = false;
18+
19+
export default {
20+
displayName: 'react-integration-tester',
21+
preset: '../../jest.preset.js',
22+
transform: {
23+
'^.+\\.[tj]s$': ['@swc/jest', swcJestConfig],
24+
},
25+
moduleFileExtensions: ['ts', 'js', 'html'],
26+
testEnvironment: 'node',
27+
coverageDirectory: '../../coverage/tools/react-integration-tester',
28+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "@fluentui/react-integration-tester",
3+
"version": "0.0.1",
4+
"private": true,
5+
"type": "commonjs",
6+
"main": "./src/index.js",
7+
"types": "./src/index.d.ts",
8+
"bin": {
9+
"rit": "./bin/rit.js"
10+
},
11+
"dependencies": {
12+
"@swc/helpers": "~0.5.1",
13+
"ejs": "^3.1.10",
14+
"yargs": "^17.7.2"
15+
}
16+
}

0 commit comments

Comments
 (0)