| 1 | #!/usr/bin/env node |
| 2 | /** |
| 3 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 4 | * |
| 5 | * This source code is licensed under the MIT license found in the |
| 6 | * LICENSE file in the root directory of this source tree. |
| 7 | */ |
| 8 | |
| 9 | 'use strict'; |
| 10 | |
| 11 | const fs = require('fs'); |
| 12 | const path = require('path'); |
| 13 | const {execSync} = require('child_process'); |
| 14 | const yargs = require('yargs/yargs'); |
| 15 | const {hideBin} = require('yargs/helpers'); |
| 16 | |
| 17 | // Constants |
| 18 | const COMPILER_ROOT = path.resolve(__dirname, '..'); |
| 19 | const ENVIRONMENT_TS_PATH = path.join( |
| 20 | COMPILER_ROOT, |
| 21 | 'packages/babel-plugin-react-compiler/src/HIR/Environment.ts' |
| 22 | ); |
| 23 | const FIXTURES_PATH = path.join( |
| 24 | COMPILER_ROOT, |
| 25 | 'packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler' |
| 26 | ); |
| 27 | const FIXTURE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx']; |
| 28 | |
| 29 | /** |
| 30 | * Parse command line arguments |
| 31 | */ |
| 32 | function parseArgs() { |
| 33 | const argv = yargs(hideBin(process.argv)) |
| 34 | .usage('Usage: $0 <flag-name>') |
| 35 | .command('$0 <flag-name>', 'Enable a feature flag by default', yargs => { |
| 36 | yargs.positional('flag-name', { |
| 37 | describe: 'Name of the feature flag to enable', |
| 38 | type: 'string', |
| 39 | }); |
| 40 | }) |
| 41 | .example( |
| 42 | '$0 validateExhaustiveMemoizationDependencies', |
| 43 | 'Enable the validateExhaustiveMemoizationDependencies flag' |
| 44 | ) |
| 45 | .help('h') |
| 46 | .alias('h', 'help') |
| 47 | .strict() |
| 48 | .parseSync(); |
| 49 | |
| 50 | return argv['flag-name']; |
| 51 | } |
| 52 | |
| 53 | /** |
| 54 | * Enable a feature flag in Environment.ts by changing default(false) to default(true) |
| 55 | */ |
| 56 | function enableFlagInEnvironment(flagName) { |
| 57 | console.log(`\nEnabling flag "${flagName}" in Environment.ts...`); |
| 58 | |
| 59 | const content = fs.readFileSync(ENVIRONMENT_TS_PATH, 'utf8'); |
| 60 | |
| 61 | // Check if the flag exists with default(false) |
| 62 | const flagPatternFalse = new RegExp( |
| 63 | `(${escapeRegex(flagName)}:\\s*z\\.boolean\\(\\)\\.default\\()false(\\))`, |
| 64 | 'g' |
| 65 | ); |
| 66 | |
| 67 | if (!flagPatternFalse.test(content)) { |
| 68 | // Check if flag exists at all |
| 69 | const flagExistsPattern = new RegExp( |
| 70 | `${escapeRegex(flagName)}:\\s*z\\.boolean\\(\\)`, |
| 71 | 'g' |
| 72 | ); |
| 73 | if (flagExistsPattern.test(content)) { |
| 74 | // Check if it's already true |
| 75 | const flagPatternTrue = new RegExp( |
| 76 | `${escapeRegex(flagName)}:\\s*z\\.boolean\\(\\)\\.default\\(true\\)`, |
| 77 | 'g' |
| 78 | ); |
| 79 | if (flagPatternTrue.test(content)) { |
| 80 | console.error(`Error: Flag "${flagName}" already has default(true)`); |
| 81 | process.exit(1); |
| 82 | } |
| 83 | console.error( |
| 84 | `Error: Flag "${flagName}" exists but doesn't have default(false)` |
| 85 | ); |
| 86 | process.exit(1); |
| 87 | } |
| 88 | console.error(`Error: Flag "${flagName}" not found in Environment.ts`); |
| 89 | process.exit(1); |
| 90 | } |
| 91 | |
| 92 | // Perform the replacement |
| 93 | const newContent = content.replace(flagPatternFalse, '$1true$2'); |
| 94 | |
| 95 | // Verify the replacement worked |
| 96 | if (content === newContent) { |
| 97 | console.error(`Error: Failed to replace flag "${flagName}"`); |
| 98 | process.exit(1); |
| 99 | } |
| 100 | |
| 101 | fs.writeFileSync(ENVIRONMENT_TS_PATH, newContent, 'utf8'); |
| 102 | console.log(`Successfully enabled "${flagName}" in Environment.ts`); |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Helper to escape regex special characters |
| 107 | */ |
| 108 | function escapeRegex(string) { |
| 109 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| 110 | } |
| 111 | |
| 112 | /** |
| 113 | * Run yarn snap and capture output |
| 114 | */ |
| 115 | function runTests() { |
| 116 | console.log('\nRunning test suite (yarn snap)...'); |
| 117 | |
| 118 | try { |
| 119 | const output = execSync('yarn snap', { |
| 120 | cwd: COMPILER_ROOT, |
| 121 | encoding: 'utf8', |
| 122 | stdio: 'pipe', |
| 123 | maxBuffer: 10 * 1024 * 1024, // 10MB buffer |
| 124 | }); |
| 125 | return {success: true, output}; |
| 126 | } catch (error) { |
| 127 | // yarn snap exits with code 1 when tests fail, which throws an error |
| 128 | return {success: false, output: error.stdout || error.message}; |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Parse failing test names from test output |
| 134 | */ |
| 135 | function parseFailingTests(output) { |
| 136 | const failingTests = []; |
| 137 | |
| 138 | // Look for lines that contain "FAIL:" followed by the test name |
| 139 | // Format: "FAIL: test-name" or with ANSI codes |
| 140 | const lines = output.split('\n'); |
| 141 | for (const line of lines) { |
| 142 | // Remove ANSI codes for easier parsing |
| 143 | const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, ''); |
| 144 | |
| 145 | // Match "FAIL: test-name" |
| 146 | const match = cleanLine.match(/^FAIL:\s*(.+)$/); |
| 147 | if (match) { |
| 148 | failingTests.push(match[1].trim()); |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | return failingTests; |
| 153 | } |
| 154 | |
| 155 | /** |
| 156 | * Find the fixture file for a given test name |
| 157 | */ |
| 158 | function findFixtureFile(testName) { |
| 159 | const basePath = path.join(FIXTURES_PATH, testName); |
| 160 | |
| 161 | for (const ext of FIXTURE_EXTENSIONS) { |
| 162 | const filePath = basePath + ext; |
| 163 | if (fs.existsSync(filePath)) { |
| 164 | return filePath; |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | return null; |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * Add pragma to disable the feature flag in a fixture file |
| 173 | */ |
| 174 | function addPragmaToFixture(filePath, flagName) { |
| 175 | const content = fs.readFileSync(filePath, 'utf8'); |
| 176 | const lines = content.split('\n'); |
| 177 | |
| 178 | if (lines.length === 0) { |
| 179 | console.warn(`Warning: Empty file ${filePath}`); |
| 180 | return false; |
| 181 | } |
| 182 | |
| 183 | const firstLine = lines[0]; |
| 184 | const pragma = `@${flagName}:false`; |
| 185 | |
| 186 | // Check if pragma already exists |
| 187 | if (firstLine.includes(pragma)) { |
| 188 | return false; // Already has the pragma |
| 189 | } |
| 190 | |
| 191 | // Check if first line is a single-line comment |
| 192 | if (firstLine.trim().startsWith('//')) { |
| 193 | // Append pragma to existing comment |
| 194 | lines[0] = firstLine + ' ' + pragma; |
| 195 | } else if (firstLine.trim().startsWith('/*')) { |
| 196 | // Multi-line comment - insert new line before it |
| 197 | lines.unshift('// ' + pragma); |
| 198 | } else { |
| 199 | // No comment - insert new comment as first line |
| 200 | lines.unshift('// ' + pragma); |
| 201 | } |
| 202 | |
| 203 | fs.writeFileSync(filePath, lines.join('\n'), 'utf8'); |
| 204 | return true; |
| 205 | } |
| 206 | |
| 207 | /** |
| 208 | * Update snapshot files |
| 209 | */ |
| 210 | function updateSnapshots() { |
| 211 | console.log('\nUpdating snapshots (yarn snap -u)...'); |
| 212 | |
| 213 | try { |
| 214 | execSync('yarn snap -u', { |
| 215 | cwd: COMPILER_ROOT, |
| 216 | encoding: 'utf8', |
| 217 | stdio: 'pipe', |
| 218 | maxBuffer: 10 * 1024 * 1024, |
| 219 | }); |
| 220 | console.log('Snapshots updated successfully'); |
| 221 | return true; |
| 222 | } catch (error) { |
| 223 | console.error('Error updating snapshots:', error.message); |
| 224 | return false; |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | /** |
| 229 | * Verify all tests pass |
| 230 | */ |
| 231 | function verifyAllTestsPass() { |
| 232 | console.log('\nRunning final verification (yarn snap)...'); |
| 233 | |
| 234 | const {success, output} = runTests(); |
| 235 | |
| 236 | // Parse summary line: "N Tests, N Passed, N Failed" |
| 237 | const summaryMatch = output.match( |
| 238 | /(\d+)\s+Tests,\s+(\d+)\s+Passed,\s+(\d+)\s+Failed/ |
| 239 | ); |
| 240 | |
| 241 | if (summaryMatch) { |
| 242 | const [, total, passed, failed] = summaryMatch; |
| 243 | console.log( |
| 244 | `\nTest Results: ${total} Tests, ${passed} Passed, ${failed} Failed` |
| 245 | ); |
| 246 | |
| 247 | if (failed === '0') { |
| 248 | console.log('All tests passed!'); |
| 249 | return true; |
| 250 | } else { |
| 251 | console.error(`${failed} tests still failing`); |
| 252 | const failingTests = parseFailingTests(output); |
| 253 | if (failingTests.length > 0) { |
| 254 | console.error('\nFailing tests:'); |
| 255 | failingTests.forEach(test => console.error(` - ${test}`)); |
| 256 | } |
| 257 | return false; |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | return success; |
| 262 | } |
| 263 | |
| 264 | /** |
| 265 | * Main function |
| 266 | */ |
| 267 | async function main() { |
| 268 | const flagName = parseArgs(); |
| 269 | |
| 270 | console.log(`\nEnabling flag: '${flagName}'`); |
| 271 | |
| 272 | try { |
| 273 | // Step 1: Enable flag in Environment.ts |
| 274 | enableFlagInEnvironment(flagName); |
| 275 | |
| 276 | // Step 2: Run tests to find failures |
| 277 | const {output} = runTests(); |
| 278 | const failingTests = parseFailingTests(output); |
| 279 | |
| 280 | console.log(`\nFound ${failingTests.length} failing tests`); |
| 281 | |
| 282 | if (failingTests.length === 0) { |
| 283 | console.log('No failing tests! Feature flag enabled successfully.'); |
| 284 | process.exit(0); |
| 285 | } |
| 286 | |
| 287 | // Step 3: Add pragma to each failing fixture |
| 288 | console.log(`\nAdding '@${flagName}:false' pragma to failing fixtures...`); |
| 289 | |
| 290 | const notFound = []; |
| 291 | let notFoundCount = 0; |
| 292 | |
| 293 | for (const testName of failingTests) { |
| 294 | const fixturePath = findFixtureFile(testName); |
| 295 | |
| 296 | if (!fixturePath) { |
| 297 | console.warn(`Could not find fixture file for: ${testName}`); |
| 298 | notFound.push(fixturePath); |
| 299 | continue; |
| 300 | } |
| 301 | |
| 302 | const updated = addPragmaToFixture(fixturePath, flagName); |
| 303 | if (updated) { |
| 304 | updatedCount++; |
| 305 | console.log(` Updated: ${testName}`); |
| 306 | } |
| 307 | } |
| 308 | |
| 309 | console.log( |
| 310 | `\nSummary: Updated ${updatedCount} fixtures, ${notFoundCount} not found` |
| 311 | ); |
| 312 | |
| 313 | if (notFoundCount.length !== 0) { |
| 314 | console.error( |
| 315 | '\nFailed to update snapshots, could not find:\n' + notFound.join('\n') |
| 316 | ); |
| 317 | process.exit(1); |
| 318 | } |
| 319 | |
| 320 | // Step 4: Update snapshots |
| 321 | if (!updateSnapshots()) { |
| 322 | console.error('\nFailed to update snapshots'); |
| 323 | process.exit(1); |
| 324 | } |
| 325 | |
| 326 | // Step 5: Verify all tests pass |
| 327 | if (!verifyAllTestsPass()) { |
| 328 | console.error('\nVerification failed: Some tests are still failing'); |
| 329 | process.exit(1); |
| 330 | } |
| 331 | |
| 332 | console.log('\nSuccess! Feature flag enabled and all tests passing.'); |
| 333 | console.log(`\nSummary:`); |
| 334 | console.log(` - Enabled "${flagName}" in Environment.ts`); |
| 335 | console.log(` - Updated ${updatedCount} fixture files with pragma`); |
| 336 | console.log(` - All tests passing`); |
| 337 | |
| 338 | process.exit(0); |
| 339 | } catch (error) { |
| 340 | console.error('\nFatal error:', error.message); |
| 341 | console.error(error.stack); |
| 342 | process.exit(1); |
| 343 | } |
| 344 | } |
| 345 | |
| 346 | // Run the script |
| 347 | main(); |