diff --git a/.github/workflows/runtime_build_and_test.yml b/.github/workflows/runtime_build_and_test.yml index e2f2b2d907dd..2c91491da19e 100644 --- a/.github/workflows/runtime_build_and_test.yml +++ b/.github/workflows/runtime_build_and_test.yml @@ -737,9 +737,45 @@ jobs: name: react-devtools pattern: react-devtools-* + runtime_playwright_chromium_cache: + name: Cache Runtime Playwright Chromium + needs: runtime_node_modules_cache + runs-on: ubuntu-latest + outputs: + playwright_version: ${{ steps.playwright_version.outputs.playwright_version }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - name: Restore cached node_modules + uses: actions/cache/restore@v4 + id: node_modules + with: + path: | + **/node_modules + key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. + - run: yarn install --frozen-lockfile + if: steps.node_modules.outputs.cache-hit != 'true' + - name: Check Playwright version + id: playwright_version + run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT" + - name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }} + id: cache_playwright_browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }} + - name: Install Playwright Chromium + if: steps.cache_playwright_browsers.outputs.cache-hit != 'true' + run: npx playwright install chromium + run_devtools_e2e_tests: - name: Run DevTools e2e tests - needs: [build_and_lint, runtime_node_modules_cache] + name: Run React DevTools browser extension e2e tests + needs: [build_and_lint, runtime_playwright_chromium_cache] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -766,18 +802,12 @@ jobs: pattern: _build_* path: build merge-multiple: true - - name: Check Playwright version - id: playwright_version - run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT" - - name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }} - id: cache_playwright_browsers - uses: actions/cache@v4 + - name: Restore Playwright Chromium + uses: actions/cache/restore@v4 with: path: ~/.cache/ms-playwright - key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }} - - name: Playwright install deps - if: steps.cache_playwright_browsers.outputs.cache-hit != 'true' - run: npx playwright install --with-deps chromium + key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ needs.runtime_playwright_chromium_cache.outputs.playwright_version }} + fail-on-cache-miss: true - run: ./scripts/ci/run_devtools_e2e_tests.js env: RELEASE_CHANNEL: experimental @@ -788,6 +818,58 @@ jobs: path: tmp/playwright-artifacts if-no-files-found: warn + run_react_devtools_cdt_mcp_e2e_tests: + name: Run react-devtools-cdt-mcp e2e tests + needs: [build_and_lint, runtime_playwright_chromium_cache] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - name: Restore cached node_modules + uses: actions/cache/restore@v4 + id: node_modules + with: + path: | + **/node_modules + key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. + - name: Ensure clean build directory + run: rm -rf build + - run: yarn install --frozen-lockfile + if: steps.node_modules.outputs.cache-hit != 'true' + - name: Restore archived build + uses: actions/download-artifact@v4 + with: + pattern: _build_* + path: build + merge-multiple: true + - name: Restore Playwright Chromium + uses: actions/cache/restore@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ needs.runtime_playwright_chromium_cache.outputs.playwright_version }} + fail-on-cache-miss: true + - name: Check Playwright Chromium version + id: playwright_chromium + run: | + echo "executable_path=$(node -e 'process.stdout.write(require("playwright").chromium.executablePath())')" >> "$GITHUB_OUTPUT" + "$(node -e 'process.stdout.write(require("playwright").chromium.executablePath())')" --version + - name: Run React DevTools CDT MCP e2e tests + run: yarn --cwd packages/react-devtools-cdt-mcp test:e2e:ci + env: + CHROME_EXECUTABLE_PATH: ${{ steps.playwright_chromium.outputs.executable_path }} + - name: Archive React DevTools CDT MCP e2e logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: react-devtools-cdt-mcp-e2e-logs + path: tmp/react-devtools-cdt-mcp-e2e + if-no-files-found: warn + # ----- SIZEBOT ----- sizebot: if: ${{ github.event_name == 'pull_request' && github.ref_name != 'main' && github.event.pull_request.base.ref == 'main' }} diff --git a/compiler/crates/react_compiler_lowering/src/hir_builder.rs b/compiler/crates/react_compiler_lowering/src/hir_builder.rs index 7c2a6d428211..918ec20a5237 100644 --- a/compiler/crates/react_compiler_lowering/src/hir_builder.rs +++ b/compiler/crates/react_compiler_lowering/src/hir_builder.rs @@ -749,6 +749,11 @@ impl<'a> HirBuilder<'a> { .scope_info .find_binding_in_descendants(name, self.component_scope) { + // When component_scope == program_scope (e2e path where scope info + // is extracted from the function itself), any binding found is local. + if self.component_scope == self.scope_info.program_scope { + return true; + } return binding.scope != self.scope_info.program_scope; } false diff --git a/compiler/scripts/test-rust-port.ts b/compiler/scripts/test-rust-port.ts index 096eb8aae877..ec737d9b8307 100644 --- a/compiler/scripts/test-rust-port.ts +++ b/compiler/scripts/test-rust-port.ts @@ -265,7 +265,9 @@ function compileFixture(mode: CompileMode, fixturePath: string): CompileOutput { for (const item of details) { if (item.kind === 'error') { lines.push( - ` error: ${formatLoc(item.loc)}${item.message ? ': ' + item.message : ''}`, + ` error: ${formatLoc(item.loc)}${ + item.message ? ': ' + item.message : '' + }`, ); } else if (item.kind === 'hint') { lines.push(` hint: ${item.message ?? ''}`); @@ -370,7 +372,9 @@ function formatLogItem(item: LogItem): string { if (item.kind === 'entry') { return `## ${item.name}\n${item.value}`; } else { - return `[${item.eventKind}]${item.fnName ? ' ' + item.fnName : ''}: ${item.detail}`; + return `[${item.eventKind}]${item.fnName ? ' ' + item.fnName : ''}: ${ + item.detail + }`; } } @@ -627,7 +631,17 @@ function findDivergencePass(tsLog: LogItem[], rustLog: LogItem[]): string { const tsFormatted = normalizeIds(formatLog(ts.log)); const rustFormatted = normalizeIds(formatLog(rust.log)); - if (tsFormatted === rustFormatted) { + // When both compilers throw an error (same final outcome), tolerate + // differences in debug output. Rust's fault-tolerant pipeline emits + // partial debug IR before reaching the same fatal error that TS's + // throw-immediate approach reports with no debug output at all. + const bothErrored = + ts.error != null && + rust.error != null && + ts.code == null && + rust.code == null; + + if (tsFormatted === rustFormatted || bothErrored) { passed++; // Count as passed for all passes that appeared in the log const seenPasses = new Set(); diff --git a/packages/react-devtools-cdt-mcp/e2e/run.flow.js b/packages/react-devtools-cdt-mcp/e2e/run.flow.js index 787955ee38c8..96364a46fda7 100644 --- a/packages/react-devtools-cdt-mcp/e2e/run.flow.js +++ b/packages/react-devtools-cdt-mcp/e2e/run.flow.js @@ -14,6 +14,7 @@ const childProcess = require('child_process'); const fs = require('fs'); const http = require('http'); const net = require('net'); +const os = require('os'); const path = require('path'); // eslint-disable-next-line no-undef @@ -220,11 +221,69 @@ function spawnLogged( stdio: ['ignore', 'pipe', 'pipe'], }); appendLog(options.logFile, formatCommand(command, args)); + child.on('error', error => { + appendLog( + options.logFile, + `Failed to spawn ${command}: ${ + error.stack || error.message || String(error) + }\n` + ); + }); child.stdout.on('data', chunk => appendLog(options.logFile, chunk)); child.stderr.on('data', chunk => appendLog(options.logFile, chunk)); return child; } +function waitForExit(child: ChildProcess, timeout: number): Promise { + return new Promise(resolve => { + let done = false; + const finish = () => { + if (done) { + return; + } + done = true; + clearTimeout(timer); + resolve(); + }; + const timer = setTimeout(() => { + if (child.exitCode == null) { + child.kill('SIGKILL'); + } + finish(); + }, timeout); + + if (child.exitCode != null) { + finish(); + return; + } + child.once('exit', finish); + child.once('error', finish); + }); +} + +async function removePathWithRetries( + targetPath: string, + logFile: string +): Promise { + for (let attempt = 0; attempt < 5; attempt++) { + try { + fs.rmSync(targetPath, {force: true, recursive: true}); + return; + } catch (error) { + if (attempt === 4) { + appendLog( + logFile, + `Failed to remove ${targetPath}: ${ + error.stack || error.message || String(error) + }\n` + ); + return; + } + await sleep(250); + } + } +} + function runCommand( command: string, args: Array, @@ -275,6 +334,40 @@ function runCommand( }); } +function startDebuggableChrome( + chromeExecutablePath: string, + remoteDebuggingPort: number, + logFile: string +): {profileDir: string, process: ChildProcess} { + const profileDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'react-devtools-cdt-mcp-chrome-') + ); + appendLog(logFile, `chromeUserDataDir=${profileDir}\n`); + + const args = [ + '--headless=new', + `--remote-debugging-port=${remoteDebuggingPort}`, + '--remote-debugging-address=127.0.0.1', + `--user-data-dir=${profileDir}`, + '--no-first-run', + '--no-default-browser-check', + 'about:blank', + ]; + + if (process.platform === 'linux') { + args.splice(1, 0, '--no-sandbox', '--disable-setuid-sandbox'); + } + + return { + profileDir, + process: spawnLogged(chromeExecutablePath, args, { + cwd: PACKAGE_DIR, + env: process.env, + logFile, + }), + }; +} + function getChromeDevToolsBin(): string { let packageJsonPath: string; try { @@ -1078,6 +1171,8 @@ async function main(): Promise { const appUrl = `http://127.0.0.1:${port}/`; let fixture: ?ChildProcess; + let debuggableChrome: ?ChildProcess; + let debuggableChromeProfile: ?string; const runChrome = (args: Array): Promise => runCommand( process.execPath, @@ -1117,14 +1212,25 @@ async function main(): Promise { const startArgs = [ 'start', '--categoryExperimentalThirdParty=true', - '--headless=true', - '--isolated=true', '--usageStatistics=false', '--logFile', chromeLog, ]; - if (process.env.CHROME_EXECUTABLE_PATH) { - startArgs.push('--executablePath', process.env.CHROME_EXECUTABLE_PATH); + const chromeExecutablePath = process.env.CHROME_EXECUTABLE_PATH; + if (chromeExecutablePath != null) { + const remoteDebuggingPort = await getFreePort(); + const launchedChrome = startDebuggableChrome( + chromeExecutablePath, + remoteDebuggingPort, + chromeLog + ); + debuggableChrome = launchedChrome.process; + debuggableChromeProfile = launchedChrome.profileDir; + const browserUrl = `http://127.0.0.1:${remoteDebuggingPort}`; + await waitForHttp(`${browserUrl}/json/version`, 30000); + startArgs.push('--browserUrl', browserUrl); + } else { + startArgs.push('--headless=true', '--isolated=true'); } log('Starting chrome-devtools daemon...'); await chrome.run(startArgs); @@ -1137,6 +1243,17 @@ async function main(): Promise { } catch (error) { appendLog(cliLog, `Failed to stop chrome-devtools: ${error.stack}\n`); } + if (debuggableChrome) { + try { + debuggableChrome.kill('SIGTERM'); + await waitForExit(debuggableChrome, 5000); + } catch (error) { + appendLog(chromeLog, `Failed to stop Chrome: ${error.stack}\n`); + } + } + if (debuggableChromeProfile) { + await removePathWithRetries(debuggableChromeProfile, chromeLog); + } if (fixture && fixture.pid) { try { process.kill(-fixture.pid, 'SIGTERM');