Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
64 changes: 47 additions & 17 deletions .github/scripts/validate-pr-number.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ module.exports = main;

/**
*
* @param {{filePath:string}} options
* @returns {number}
* @param {Pick<import('../../scripts/triage-bot/src/types.ts').GithubScriptsParams,'github'|'context'> & {filePath:string}} options
* @returns {Promise<number>}
*/
function main(options) {
return validatePrNumber(options.filePath);
async function main(options) {
const { filePath, github, context } = options;
const prNumber = validatePrNumber(filePath);

await validatePrIdentity({ prNumber, github, context });

return prNumber;
}

/**
Expand All @@ -19,18 +24,43 @@ function main(options) {
* @returns {number}
*/
function validatePrNumber(filePath) {
try {
const content = readFileSync(filePath, 'utf-8').trim();
const prNumber = Number(content);

if (isNaN(prNumber) || !Number.isInteger(prNumber)) {
throw new Error('The ID in pr.txt is not a valid PR number.');
}

console.info('✅ PR ID valid');
return prNumber;
} catch (err) {
console.error(`Error: ${err.message}`);
process.exit(1);
const content = readFileSync(filePath, 'utf-8').trim();
const prNumber = Number(content);

if (isNaN(prNumber) || !Number.isInteger(prNumber)) {
throw new Error('The ID in pr.txt is not a valid PR number.');
}

console.info('✅ PR ID valid');
return prNumber;
}

/**
* Validates that the PR number from the artifact matches the workflow run that produced it.
* This prevents a malicious PR from injecting a different PR number in the artifact.
*
* NOTE: This function is designed to run exclusively within `workflow_run` triggered workflows.
* It uses `context.payload.workflow_run.repository` (not `context.repo`) because in a `workflow_run`
* context we need to reference the repository that originated the triggering workflow run.
*
* @param {{prNumber: number} & Pick<import('../../scripts/triage-bot/src/types.ts').GithubScriptsParams,'github'|'context'>} options
*/
async function validatePrIdentity({ prNumber, github, context }) {
const owner = context.payload.workflow_run.repository.owner.login;
const repo = context.payload.workflow_run.repository.name;
const expectedSha = context.payload.workflow_run.head_sha;

const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber,
});

if (pr.head.sha !== expectedSha) {
throw new Error(
`❌ PR #${prNumber} head SHA (${pr.head.sha}) does not match workflow run head SHA (${expectedSha}).`,
);
}

console.info('✅ PR identity verified against workflow run');
}
2 changes: 1 addition & 1 deletion .github/workflows/bundle-size-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
result-encoding: string
script: |
const run = require('./.github/scripts/validate-pr-number');
return run({ filePath: 'results/pr.txt' });
return await run({ filePath: 'results/pr.txt', github, context });

- name: 'Comment on PR'
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
Expand Down
11 changes: 1 addition & 10 deletions .github/workflows/pr-vrt-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
with:
script: |
const run = require('./.github/scripts/validate-pr-number');
const pull_number = run({filePath:'results/pr.txt'});
const pull_number = await run({filePath:'results/pr.txt', github, context});

