|
| 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(); |
0 commit comments