const result = await github.rest.pulls.get({
owner: context.payload.workflow_run.repository.owner.login,
Expand All @@ -84,15 +84,6 @@ jobs:

const pr = result.data;

if (pr.head.sha !== context.payload.workflow_run.head_commit.id) {
console.log(
"PR head sha does not match the workflow run head commit id. " +
"There has probably been a push to the PR, so we will not run the VR Diff for the last iteration. " +
"Exiting the script."
);
process.exit(0);
}

const headOwner = pr.head.repo.owner.login;
const baseOwner = pr.base.repo.owner.login;

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-website-deploy-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
with:
script: |
const run = require('./.github/scripts/validate-pr-number');
const result = run({filePath:'results/pr.txt'});
const result = await run({filePath:'results/pr.txt', github, context});
return result;
result-encoding: string

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@eslint/compat": "1.3.0",
"@eslint/js": "9.26.0",
"@floating-ui/dom": "1.6.12",
"@fluentui/react-compiler-analyzer": "0.0.0-experimental.rc-analyzer.20260505-0d531266b1.0",
"@fluentui/react-icons": "^2.0.306",
"@griffel/babel-preset": "1.5.8",
"@griffel/eslint-plugin": "^2.0.0",
Expand Down Expand Up @@ -168,6 +169,7 @@
"babel-plugin-iife-wrap-react-components": "1.0.0-5",
"babel-plugin-lodash": "3.3.4",
"babel-plugin-module-resolver": "5.0.0",
"babel-plugin-react-compiler": "1.0.0",
"babel-plugin-tester": "10.1.0",
"beachball": "2.31.0",
"chalk": "4.1.0",
Expand Down
1 change: 1 addition & 0 deletions tools/workspace-plugin/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const fluentPlugin = require('@fluentui/eslint-plugin');

/** @type {import("eslint").Linter.Config[]} */
module.exports = [
{ ignores: ['src/**/__fixtures__/**'] },
...fluentPlugin.configs['flat/node'],
{
rules: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"exclude": [
"/testing",
"/**/*.cy.ts",
"/**/*.cy.tsx",
"/**/*.spec.ts",
"/**/*.spec.tsx",
"/**/*.test.ts",
"/**/*.test.tsx"
],
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": false,
"dynamicImport": false
},
"externalHelpers": true,
"transform": {
"react": {
"runtime": "classic",
"useSpread": true
}
},
"target": "es2019"
},
"minify": false,
"sourceMaps": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "react-compiler-proj"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as React from 'react';

export function Counter({ initialCount = 0 }: { initialCount?: number }) {
const [count, setCount] = React.useState(initialCount);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Counter } from './Counter';
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"moduleResolution": "Node",
"target": "ES2019",
"jsx": "react",
"skipLibCheck": true,
"pretty": true
},
"include": [],
"files": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./dist/out-tsc",
"declaration": true,
"types": []
},
"include": ["src/*.ts", "src/*.tsx"],
"exclude": ["src/*.spec.ts", "src/*.spec.tsx"]
}
116 changes: 116 additions & 0 deletions tools/workspace-plugin/src/executors/build/executor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,4 +389,120 @@ describe('Build Executor', () => {
expect(existsSync(join(workspaceRoot, 'libs/proj/lib-commonjs/greeter.styles.raw.js.map'))).toBe(false);
}, 60000);
});

describe(`#reactCompiler`, () => {
const reactCompilerContext: ExecutorContext = {
root: workspaceRoot,
cwd: process.cwd(),
isVerbose: false,
projectName: 'react-compiler-proj',
projectsConfigurations: {
version: 2,
projects: {
'react-compiler-proj': {
root: 'libs/react-compiler-proj',
name: 'react-compiler-proj',
},
},
},
nxJsonConfiguration: {},
projectGraph: { nodes: {}, dependencies: {} },
};

it('applies react-compiler transforms to ESM output (without styles)', async () => {
jest.spyOn(logger, 'log').mockImplementation(() => {
return;
});
jest.spyOn(logger, 'verbose').mockImplementation(() => {
return;
});

const reactCompilerOptions: BuildExecutorSchema = {
sourceRoot: 'src',
outputPathRoot: 'libs/react-compiler-proj/dist',
moduleOutput: [
{ module: 'es6', outputPath: 'lib' },
{ module: 'commonjs', outputPath: 'lib-commonjs' },
],
assets: [],
generateApi: false,
clean: true,
reactCompiler: true,
};

const output = await executor(reactCompilerOptions, reactCompilerContext);
expect(output.success).toBe(true);

// assert react-compiler memoization cache is present in ESM output
expect(readFileSync(join(workspaceRoot, 'libs/react-compiler-proj/lib/Counter.js'), 'utf-8'))
.toMatchInlineSnapshot(`
"import { c as _c } from \\"react/compiler-runtime\\";
import * as React from 'react';
export function Counter(t0) {
const $ = _c(2);
const { initialCount: t1 } = t0;
const initialCount = t1 === undefined ? 0 : t1;
const [count, setCount] = React.useState(initialCount);
let t2;
if ($[0] !== count) {
t2 = /*#__PURE__*/ React.createElement(\\"div\\", null, /*#__PURE__*/ React.createElement(\\"p\\", null, \\"Count: \\", count), /*#__PURE__*/ React.createElement(\\"button\\", {
onClick: ()=>setCount(count + 1)
}, \\"Increment\\"));
$[0] = count;
$[1] = t2;
} else {
t2 = $[1];
}
return t2;
}
"
`);

// assert intermediate directory is cleaned up
expect(existsSync(join(workspaceRoot, 'libs/react-compiler-proj/temp/react-compiler-intermediate'))).toBe(false);
}, 60000);

it('applies react-compiler before griffel AOT (with styles)', async () => {
jest.spyOn(logger, 'log').mockImplementation(() => {
return;
});
jest.spyOn(logger, 'verbose').mockImplementation(() => {
return;
});

const optionsWithReactCompiler: BuildExecutorSchema = {
...options,
reactCompiler: true,
};

const output = await executor(optionsWithReactCompiler, context);
expect(output.success).toBe(true);

// assert greeter.js ESM output is valid (react-compiler ran on TS source, SWC compiled from intermediate)
expect(readFileSync(join(workspaceRoot, 'libs/proj/lib/greeter.js'), 'utf-8')).toMatchInlineSnapshot(`
"import { useStyles } from './greeter.styles';
export function greeter(greeting, user) {
var _user_hometown;
const styles = useStyles();
return \`<h1 class=\\"\${styles}\\">\${greeting} \${user.name} from \${(_user_hometown = user.hometown) === null || _user_hometown === void 0 ? void 0 : _user_hometown.name}</h1>\`;
}
"
`);

// assert griffel AOT still applied correctly on styles files
expect(readFileSync(join(workspaceRoot, 'libs/proj/lib/greeter.styles.js'), 'utf-8')).toMatchInlineSnapshot(`
"import { __styles } from '@griffel/react';
export const useStyles = /*#__PURE__*/__styles({
root: {
sj55zd: \\"fe3e8s9\\"
}
}, {
d: [\\".fe3e8s9{color:red;}\\"]
});"
`);

// assert intermediate directory is cleaned up
expect(existsSync(join(workspaceRoot, 'libs/proj/temp/react-compiler-intermediate'))).toBe(false);
}, 60000);
});
});
6 changes: 5 additions & 1 deletion tools/workspace-plugin/src/executors/build/executor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type ExecutorContext, type PromiseExecutor } from '@nx/devkit';

import { compileSwc } from './lib/swc';
import { compileWithGriffelStylesAOT, hasStylesFilesToProcess } from './lib/babel';
import { compileWithGriffelStylesAOT, compileWithReactCompiler, hasStylesFilesToProcess } from './lib/babel';
import { assetGlobsToFiles, copyAssets } from './lib/assets';
import { cleanOutput } from './lib/clean';
import { NormalizedOptions, normalizeOptions, processAsyncQueue, runInParallel, runSerially } from './lib/shared';
Expand Down Expand Up @@ -49,6 +49,10 @@ async function runBuild(options: NormalizedOptions, _context: ExecutorContext):
return compileWithGriffelStylesAOT(options);
}

if (options.reactCompiler) {
return compileWithReactCompiler(options);
}

const compilationQueue = options.moduleOutput.map(outputConfig => {
return compileSwc(outputConfig, options);
});
Expand Down
Loading
Loading